kimaki 0.4.44 → 0.4.46
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/create-new-project.js +2 -0
- package/dist/commands/fork.js +2 -0
- package/dist/commands/permissions.js +21 -5
- package/dist/commands/queue.js +5 -1
- package/dist/commands/resume.js +10 -16
- package/dist/commands/session.js +20 -42
- package/dist/commands/user-command.js +10 -17
- package/dist/commands/verbosity.js +53 -0
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +134 -25
- package/dist/database.js +49 -0
- package/dist/discord-bot.js +26 -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 +581 -414
- 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/create-new-project.ts +3 -0
- package/src/commands/fork.ts +3 -0
- package/src/commands/permissions.ts +31 -5
- package/src/commands/queue.ts +5 -1
- package/src/commands/resume.ts +11 -18
- package/src/commands/session.ts +21 -44
- package/src/commands/user-command.ts +11 -19
- package/src/commands/verbosity.ts +71 -0
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +163 -27
- package/src/database.ts +65 -0
- package/src/discord-bot.ts +29 -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 +711 -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,
|
|
@@ -63,6 +63,8 @@ export async function handleCreateNewProjectCommand({ command, appId, }) {
|
|
|
63
63
|
autoArchiveDuration: 1440,
|
|
64
64
|
reason: 'New project session',
|
|
65
65
|
});
|
|
66
|
+
// Add user to thread so it appears in their sidebar
|
|
67
|
+
await thread.members.add(command.user.id);
|
|
66
68
|
await handleOpencodeSession({
|
|
67
69
|
prompt: 'The project was just initialized. Say hi and ask what the user wants to build.',
|
|
68
70
|
thread,
|
package/dist/commands/fork.js
CHANGED
|
@@ -162,6 +162,8 @@ export async function handleForkSelectMenu(interaction) {
|
|
|
162
162
|
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
163
163
|
reason: `Forked from session ${sessionId}`,
|
|
164
164
|
});
|
|
165
|
+
// Add user to thread so it appears in their sidebar
|
|
166
|
+
await thread.members.add(interaction.user.id);
|
|
165
167
|
getDatabase()
|
|
166
168
|
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
167
169
|
.run(thread.id, forkedSession.id);
|
|
@@ -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;
|
|
@@ -59,6 +51,8 @@ export async function handleResumeCommand({ command, appId }) {
|
|
|
59
51
|
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
60
52
|
reason: `Resuming session ${sessionId}`,
|
|
61
53
|
});
|
|
54
|
+
// Add user to thread so it appears in their sidebar
|
|
55
|
+
await thread.members.add(command.user.id);
|
|
62
56
|
getDatabase()
|
|
63
57
|
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
64
58
|
.run(thread.id, sessionId);
|
|
@@ -102,12 +96,12 @@ export async function handleResumeAutocomplete({ interaction, appId, }) {
|
|
|
102
96
|
if (interaction.channel) {
|
|
103
97
|
const textChannel = await resolveTextChannel(interaction.channel);
|
|
104
98
|
if (textChannel) {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
99
|
+
const channelConfig = getChannelDirectory(textChannel.id);
|
|
100
|
+
if (channelConfig?.appId && channelConfig.appId !== appId) {
|
|
107
101
|
await interaction.respond([]);
|
|
108
102
|
return;
|
|
109
103
|
}
|
|
110
|
-
projectDirectory = directory;
|
|
104
|
+
projectDirectory = channelConfig?.directory;
|
|
111
105
|
}
|
|
112
106
|
}
|
|
113
107
|
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;
|
|
@@ -66,6 +58,8 @@ export async function handleSessionCommand({ command, appId }) {
|
|
|
66
58
|
autoArchiveDuration: 1440,
|
|
67
59
|
reason: 'OpenCode session',
|
|
68
60
|
});
|
|
61
|
+
// Add user to thread so it appears in their sidebar
|
|
62
|
+
await thread.members.add(command.user.id);
|
|
69
63
|
await command.editReply(`Created new session in ${thread.toString()}`);
|
|
70
64
|
await handleOpencodeSession({
|
|
71
65
|
prompt: fullPrompt,
|
|
@@ -83,22 +77,14 @@ export async function handleSessionCommand({ command, appId }) {
|
|
|
83
77
|
async function handleAgentAutocomplete({ interaction, appId }) {
|
|
84
78
|
const focusedValue = interaction.options.getFocused();
|
|
85
79
|
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();
|
|
80
|
+
if (interaction.channel && interaction.channel.type === ChannelType.GuildText) {
|
|
81
|
+
const channelConfig = getChannelDirectory(interaction.channel.id);
|
|
82
|
+
if (channelConfig) {
|
|
83
|
+
if (channelConfig.appId && channelConfig.appId !== appId) {
|
|
84
|
+
await interaction.respond([]);
|
|
85
|
+
return;
|
|
101
86
|
}
|
|
87
|
+
projectDirectory = channelConfig.directory;
|
|
102
88
|
}
|
|
103
89
|
}
|
|
104
90
|
if (!projectDirectory) {
|
|
@@ -150,22 +136,14 @@ export async function handleSessionAutocomplete({ interaction, appId, }) {
|
|
|
150
136
|
.filter((f) => f);
|
|
151
137
|
const currentQuery = (parts[parts.length - 1] || '').trim();
|
|
152
138
|
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();
|
|
139
|
+
if (interaction.channel && interaction.channel.type === ChannelType.GuildText) {
|
|
140
|
+
const channelConfig = getChannelDirectory(interaction.channel.id);
|
|
141
|
+
if (channelConfig) {
|
|
142
|
+
if (channelConfig.appId && channelConfig.appId !== appId) {
|
|
143
|
+
await interaction.respond([]);
|
|
144
|
+
return;
|
|
168
145
|
}
|
|
146
|
+
projectDirectory = channelConfig.directory;
|
|
169
147
|
}
|
|
170
148
|
}
|
|
171
149
|
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({
|
|
@@ -114,6 +105,8 @@ export const handleUserCommand = async ({ command, appId }) => {
|
|
|
114
105
|
autoArchiveDuration: 1440,
|
|
115
106
|
reason: `OpenCode command: ${commandName}`,
|
|
116
107
|
});
|
|
108
|
+
// Add user to thread so it appears in their sidebar
|
|
109
|
+
await newThread.members.add(command.user.id);
|
|
117
110
|
await command.editReply(`Started /${commandName} in ${newThread.toString()}`);
|
|
118
111
|
await handleOpencodeSession({
|
|
119
112
|
prompt: '', // Not used when command is set
|
|
@@ -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;
|