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
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { ChannelType, type CategoryChannel, type Guild, type TextChannel } from 'discord.js'
|
|
6
6
|
import path from 'node:path'
|
|
7
|
-
import { getDatabase } from './database.js'
|
|
8
|
-
import { extractTagsArrays } from './xml.js'
|
|
7
|
+
import { getDatabase, getChannelDirectory } from './database.js'
|
|
9
8
|
|
|
10
9
|
export async function ensureKimakiCategory(
|
|
11
10
|
guild: Guild,
|
|
@@ -83,7 +82,7 @@ export async function createProjectChannels({
|
|
|
83
82
|
name: channelName,
|
|
84
83
|
type: ChannelType.GuildText,
|
|
85
84
|
parent: kimakiCategory,
|
|
86
|
-
topic
|
|
85
|
+
// Channel configuration is stored in SQLite, not in the topic
|
|
87
86
|
})
|
|
88
87
|
|
|
89
88
|
const voiceChannel = await guild.channels.create({
|
|
@@ -128,25 +127,15 @@ export async function getChannelsWithDescriptions(guild: Guild): Promise<Channel
|
|
|
128
127
|
const textChannel = channel as TextChannel
|
|
129
128
|
const description = textChannel.topic || null
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (description) {
|
|
135
|
-
const extracted = extractTagsArrays({
|
|
136
|
-
xml: description,
|
|
137
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
kimakiDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
141
|
-
kimakiApp = extracted['kimaki.app']?.[0]?.trim()
|
|
142
|
-
}
|
|
130
|
+
// Get channel config from database instead of parsing XML from topic
|
|
131
|
+
const channelConfig = getChannelDirectory(textChannel.id)
|
|
143
132
|
|
|
144
133
|
channels.push({
|
|
145
134
|
id: textChannel.id,
|
|
146
135
|
name: textChannel.name,
|
|
147
136
|
description,
|
|
148
|
-
kimakiDirectory,
|
|
149
|
-
kimakiApp,
|
|
137
|
+
kimakiDirectory: channelConfig?.directory,
|
|
138
|
+
kimakiApp: channelConfig?.appId || undefined,
|
|
150
139
|
})
|
|
151
140
|
})
|
|
152
141
|
|
package/src/cli.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
getChannelsWithDescriptions,
|
|
22
22
|
createDiscordClient,
|
|
23
23
|
getDatabase,
|
|
24
|
+
getChannelDirectory,
|
|
24
25
|
startDiscordBot,
|
|
25
26
|
initializeOpencodeForDirectory,
|
|
26
27
|
ensureKimakiCategory,
|
|
@@ -47,7 +48,6 @@ import { uploadFilesToDiscord } from './discord-utils.js'
|
|
|
47
48
|
import { spawn, spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
|
|
48
49
|
import http from 'node:http'
|
|
49
50
|
import { setDataDir, getDataDir, getLockPort } from './config.js'
|
|
50
|
-
import { extractTagsArrays } from './xml.js'
|
|
51
51
|
import { sanitizeAgentName } from './commands/agent.js'
|
|
52
52
|
|
|
53
53
|
const cliLogger = createLogger('CLI')
|
|
@@ -240,12 +240,12 @@ async function registerCommands({
|
|
|
240
240
|
.toJSON(),
|
|
241
241
|
new SlashCommandBuilder()
|
|
242
242
|
.setName('new-worktree')
|
|
243
|
-
.setDescription('Create a new git worktree
|
|
243
|
+
.setDescription('Create a new git worktree (in thread: uses thread name if no name given)')
|
|
244
244
|
.addStringOption((option) => {
|
|
245
245
|
option
|
|
246
246
|
.setName('name')
|
|
247
|
-
.setDescription('Name for
|
|
248
|
-
.setRequired(
|
|
247
|
+
.setDescription('Name for worktree (optional in threads - uses thread name)')
|
|
248
|
+
.setRequired(false)
|
|
249
249
|
|
|
250
250
|
return option
|
|
251
251
|
})
|
|
@@ -342,6 +342,21 @@ async function registerCommands({
|
|
|
342
342
|
.setName('redo')
|
|
343
343
|
.setDescription('Redo previously undone changes')
|
|
344
344
|
.toJSON(),
|
|
345
|
+
new SlashCommandBuilder()
|
|
346
|
+
.setName('verbosity')
|
|
347
|
+
.setDescription('Set output verbosity for new sessions in this channel')
|
|
348
|
+
.addStringOption((option) => {
|
|
349
|
+
option
|
|
350
|
+
.setName('level')
|
|
351
|
+
.setDescription('Verbosity level')
|
|
352
|
+
.setRequired(true)
|
|
353
|
+
.addChoices(
|
|
354
|
+
{ name: 'tools-and-text (default)', value: 'tools-and-text' },
|
|
355
|
+
{ name: 'text-only', value: 'text-only' },
|
|
356
|
+
)
|
|
357
|
+
return option
|
|
358
|
+
})
|
|
359
|
+
.toJSON(),
|
|
345
360
|
]
|
|
346
361
|
|
|
347
362
|
// Add user-defined commands with -cmd suffix
|
|
@@ -1367,25 +1382,17 @@ cli
|
|
|
1367
1382
|
guild_id: string
|
|
1368
1383
|
}
|
|
1369
1384
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1385
|
+
const channelConfig = getChannelDirectory(channelData.id)
|
|
1386
|
+
|
|
1387
|
+
if (!channelConfig) {
|
|
1388
|
+
s.stop('Channel not configured')
|
|
1372
1389
|
throw new Error(
|
|
1373
|
-
`Channel #${channelData.name}
|
|
1390
|
+
`Channel #${channelData.name} is not configured with a project directory. Run the bot first to sync channel data.`,
|
|
1374
1391
|
)
|
|
1375
1392
|
}
|
|
1376
1393
|
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
1380
|
-
})
|
|
1381
|
-
|
|
1382
|
-
const projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
1383
|
-
const channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
1384
|
-
|
|
1385
|
-
if (!projectDirectory) {
|
|
1386
|
-
s.stop('No kimaki.directory tag found')
|
|
1387
|
-
throw new Error(`Channel #${channelData.name} has no <kimaki.directory> tag in topic.`)
|
|
1388
|
-
}
|
|
1394
|
+
const projectDirectory = channelConfig.directory
|
|
1395
|
+
const channelAppId = channelConfig.appId || undefined
|
|
1389
1396
|
|
|
1390
1397
|
// Verify app ID matches if both are present
|
|
1391
1398
|
if (channelAppId && appId && channelAppId !== appId) {
|
|
@@ -1599,30 +1606,7 @@ cli
|
|
|
1599
1606
|
}
|
|
1600
1607
|
|
|
1601
1608
|
const s = spinner()
|
|
1602
|
-
s.start('
|
|
1603
|
-
|
|
1604
|
-
// Check if channel already exists
|
|
1605
|
-
try {
|
|
1606
|
-
const db = getDatabase()
|
|
1607
|
-
const existingChannel = db
|
|
1608
|
-
.prepare(
|
|
1609
|
-
'SELECT channel_id FROM channel_directories WHERE directory = ? AND channel_type = ? AND app_id = ?',
|
|
1610
|
-
)
|
|
1611
|
-
.get(absolutePath, 'text', appId) as { channel_id: string } | undefined
|
|
1612
|
-
|
|
1613
|
-
if (existingChannel) {
|
|
1614
|
-
s.stop('Channel already exists')
|
|
1615
|
-
note(
|
|
1616
|
-
`Channel already exists for this directory.\n\nChannel: <#${existingChannel.channel_id}>\nDirectory: ${absolutePath}`,
|
|
1617
|
-
'⚠️ Already Exists',
|
|
1618
|
-
)
|
|
1619
|
-
process.exit(0)
|
|
1620
|
-
}
|
|
1621
|
-
} catch {
|
|
1622
|
-
// Database might not exist, continue to create
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
s.message('Connecting to Discord...')
|
|
1609
|
+
s.start('Connecting to Discord...')
|
|
1626
1610
|
const client = await createDiscordClient()
|
|
1627
1611
|
|
|
1628
1612
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -1638,10 +1622,14 @@ cli
|
|
|
1638
1622
|
// Find guild
|
|
1639
1623
|
let guild: Guild
|
|
1640
1624
|
if (options.guild) {
|
|
1641
|
-
|
|
1625
|
+
// Get raw guild ID from argv to avoid cac's number coercion losing precision on large IDs
|
|
1626
|
+
const guildArgIndex = process.argv.findIndex((arg) => arg === '-g' || arg === '--guild')
|
|
1627
|
+
const rawGuildArg = guildArgIndex >= 0 ? process.argv[guildArgIndex + 1] : undefined
|
|
1628
|
+
const guildId = rawGuildArg || String(options.guild)
|
|
1629
|
+
const foundGuild = client.guilds.cache.get(guildId)
|
|
1642
1630
|
if (!foundGuild) {
|
|
1643
1631
|
s.stop('Guild not found')
|
|
1644
|
-
cliLogger.error(`Guild not found: ${
|
|
1632
|
+
cliLogger.error(`Guild not found: ${guildId}`)
|
|
1645
1633
|
client.destroy()
|
|
1646
1634
|
process.exit(EXIT_NO_RESTART)
|
|
1647
1635
|
}
|
|
@@ -1686,6 +1674,36 @@ cli
|
|
|
1686
1674
|
}
|
|
1687
1675
|
}
|
|
1688
1676
|
|
|
1677
|
+
// Check if channel already exists in this guild
|
|
1678
|
+
s.message('Checking for existing channel...')
|
|
1679
|
+
try {
|
|
1680
|
+
const db = getDatabase()
|
|
1681
|
+
const existingChannels = db
|
|
1682
|
+
.prepare(
|
|
1683
|
+
'SELECT channel_id FROM channel_directories WHERE directory = ? AND channel_type = ? AND app_id = ?',
|
|
1684
|
+
)
|
|
1685
|
+
.all(absolutePath, 'text', appId) as { channel_id: string }[]
|
|
1686
|
+
|
|
1687
|
+
for (const existingChannel of existingChannels) {
|
|
1688
|
+
try {
|
|
1689
|
+
const ch = await client.channels.fetch(existingChannel.channel_id)
|
|
1690
|
+
if (ch && 'guild' in ch && ch.guild?.id === guild.id) {
|
|
1691
|
+
s.stop('Channel already exists')
|
|
1692
|
+
note(
|
|
1693
|
+
`Channel already exists for this directory in ${guild.name}.\n\nChannel: <#${existingChannel.channel_id}>\nDirectory: ${absolutePath}`,
|
|
1694
|
+
'⚠️ Already Exists',
|
|
1695
|
+
)
|
|
1696
|
+
client.destroy()
|
|
1697
|
+
process.exit(0)
|
|
1698
|
+
}
|
|
1699
|
+
} catch {
|
|
1700
|
+
// Channel might be deleted, continue checking
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
} catch {
|
|
1704
|
+
// Database might not exist, continue to create
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1689
1707
|
s.message(`Creating channels in ${guild.name}...`)
|
|
1690
1708
|
|
|
1691
1709
|
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
|
@@ -88,6 +88,9 @@ export async function handleCreateNewProjectCommand({
|
|
|
88
88
|
reason: 'New project session',
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
+
// Add user to thread so it appears in their sidebar
|
|
92
|
+
await thread.members.add(command.user.id)
|
|
93
|
+
|
|
91
94
|
await handleOpencodeSession({
|
|
92
95
|
prompt: 'The project was just initialized. Say hi and ask what the user wants to build.',
|
|
93
96
|
thread,
|
package/src/commands/fork.ts
CHANGED
|
@@ -215,6 +215,9 @@ export async function handleForkSelectMenu(
|
|
|
215
215
|
reason: `Forked from session ${sessionId}`,
|
|
216
216
|
})
|
|
217
217
|
|
|
218
|
+
// Add user to thread so it appears in their sidebar
|
|
219
|
+
await thread.members.add(interaction.user.id)
|
|
220
|
+
|
|
218
221
|
getDatabase()
|
|
219
222
|
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
220
223
|
.run(thread.id, forkedSession.id)
|
|
@@ -18,6 +18,7 @@ const logger = createLogger('PERMISSIONS')
|
|
|
18
18
|
|
|
19
19
|
type PendingPermissionContext = {
|
|
20
20
|
permission: PermissionRequest
|
|
21
|
+
requestIds: string[]
|
|
21
22
|
directory: string
|
|
22
23
|
thread: ThreadChannel
|
|
23
24
|
contextHash: string
|
|
@@ -43,6 +44,7 @@ export async function showPermissionDropdown({
|
|
|
43
44
|
|
|
44
45
|
const context: PendingPermissionContext = {
|
|
45
46
|
permission,
|
|
47
|
+
requestIds: [permission.id],
|
|
46
48
|
directory,
|
|
47
49
|
thread,
|
|
48
50
|
contextHash,
|
|
@@ -124,10 +126,15 @@ export async function handlePermissionSelectMenu(
|
|
|
124
126
|
if (!clientV2) {
|
|
125
127
|
throw new Error('OpenCode server not found for directory')
|
|
126
128
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
const requestIds = context.requestIds.length > 0 ? context.requestIds : [context.permission.id]
|
|
130
|
+
await Promise.all(
|
|
131
|
+
requestIds.map((requestId) => {
|
|
132
|
+
return clientV2.permission.reply({
|
|
133
|
+
requestID: requestId,
|
|
134
|
+
reply: response,
|
|
135
|
+
})
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
131
138
|
|
|
132
139
|
pendingPermissionContexts.delete(contextHash)
|
|
133
140
|
|
|
@@ -153,7 +160,7 @@ export async function handlePermissionSelectMenu(
|
|
|
153
160
|
components: [], // Remove the dropdown
|
|
154
161
|
})
|
|
155
162
|
|
|
156
|
-
logger.log(`Permission ${context.permission.id} ${response}`)
|
|
163
|
+
logger.log(`Permission ${context.permission.id} ${response} (${requestIds.length} request(s))`)
|
|
157
164
|
} catch (error) {
|
|
158
165
|
logger.error('Error handling permission:', error)
|
|
159
166
|
await interaction.editReply({
|
|
@@ -163,6 +170,25 @@ export async function handlePermissionSelectMenu(
|
|
|
163
170
|
}
|
|
164
171
|
}
|
|
165
172
|
|
|
173
|
+
export function addPermissionRequestToContext({
|
|
174
|
+
contextHash,
|
|
175
|
+
requestId,
|
|
176
|
+
}: {
|
|
177
|
+
contextHash: string
|
|
178
|
+
requestId: string
|
|
179
|
+
}): boolean {
|
|
180
|
+
const context = pendingPermissionContexts.get(contextHash)
|
|
181
|
+
if (!context) {
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
if (context.requestIds.includes(requestId)) {
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
context.requestIds = [...context.requestIds, requestId]
|
|
188
|
+
pendingPermissionContexts.set(contextHash, context)
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
|
|
166
192
|
/**
|
|
167
193
|
* Clean up a pending permission context (e.g., on auto-reject).
|
|
168
194
|
*/
|
package/src/commands/queue.ts
CHANGED
|
@@ -62,7 +62,11 @@ export async function handleQueueCommand({ command }: CommandContext): Promise<v
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Check if there's an active request running
|
|
65
|
-
const
|
|
65
|
+
const existingController = abortControllers.get(row.session_id)
|
|
66
|
+
const hasActiveRequest = Boolean(existingController && !existingController.signal.aborted)
|
|
67
|
+
if (existingController && existingController.signal.aborted) {
|
|
68
|
+
abortControllers.delete(row.session_id)
|
|
69
|
+
}
|
|
66
70
|
|
|
67
71
|
if (!hasActiveRequest) {
|
|
68
72
|
// No active request, send immediately
|
package/src/commands/resume.ts
CHANGED
|
@@ -8,10 +8,9 @@ import {
|
|
|
8
8
|
} from 'discord.js'
|
|
9
9
|
import fs from 'node:fs'
|
|
10
10
|
import type { CommandContext, AutocompleteContext } from './types.js'
|
|
11
|
-
import { getDatabase } from '../database.js'
|
|
11
|
+
import { getDatabase, getChannelDirectory } from '../database.js'
|
|
12
12
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
13
|
-
import { sendThreadMessage, resolveTextChannel
|
|
14
|
-
import { extractTagsArrays } from '../xml.js'
|
|
13
|
+
import { sendThreadMessage, resolveTextChannel } from '../discord-utils.js'
|
|
15
14
|
import { collectLastAssistantParts } from '../message-formatting.js'
|
|
16
15
|
import { createLogger } from '../logger.js'
|
|
17
16
|
import * as errore from 'errore'
|
|
@@ -31,18 +30,9 @@ export async function handleResumeCommand({ command, appId }: CommandContext): P
|
|
|
31
30
|
|
|
32
31
|
const textChannel = channel as TextChannel
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (textChannel.topic) {
|
|
38
|
-
const extracted = extractTagsArrays({
|
|
39
|
-
xml: textChannel.topic,
|
|
40
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
44
|
-
channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
45
|
-
}
|
|
33
|
+
const channelConfig = getChannelDirectory(textChannel.id)
|
|
34
|
+
const projectDirectory = channelConfig?.directory
|
|
35
|
+
const channelAppId = channelConfig?.appId || undefined
|
|
46
36
|
|
|
47
37
|
if (channelAppId && channelAppId !== appId) {
|
|
48
38
|
await command.editReply('This channel is not configured for this bot')
|
|
@@ -83,6 +73,9 @@ export async function handleResumeCommand({ command, appId }: CommandContext): P
|
|
|
83
73
|
reason: `Resuming session ${sessionId}`,
|
|
84
74
|
})
|
|
85
75
|
|
|
76
|
+
// Add user to thread so it appears in their sidebar
|
|
77
|
+
await thread.members.add(command.user.id)
|
|
78
|
+
|
|
86
79
|
getDatabase()
|
|
87
80
|
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
88
81
|
.run(thread.id, sessionId)
|
|
@@ -157,12 +150,12 @@ export async function handleResumeAutocomplete({
|
|
|
157
150
|
interaction.channel as TextChannel | ThreadChannel | null,
|
|
158
151
|
)
|
|
159
152
|
if (textChannel) {
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
153
|
+
const channelConfig = getChannelDirectory(textChannel.id)
|
|
154
|
+
if (channelConfig?.appId && channelConfig.appId !== appId) {
|
|
162
155
|
await interaction.respond([])
|
|
163
156
|
return
|
|
164
157
|
}
|
|
165
|
-
projectDirectory = directory
|
|
158
|
+
projectDirectory = channelConfig?.directory
|
|
166
159
|
}
|
|
167
160
|
}
|
|
168
161
|
|
package/src/commands/session.ts
CHANGED
|
@@ -4,10 +4,9 @@ import { ChannelType, type TextChannel } from 'discord.js'
|
|
|
4
4
|
import fs from 'node:fs'
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import type { CommandContext, AutocompleteContext } from './types.js'
|
|
7
|
-
import { getDatabase } from '../database.js'
|
|
7
|
+
import { getDatabase, getChannelDirectory } from '../database.js'
|
|
8
8
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
9
9
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
10
|
-
import { extractTagsArrays } from '../xml.js'
|
|
11
10
|
import { handleOpencodeSession } from '../session-handler.js'
|
|
12
11
|
import { createLogger } from '../logger.js'
|
|
13
12
|
import * as errore from 'errore'
|
|
@@ -29,18 +28,9 @@ export async function handleSessionCommand({ command, appId }: CommandContext):
|
|
|
29
28
|
|
|
30
29
|
const textChannel = channel as TextChannel
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (textChannel.topic) {
|
|
36
|
-
const extracted = extractTagsArrays({
|
|
37
|
-
xml: textChannel.topic,
|
|
38
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
42
|
-
channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
43
|
-
}
|
|
31
|
+
const channelConfig = getChannelDirectory(textChannel.id)
|
|
32
|
+
const projectDirectory = channelConfig?.directory
|
|
33
|
+
const channelAppId = channelConfig?.appId || undefined
|
|
44
34
|
|
|
45
35
|
if (channelAppId && channelAppId !== appId) {
|
|
46
36
|
await command.editReply('This channel is not configured for this bot')
|
|
@@ -85,6 +75,9 @@ export async function handleSessionCommand({ command, appId }: CommandContext):
|
|
|
85
75
|
reason: 'OpenCode session',
|
|
86
76
|
})
|
|
87
77
|
|
|
78
|
+
// Add user to thread so it appears in their sidebar
|
|
79
|
+
await thread.members.add(command.user.id)
|
|
80
|
+
|
|
88
81
|
await command.editReply(`Created new session in ${thread.toString()}`)
|
|
89
82
|
|
|
90
83
|
await handleOpencodeSession({
|
|
@@ -107,22 +100,14 @@ async function handleAgentAutocomplete({ interaction, appId }: AutocompleteConte
|
|
|
107
100
|
|
|
108
101
|
let projectDirectory: string | undefined
|
|
109
102
|
|
|
110
|
-
if (interaction.channel) {
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
xml: textChannel.topic,
|
|
117
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
118
|
-
})
|
|
119
|
-
const channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
120
|
-
if (channelAppId && channelAppId !== appId) {
|
|
121
|
-
await interaction.respond([])
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
103
|
+
if (interaction.channel && interaction.channel.type === ChannelType.GuildText) {
|
|
104
|
+
const channelConfig = getChannelDirectory(interaction.channel.id)
|
|
105
|
+
if (channelConfig) {
|
|
106
|
+
if (channelConfig.appId && channelConfig.appId !== appId) {
|
|
107
|
+
await interaction.respond([])
|
|
108
|
+
return
|
|
125
109
|
}
|
|
110
|
+
projectDirectory = channelConfig.directory
|
|
126
111
|
}
|
|
127
112
|
}
|
|
128
113
|
|
|
@@ -190,22 +175,14 @@ export async function handleSessionAutocomplete({
|
|
|
190
175
|
|
|
191
176
|
let projectDirectory: string | undefined
|
|
192
177
|
|
|
193
|
-
if (interaction.channel) {
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
xml: textChannel.topic,
|
|
200
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
201
|
-
})
|
|
202
|
-
const channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
203
|
-
if (channelAppId && channelAppId !== appId) {
|
|
204
|
-
await interaction.respond([])
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
178
|
+
if (interaction.channel && interaction.channel.type === ChannelType.GuildText) {
|
|
179
|
+
const channelConfig = getChannelDirectory(interaction.channel.id)
|
|
180
|
+
if (channelConfig) {
|
|
181
|
+
if (channelConfig.appId && channelConfig.appId !== appId) {
|
|
182
|
+
await interaction.respond([])
|
|
183
|
+
return
|
|
208
184
|
}
|
|
185
|
+
projectDirectory = channelConfig.directory
|
|
209
186
|
}
|
|
210
187
|
}
|
|
211
188
|
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import type { CommandContext, CommandHandler } from './types.js'
|
|
5
5
|
import { ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
|
|
6
|
-
import { extractTagsArrays } from '../xml.js'
|
|
7
6
|
import { handleOpencodeSession } from '../session-handler.js'
|
|
8
7
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
9
8
|
import { createLogger } from '../logger.js'
|
|
10
|
-
import { getDatabase } from '../database.js'
|
|
9
|
+
import { getDatabase, getChannelDirectory } from '../database.js'
|
|
11
10
|
import fs from 'node:fs'
|
|
12
11
|
|
|
13
12
|
const userCommandLogger = createLogger('USER_CMD')
|
|
@@ -68,28 +67,18 @@ export const handleUserCommand: CommandHandler = async ({ command, appId }: Comm
|
|
|
68
67
|
return
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
if (textChannel
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
78
|
-
channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
70
|
+
if (textChannel) {
|
|
71
|
+
const channelConfig = getChannelDirectory(textChannel.id)
|
|
72
|
+
projectDirectory = channelConfig?.directory
|
|
73
|
+
channelAppId = channelConfig?.appId || undefined
|
|
79
74
|
}
|
|
80
75
|
} else {
|
|
81
76
|
// Running in a text channel - will create a new thread
|
|
82
77
|
textChannel = channel as TextChannel
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
91
|
-
channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
92
|
-
}
|
|
79
|
+
const channelConfig = getChannelDirectory(textChannel.id)
|
|
80
|
+
projectDirectory = channelConfig?.directory
|
|
81
|
+
channelAppId = channelConfig?.appId || undefined
|
|
93
82
|
}
|
|
94
83
|
|
|
95
84
|
if (channelAppId && channelAppId !== appId) {
|
|
@@ -147,6 +136,9 @@ export const handleUserCommand: CommandHandler = async ({ command, appId }: Comm
|
|
|
147
136
|
reason: `OpenCode command: ${commandName}`,
|
|
148
137
|
})
|
|
149
138
|
|
|
139
|
+
// Add user to thread so it appears in their sidebar
|
|
140
|
+
await newThread.members.add(command.user.id)
|
|
141
|
+
|
|
150
142
|
await command.editReply(`Started /${commandName} in ${newThread.toString()}`)
|
|
151
143
|
|
|
152
144
|
await handleOpencodeSession({
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
|
|
6
|
+
import { ChatInputCommandInteraction, ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
|
|
7
|
+
import { getChannelVerbosity, setChannelVerbosity, type VerbosityLevel } from '../database.js'
|
|
8
|
+
import { createLogger } from '../logger.js'
|
|
9
|
+
|
|
10
|
+
const verbosityLogger = createLogger('VERBOSITY')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handle the /verbosity slash command.
|
|
14
|
+
* Sets output verbosity for the channel (applies to new sessions).
|
|
15
|
+
*/
|
|
16
|
+
export async function handleVerbosityCommand({
|
|
17
|
+
command,
|
|
18
|
+
appId,
|
|
19
|
+
}: {
|
|
20
|
+
command: ChatInputCommandInteraction
|
|
21
|
+
appId: string
|
|
22
|
+
}): Promise<void> {
|
|
23
|
+
verbosityLogger.log('[VERBOSITY] Command called')
|
|
24
|
+
|
|
25
|
+
const channel = command.channel
|
|
26
|
+
if (!channel) {
|
|
27
|
+
await command.reply({
|
|
28
|
+
content: 'Could not determine channel.',
|
|
29
|
+
ephemeral: true,
|
|
30
|
+
})
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get the parent channel ID (for threads, use parent; for text channels, use self)
|
|
35
|
+
const channelId = (() => {
|
|
36
|
+
if (channel.type === ChannelType.GuildText) {
|
|
37
|
+
return channel.id
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
channel.type === ChannelType.PublicThread ||
|
|
41
|
+
channel.type === ChannelType.PrivateThread ||
|
|
42
|
+
channel.type === ChannelType.AnnouncementThread
|
|
43
|
+
) {
|
|
44
|
+
return (channel as ThreadChannel).parentId || channel.id
|
|
45
|
+
}
|
|
46
|
+
return channel.id
|
|
47
|
+
})()
|
|
48
|
+
|
|
49
|
+
const level = command.options.getString('level', true) as VerbosityLevel
|
|
50
|
+
const currentLevel = getChannelVerbosity(channelId)
|
|
51
|
+
|
|
52
|
+
if (currentLevel === level) {
|
|
53
|
+
await command.reply({
|
|
54
|
+
content: `Verbosity is already set to **${level}**.`,
|
|
55
|
+
ephemeral: true,
|
|
56
|
+
})
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setChannelVerbosity(channelId, level)
|
|
61
|
+
verbosityLogger.log(`[VERBOSITY] Set channel ${channelId} to ${level}`)
|
|
62
|
+
|
|
63
|
+
const description = level === 'text-only'
|
|
64
|
+
? 'Only text responses will be shown. Tool executions, status messages, and thinking will be hidden.'
|
|
65
|
+
: 'All output will be shown, including tool executions and status messages.'
|
|
66
|
+
|
|
67
|
+
await command.reply({
|
|
68
|
+
content: `Verbosity set to **${level}**.\n\n${description}\n\nThis applies to all new sessions in this channel.`,
|
|
69
|
+
ephemeral: true,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
@@ -46,7 +46,7 @@ export async function handleEnableWorktreesCommand({
|
|
|
46
46
|
if (!metadata.projectDirectory) {
|
|
47
47
|
await command.reply({
|
|
48
48
|
content:
|
|
49
|
-
'This channel is not configured with a project directory.\
|
|
49
|
+
'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
|
|
50
50
|
ephemeral: true,
|
|
51
51
|
})
|
|
52
52
|
return
|
|
@@ -102,7 +102,7 @@ export async function handleDisableWorktreesCommand({
|
|
|
102
102
|
if (!metadata.projectDirectory) {
|
|
103
103
|
await command.reply({
|
|
104
104
|
content:
|
|
105
|
-
'This channel is not configured with a project directory.\
|
|
105
|
+
'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
|
|
106
106
|
ephemeral: true,
|
|
107
107
|
})
|
|
108
108
|
return
|