kimaki 0.4.44 → 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 +54 -37
- 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 +2 -2
- package/dist/commands/worktree.js +132 -25
- package/dist/database.js +49 -0
- package/dist/discord-bot.js +24 -38
- 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 +4 -0
- package/dist/session-handler.js +541 -413
- 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 +63 -45
- 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 +2 -2
- package/src/commands/worktree.ts +160 -27
- package/src/database.ts +65 -0
- package/src/discord-bot.ts +26 -42
- 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 +5 -0
- package/src/session-handler.ts +669 -436
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// extracts channel metadata from topic tags, and ensures category structure.
|
|
4
4
|
import { ChannelType } from 'discord.js';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { getDatabase } from './database.js';
|
|
7
|
-
import { extractTagsArrays } from './xml.js';
|
|
6
|
+
import { getDatabase, getChannelDirectory } from './database.js';
|
|
8
7
|
export async function ensureKimakiCategory(guild, botName) {
|
|
9
8
|
// Skip appending bot name if it's already "kimaki" to avoid "Kimaki kimaki"
|
|
10
9
|
const isKimakiBot = botName?.toLowerCase() === 'kimaki';
|
|
@@ -53,7 +52,7 @@ export async function createProjectChannels({ guild, projectDirectory, appId, bo
|
|
|
53
52
|
name: channelName,
|
|
54
53
|
type: ChannelType.GuildText,
|
|
55
54
|
parent: kimakiCategory,
|
|
56
|
-
topic
|
|
55
|
+
// Channel configuration is stored in SQLite, not in the topic
|
|
57
56
|
});
|
|
58
57
|
const voiceChannel = await guild.channels.create({
|
|
59
58
|
name: channelName,
|
|
@@ -79,22 +78,14 @@ export async function getChannelsWithDescriptions(guild) {
|
|
|
79
78
|
.forEach((channel) => {
|
|
80
79
|
const textChannel = channel;
|
|
81
80
|
const description = textChannel.topic || null;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (description) {
|
|
85
|
-
const extracted = extractTagsArrays({
|
|
86
|
-
xml: description,
|
|
87
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
88
|
-
});
|
|
89
|
-
kimakiDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
90
|
-
kimakiApp = extracted['kimaki.app']?.[0]?.trim();
|
|
91
|
-
}
|
|
81
|
+
// Get channel config from database instead of parsing XML from topic
|
|
82
|
+
const channelConfig = getChannelDirectory(textChannel.id);
|
|
92
83
|
channels.push({
|
|
93
84
|
id: textChannel.id,
|
|
94
85
|
name: textChannel.name,
|
|
95
86
|
description,
|
|
96
|
-
kimakiDirectory,
|
|
97
|
-
kimakiApp,
|
|
87
|
+
kimakiDirectory: channelConfig?.directory,
|
|
88
|
+
kimakiApp: channelConfig?.appId || undefined,
|
|
98
89
|
});
|
|
99
90
|
});
|
|
100
91
|
return channels;
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { cac } from 'cac';
|
|
6
6
|
import { intro, outro, text, password, note, cancel, isCancel, confirm, log, multiselect, spinner, } from '@clack/prompts';
|
|
7
7
|
import { deduplicateByKey, generateBotInstallUrl, abbreviatePath } from './utils.js';
|
|
8
|
-
import { getChannelsWithDescriptions, createDiscordClient, getDatabase, startDiscordBot, initializeOpencodeForDirectory, ensureKimakiCategory, createProjectChannels, } from './discord-bot.js';
|
|
8
|
+
import { getChannelsWithDescriptions, createDiscordClient, getDatabase, getChannelDirectory, startDiscordBot, initializeOpencodeForDirectory, ensureKimakiCategory, createProjectChannels, } from './discord-bot.js';
|
|
9
9
|
import { Events, ChannelType, REST, Routes, SlashCommandBuilder, AttachmentBuilder, } from 'discord.js';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import fs from 'node:fs';
|
|
@@ -15,7 +15,6 @@ import { uploadFilesToDiscord } from './discord-utils.js';
|
|
|
15
15
|
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
16
16
|
import http from 'node:http';
|
|
17
17
|
import { setDataDir, getDataDir, getLockPort } from './config.js';
|
|
18
|
-
import { extractTagsArrays } from './xml.js';
|
|
19
18
|
import { sanitizeAgentName } from './commands/agent.js';
|
|
20
19
|
const cliLogger = createLogger('CLI');
|
|
21
20
|
const cli = cac('kimaki');
|
|
@@ -161,12 +160,12 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
161
160
|
.toJSON(),
|
|
162
161
|
new SlashCommandBuilder()
|
|
163
162
|
.setName('new-worktree')
|
|
164
|
-
.setDescription('Create a new git worktree
|
|
163
|
+
.setDescription('Create a new git worktree (in thread: uses thread name if no name given)')
|
|
165
164
|
.addStringOption((option) => {
|
|
166
165
|
option
|
|
167
166
|
.setName('name')
|
|
168
|
-
.setDescription('Name for
|
|
169
|
-
.setRequired(
|
|
167
|
+
.setDescription('Name for worktree (optional in threads - uses thread name)')
|
|
168
|
+
.setRequired(false);
|
|
170
169
|
return option;
|
|
171
170
|
})
|
|
172
171
|
.toJSON(),
|
|
@@ -258,6 +257,18 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
258
257
|
.setName('redo')
|
|
259
258
|
.setDescription('Redo previously undone changes')
|
|
260
259
|
.toJSON(),
|
|
260
|
+
new SlashCommandBuilder()
|
|
261
|
+
.setName('verbosity')
|
|
262
|
+
.setDescription('Set output verbosity for new sessions in this channel')
|
|
263
|
+
.addStringOption((option) => {
|
|
264
|
+
option
|
|
265
|
+
.setName('level')
|
|
266
|
+
.setDescription('Verbosity level')
|
|
267
|
+
.setRequired(true)
|
|
268
|
+
.addChoices({ name: 'tools-and-text (default)', value: 'tools-and-text' }, { name: 'text-only', value: 'text-only' });
|
|
269
|
+
return option;
|
|
270
|
+
})
|
|
271
|
+
.toJSON(),
|
|
261
272
|
];
|
|
262
273
|
// Add user-defined commands with -cmd suffix
|
|
263
274
|
for (const cmd of userCommands) {
|
|
@@ -1021,20 +1032,13 @@ cli
|
|
|
1021
1032
|
throw new Error(`Discord API error: ${channelResponse.status} - ${error}`);
|
|
1022
1033
|
}
|
|
1023
1034
|
const channelData = (await channelResponse.json());
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const extracted = extractTagsArrays({
|
|
1029
|
-
xml: channelData.topic,
|
|
1030
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
1031
|
-
});
|
|
1032
|
-
const projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
1033
|
-
const channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
1034
|
-
if (!projectDirectory) {
|
|
1035
|
-
s.stop('No kimaki.directory tag found');
|
|
1036
|
-
throw new Error(`Channel #${channelData.name} has no <kimaki.directory> tag in topic.`);
|
|
1035
|
+
const channelConfig = getChannelDirectory(channelData.id);
|
|
1036
|
+
if (!channelConfig) {
|
|
1037
|
+
s.stop('Channel not configured');
|
|
1038
|
+
throw new Error(`Channel #${channelData.name} is not configured with a project directory. Run the bot first to sync channel data.`);
|
|
1037
1039
|
}
|
|
1040
|
+
const projectDirectory = channelConfig.directory;
|
|
1041
|
+
const channelAppId = channelConfig.appId || undefined;
|
|
1038
1042
|
// Verify app ID matches if both are present
|
|
1039
1043
|
if (channelAppId && appId && channelAppId !== appId) {
|
|
1040
1044
|
s.stop('Channel belongs to different bot');
|
|
@@ -1199,23 +1203,7 @@ cli
|
|
|
1199
1203
|
process.exit(EXIT_NO_RESTART);
|
|
1200
1204
|
}
|
|
1201
1205
|
const s = spinner();
|
|
1202
|
-
s.start('
|
|
1203
|
-
// Check if channel already exists
|
|
1204
|
-
try {
|
|
1205
|
-
const db = getDatabase();
|
|
1206
|
-
const existingChannel = db
|
|
1207
|
-
.prepare('SELECT channel_id FROM channel_directories WHERE directory = ? AND channel_type = ? AND app_id = ?')
|
|
1208
|
-
.get(absolutePath, 'text', appId);
|
|
1209
|
-
if (existingChannel) {
|
|
1210
|
-
s.stop('Channel already exists');
|
|
1211
|
-
note(`Channel already exists for this directory.\n\nChannel: <#${existingChannel.channel_id}>\nDirectory: ${absolutePath}`, '⚠️ Already Exists');
|
|
1212
|
-
process.exit(0);
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
catch {
|
|
1216
|
-
// Database might not exist, continue to create
|
|
1217
|
-
}
|
|
1218
|
-
s.message('Connecting to Discord...');
|
|
1206
|
+
s.start('Connecting to Discord...');
|
|
1219
1207
|
const client = await createDiscordClient();
|
|
1220
1208
|
await new Promise((resolve, reject) => {
|
|
1221
1209
|
client.once(Events.ClientReady, () => {
|
|
@@ -1228,10 +1216,14 @@ cli
|
|
|
1228
1216
|
// Find guild
|
|
1229
1217
|
let guild;
|
|
1230
1218
|
if (options.guild) {
|
|
1231
|
-
|
|
1219
|
+
// Get raw guild ID from argv to avoid cac's number coercion losing precision on large IDs
|
|
1220
|
+
const guildArgIndex = process.argv.findIndex((arg) => arg === '-g' || arg === '--guild');
|
|
1221
|
+
const rawGuildArg = guildArgIndex >= 0 ? process.argv[guildArgIndex + 1] : undefined;
|
|
1222
|
+
const guildId = rawGuildArg || String(options.guild);
|
|
1223
|
+
const foundGuild = client.guilds.cache.get(guildId);
|
|
1232
1224
|
if (!foundGuild) {
|
|
1233
1225
|
s.stop('Guild not found');
|
|
1234
|
-
cliLogger.error(`Guild not found: ${
|
|
1226
|
+
cliLogger.error(`Guild not found: ${guildId}`);
|
|
1235
1227
|
client.destroy();
|
|
1236
1228
|
process.exit(EXIT_NO_RESTART);
|
|
1237
1229
|
}
|
|
@@ -1276,6 +1268,31 @@ cli
|
|
|
1276
1268
|
guild = firstGuild;
|
|
1277
1269
|
}
|
|
1278
1270
|
}
|
|
1271
|
+
// Check if channel already exists in this guild
|
|
1272
|
+
s.message('Checking for existing channel...');
|
|
1273
|
+
try {
|
|
1274
|
+
const db = getDatabase();
|
|
1275
|
+
const existingChannels = db
|
|
1276
|
+
.prepare('SELECT channel_id FROM channel_directories WHERE directory = ? AND channel_type = ? AND app_id = ?')
|
|
1277
|
+
.all(absolutePath, 'text', appId);
|
|
1278
|
+
for (const existingChannel of existingChannels) {
|
|
1279
|
+
try {
|
|
1280
|
+
const ch = await client.channels.fetch(existingChannel.channel_id);
|
|
1281
|
+
if (ch && 'guild' in ch && ch.guild?.id === guild.id) {
|
|
1282
|
+
s.stop('Channel already exists');
|
|
1283
|
+
note(`Channel already exists for this directory in ${guild.name}.\n\nChannel: <#${existingChannel.channel_id}>\nDirectory: ${absolutePath}`, '⚠️ Already Exists');
|
|
1284
|
+
client.destroy();
|
|
1285
|
+
process.exit(0);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
catch {
|
|
1289
|
+
// Channel might be deleted, continue checking
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
// Database might not exist, continue to create
|
|
1295
|
+
}
|
|
1279
1296
|
s.message(`Creating channels in ${guild.name}...`);
|
|
1280
1297
|
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
|
1281
1298
|
guild,
|
|
@@ -17,6 +17,7 @@ export async function showPermissionDropdown({ thread, permission, directory, })
|
|
|
17
17
|
const contextHash = crypto.randomBytes(8).toString('hex');
|
|
18
18
|
const context = {
|
|
19
19
|
permission,
|
|
20
|
+
requestIds: [permission.id],
|
|
20
21
|
directory,
|
|
21
22
|
thread,
|
|
22
23
|
contextHash,
|
|
@@ -80,10 +81,13 @@ export async function handlePermissionSelectMenu(interaction) {
|
|
|
80
81
|
if (!clientV2) {
|
|
81
82
|
throw new Error('OpenCode server not found for directory');
|
|
82
83
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
reply
|
|
86
|
-
|
|
84
|
+
const requestIds = context.requestIds.length > 0 ? context.requestIds : [context.permission.id];
|
|
85
|
+
await Promise.all(requestIds.map((requestId) => {
|
|
86
|
+
return clientV2.permission.reply({
|
|
87
|
+
requestID: requestId,
|
|
88
|
+
reply: response,
|
|
89
|
+
});
|
|
90
|
+
}));
|
|
87
91
|
pendingPermissionContexts.delete(contextHash);
|
|
88
92
|
// Update message: show result and remove dropdown
|
|
89
93
|
const resultText = (() => {
|
|
@@ -104,7 +108,7 @@ export async function handlePermissionSelectMenu(interaction) {
|
|
|
104
108
|
resultText,
|
|
105
109
|
components: [], // Remove the dropdown
|
|
106
110
|
});
|
|
107
|
-
logger.log(`Permission ${context.permission.id} ${response}`);
|
|
111
|
+
logger.log(`Permission ${context.permission.id} ${response} (${requestIds.length} request(s))`);
|
|
108
112
|
}
|
|
109
113
|
catch (error) {
|
|
110
114
|
logger.error('Error handling permission:', error);
|
|
@@ -114,6 +118,18 @@ export async function handlePermissionSelectMenu(interaction) {
|
|
|
114
118
|
});
|
|
115
119
|
}
|
|
116
120
|
}
|
|
121
|
+
export function addPermissionRequestToContext({ contextHash, requestId, }) {
|
|
122
|
+
const context = pendingPermissionContexts.get(contextHash);
|
|
123
|
+
if (!context) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (context.requestIds.includes(requestId)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
context.requestIds = [...context.requestIds, requestId];
|
|
130
|
+
pendingPermissionContexts.set(contextHash, context);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
117
133
|
/**
|
|
118
134
|
* Clean up a pending permission context (e.g., on auto-reject).
|
|
119
135
|
*/
|
package/dist/commands/queue.js
CHANGED
|
@@ -41,7 +41,11 @@ export async function handleQueueCommand({ command }) {
|
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
// Check if there's an active request running
|
|
44
|
-
const
|
|
44
|
+
const existingController = abortControllers.get(row.session_id);
|
|
45
|
+
const hasActiveRequest = Boolean(existingController && !existingController.signal.aborted);
|
|
46
|
+
if (existingController && existingController.signal.aborted) {
|
|
47
|
+
abortControllers.delete(row.session_id);
|
|
48
|
+
}
|
|
45
49
|
if (!hasActiveRequest) {
|
|
46
50
|
// No active request, send immediately
|
|
47
51
|
const textChannel = await resolveTextChannel(channel);
|
package/dist/commands/resume.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// /resume command - Resume an existing OpenCode session.
|
|
2
2
|
import { ChannelType, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
|
-
import { getDatabase } from '../database.js';
|
|
4
|
+
import { getDatabase, getChannelDirectory } from '../database.js';
|
|
5
5
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
|
-
import { sendThreadMessage, resolveTextChannel
|
|
7
|
-
import { extractTagsArrays } from '../xml.js';
|
|
6
|
+
import { sendThreadMessage, resolveTextChannel } from '../discord-utils.js';
|
|
8
7
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
9
8
|
import { createLogger } from '../logger.js';
|
|
10
9
|
import * as errore from 'errore';
|
|
@@ -18,16 +17,9 @@ export async function handleResumeCommand({ command, appId }) {
|
|
|
18
17
|
return;
|
|
19
18
|
}
|
|
20
19
|
const textChannel = channel;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const extracted = extractTagsArrays({
|
|
25
|
-
xml: textChannel.topic,
|
|
26
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
27
|
-
});
|
|
28
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
29
|
-
channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
30
|
-
}
|
|
20
|
+
const channelConfig = getChannelDirectory(textChannel.id);
|
|
21
|
+
const projectDirectory = channelConfig?.directory;
|
|
22
|
+
const channelAppId = channelConfig?.appId || undefined;
|
|
31
23
|
if (channelAppId && channelAppId !== appId) {
|
|
32
24
|
await command.editReply('This channel is not configured for this bot');
|
|
33
25
|
return;
|
|
@@ -102,12 +94,12 @@ export async function handleResumeAutocomplete({ interaction, appId, }) {
|
|
|
102
94
|
if (interaction.channel) {
|
|
103
95
|
const textChannel = await resolveTextChannel(interaction.channel);
|
|
104
96
|
if (textChannel) {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
97
|
+
const channelConfig = getChannelDirectory(textChannel.id);
|
|
98
|
+
if (channelConfig?.appId && channelConfig.appId !== appId) {
|
|
107
99
|
await interaction.respond([]);
|
|
108
100
|
return;
|
|
109
101
|
}
|
|
110
|
-
projectDirectory = directory;
|
|
102
|
+
projectDirectory = channelConfig?.directory;
|
|
111
103
|
}
|
|
112
104
|
}
|
|
113
105
|
if (!projectDirectory) {
|
package/dist/commands/session.js
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
import { ChannelType } from 'discord.js';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { getDatabase } from '../database.js';
|
|
5
|
+
import { getDatabase, getChannelDirectory } from '../database.js';
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
7
7
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
8
|
-
import { extractTagsArrays } from '../xml.js';
|
|
9
8
|
import { handleOpencodeSession } from '../session-handler.js';
|
|
10
9
|
import { createLogger } from '../logger.js';
|
|
11
10
|
import * as errore from 'errore';
|
|
@@ -21,16 +20,9 @@ export async function handleSessionCommand({ command, appId }) {
|
|
|
21
20
|
return;
|
|
22
21
|
}
|
|
23
22
|
const textChannel = channel;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const extracted = extractTagsArrays({
|
|
28
|
-
xml: textChannel.topic,
|
|
29
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
30
|
-
});
|
|
31
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
32
|
-
channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
33
|
-
}
|
|
23
|
+
const channelConfig = getChannelDirectory(textChannel.id);
|
|
24
|
+
const projectDirectory = channelConfig?.directory;
|
|
25
|
+
const channelAppId = channelConfig?.appId || undefined;
|
|
34
26
|
if (channelAppId && channelAppId !== appId) {
|
|
35
27
|
await command.editReply('This channel is not configured for this bot');
|
|
36
28
|
return;
|
|
@@ -83,22 +75,14 @@ export async function handleSessionCommand({ command, appId }) {
|
|
|
83
75
|
async function handleAgentAutocomplete({ interaction, appId }) {
|
|
84
76
|
const focusedValue = interaction.options.getFocused();
|
|
85
77
|
let projectDirectory;
|
|
86
|
-
if (interaction.channel) {
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
xml: textChannel.topic,
|
|
93
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
94
|
-
});
|
|
95
|
-
const channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
96
|
-
if (channelAppId && channelAppId !== appId) {
|
|
97
|
-
await interaction.respond([]);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
78
|
+
if (interaction.channel && interaction.channel.type === ChannelType.GuildText) {
|
|
79
|
+
const channelConfig = getChannelDirectory(interaction.channel.id);
|
|
80
|
+
if (channelConfig) {
|
|
81
|
+
if (channelConfig.appId && channelConfig.appId !== appId) {
|
|
82
|
+
await interaction.respond([]);
|
|
83
|
+
return;
|
|
101
84
|
}
|
|
85
|
+
projectDirectory = channelConfig.directory;
|
|
102
86
|
}
|
|
103
87
|
}
|
|
104
88
|
if (!projectDirectory) {
|
|
@@ -150,22 +134,14 @@ export async function handleSessionAutocomplete({ interaction, appId, }) {
|
|
|
150
134
|
.filter((f) => f);
|
|
151
135
|
const currentQuery = (parts[parts.length - 1] || '').trim();
|
|
152
136
|
let projectDirectory;
|
|
153
|
-
if (interaction.channel) {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
xml: textChannel.topic,
|
|
160
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
161
|
-
});
|
|
162
|
-
const channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
163
|
-
if (channelAppId && channelAppId !== appId) {
|
|
164
|
-
await interaction.respond([]);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
137
|
+
if (interaction.channel && interaction.channel.type === ChannelType.GuildText) {
|
|
138
|
+
const channelConfig = getChannelDirectory(interaction.channel.id);
|
|
139
|
+
if (channelConfig) {
|
|
140
|
+
if (channelConfig.appId && channelConfig.appId !== appId) {
|
|
141
|
+
await interaction.respond([]);
|
|
142
|
+
return;
|
|
168
143
|
}
|
|
144
|
+
projectDirectory = channelConfig.directory;
|
|
169
145
|
}
|
|
170
146
|
}
|
|
171
147
|
if (!projectDirectory) {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// User-defined OpenCode command handler.
|
|
2
2
|
// Handles slash commands that map to user-configured commands in opencode.json.
|
|
3
3
|
import { ChannelType } from 'discord.js';
|
|
4
|
-
import { extractTagsArrays } from '../xml.js';
|
|
5
4
|
import { handleOpencodeSession } from '../session-handler.js';
|
|
6
5
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
7
6
|
import { createLogger } from '../logger.js';
|
|
8
|
-
import { getDatabase } from '../database.js';
|
|
7
|
+
import { getDatabase, getChannelDirectory } from '../database.js';
|
|
9
8
|
import fs from 'node:fs';
|
|
10
9
|
const userCommandLogger = createLogger('USER_CMD');
|
|
11
10
|
export const handleUserCommand = async ({ command, appId }) => {
|
|
@@ -45,26 +44,18 @@ export const handleUserCommand = async ({ command, appId }) => {
|
|
|
45
44
|
});
|
|
46
45
|
return;
|
|
47
46
|
}
|
|
48
|
-
if (textChannel
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
53
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
54
|
-
channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
47
|
+
if (textChannel) {
|
|
48
|
+
const channelConfig = getChannelDirectory(textChannel.id);
|
|
49
|
+
projectDirectory = channelConfig?.directory;
|
|
50
|
+
channelAppId = channelConfig?.appId || undefined;
|
|
55
51
|
}
|
|
56
52
|
}
|
|
57
53
|
else {
|
|
58
54
|
// Running in a text channel - will create a new thread
|
|
59
55
|
textChannel = channel;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
64
|
-
});
|
|
65
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
66
|
-
channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
67
|
-
}
|
|
56
|
+
const channelConfig = getChannelDirectory(textChannel.id);
|
|
57
|
+
projectDirectory = channelConfig?.directory;
|
|
58
|
+
channelAppId = channelConfig?.appId || undefined;
|
|
68
59
|
}
|
|
69
60
|
if (channelAppId && channelAppId !== appId) {
|
|
70
61
|
await command.reply({
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// /verbosity command.
|
|
2
|
+
// Sets the output verbosity level for sessions in a channel.
|
|
3
|
+
// 'tools-and-text' (default): shows all output including tool executions
|
|
4
|
+
// 'text-only': only shows text responses (⬥ diamond parts)
|
|
5
|
+
import { ChatInputCommandInteraction, ChannelType } from 'discord.js';
|
|
6
|
+
import { getChannelVerbosity, setChannelVerbosity } from '../database.js';
|
|
7
|
+
import { createLogger } from '../logger.js';
|
|
8
|
+
const verbosityLogger = createLogger('VERBOSITY');
|
|
9
|
+
/**
|
|
10
|
+
* Handle the /verbosity slash command.
|
|
11
|
+
* Sets output verbosity for the channel (applies to new sessions).
|
|
12
|
+
*/
|
|
13
|
+
export async function handleVerbosityCommand({ command, appId, }) {
|
|
14
|
+
verbosityLogger.log('[VERBOSITY] Command called');
|
|
15
|
+
const channel = command.channel;
|
|
16
|
+
if (!channel) {
|
|
17
|
+
await command.reply({
|
|
18
|
+
content: 'Could not determine channel.',
|
|
19
|
+
ephemeral: true,
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Get the parent channel ID (for threads, use parent; for text channels, use self)
|
|
24
|
+
const channelId = (() => {
|
|
25
|
+
if (channel.type === ChannelType.GuildText) {
|
|
26
|
+
return channel.id;
|
|
27
|
+
}
|
|
28
|
+
if (channel.type === ChannelType.PublicThread ||
|
|
29
|
+
channel.type === ChannelType.PrivateThread ||
|
|
30
|
+
channel.type === ChannelType.AnnouncementThread) {
|
|
31
|
+
return channel.parentId || channel.id;
|
|
32
|
+
}
|
|
33
|
+
return channel.id;
|
|
34
|
+
})();
|
|
35
|
+
const level = command.options.getString('level', true);
|
|
36
|
+
const currentLevel = getChannelVerbosity(channelId);
|
|
37
|
+
if (currentLevel === level) {
|
|
38
|
+
await command.reply({
|
|
39
|
+
content: `Verbosity is already set to **${level}**.`,
|
|
40
|
+
ephemeral: true,
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setChannelVerbosity(channelId, level);
|
|
45
|
+
verbosityLogger.log(`[VERBOSITY] Set channel ${channelId} to ${level}`);
|
|
46
|
+
const description = level === 'text-only'
|
|
47
|
+
? 'Only text responses will be shown. Tool executions, status messages, and thinking will be hidden.'
|
|
48
|
+
: 'All output will be shown, including tool executions and status messages.';
|
|
49
|
+
await command.reply({
|
|
50
|
+
content: `Verbosity set to **${level}**.\n\n${description}\n\nThis applies to all new sessions in this channel.`,
|
|
51
|
+
ephemeral: true,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -31,7 +31,7 @@ export async function handleEnableWorktreesCommand({ command, appId, }) {
|
|
|
31
31
|
}
|
|
32
32
|
if (!metadata.projectDirectory) {
|
|
33
33
|
await command.reply({
|
|
34
|
-
content: 'This channel is not configured with a project directory.\
|
|
34
|
+
content: 'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
|
|
35
35
|
ephemeral: true,
|
|
36
36
|
});
|
|
37
37
|
return;
|
|
@@ -71,7 +71,7 @@ export async function handleDisableWorktreesCommand({ command, appId, }) {
|
|
|
71
71
|
}
|
|
72
72
|
if (!metadata.projectDirectory) {
|
|
73
73
|
await command.reply({
|
|
74
|
-
content: 'This channel is not configured with a project directory.\
|
|
74
|
+
content: 'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
|
|
75
75
|
ephemeral: true,
|
|
76
76
|
});
|
|
77
77
|
return;
|