kimaki 0.4.37 → 0.4.39
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 -2
- package/dist/cli.js +41 -15
- package/dist/commands/abort.js +15 -6
- package/dist/commands/add-project.js +9 -0
- package/dist/commands/agent.js +114 -20
- package/dist/commands/fork.js +13 -2
- package/dist/commands/model.js +12 -0
- package/dist/commands/remove-project.js +26 -16
- package/dist/commands/resume.js +9 -0
- package/dist/commands/session.js +13 -0
- package/dist/commands/share.js +10 -1
- package/dist/commands/undo-redo.js +13 -4
- package/dist/database.js +24 -5
- package/dist/discord-bot.js +38 -31
- package/dist/errors.js +110 -0
- package/dist/genai-worker.js +18 -16
- package/dist/interaction-handler.js +6 -1
- package/dist/markdown.js +96 -85
- package/dist/markdown.test.js +10 -3
- package/dist/message-formatting.js +50 -37
- package/dist/opencode.js +43 -46
- package/dist/session-handler.js +136 -8
- package/dist/system-message.js +2 -0
- package/dist/tools.js +18 -8
- package/dist/voice-handler.js +48 -25
- package/dist/voice.js +159 -131
- package/package.json +2 -1
- package/src/channel-management.ts +6 -2
- package/src/cli.ts +67 -19
- package/src/commands/abort.ts +17 -7
- package/src/commands/add-project.ts +9 -0
- package/src/commands/agent.ts +160 -25
- package/src/commands/fork.ts +18 -7
- package/src/commands/model.ts +12 -0
- package/src/commands/remove-project.ts +28 -16
- package/src/commands/resume.ts +9 -0
- package/src/commands/session.ts +13 -0
- package/src/commands/share.ts +11 -1
- package/src/commands/undo-redo.ts +15 -6
- package/src/database.ts +26 -4
- package/src/discord-bot.ts +42 -34
- package/src/errors.ts +208 -0
- package/src/genai-worker.ts +20 -17
- package/src/interaction-handler.ts +7 -1
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +111 -95
- package/src/message-formatting.ts +55 -38
- package/src/opencode.ts +52 -49
- package/src/session-handler.ts +164 -11
- package/src/system-message.ts +2 -0
- package/src/tools.ts +18 -8
- package/src/voice-handler.ts +48 -23
- package/src/voice.ts +195 -148
|
@@ -6,7 +6,9 @@ import path from 'node:path';
|
|
|
6
6
|
import { getDatabase } from './database.js';
|
|
7
7
|
import { extractTagsArrays } from './xml.js';
|
|
8
8
|
export async function ensureKimakiCategory(guild, botName) {
|
|
9
|
-
|
|
9
|
+
// Skip appending bot name if it's already "kimaki" to avoid "Kimaki kimaki"
|
|
10
|
+
const isKimakiBot = botName?.toLowerCase() === 'kimaki';
|
|
11
|
+
const categoryName = botName && !isKimakiBot ? `Kimaki ${botName}` : 'Kimaki';
|
|
10
12
|
const existingCategory = guild.channels.cache.find((channel) => {
|
|
11
13
|
if (channel.type !== ChannelType.GuildCategory) {
|
|
12
14
|
return false;
|
|
@@ -22,7 +24,9 @@ export async function ensureKimakiCategory(guild, botName) {
|
|
|
22
24
|
});
|
|
23
25
|
}
|
|
24
26
|
export async function ensureKimakiAudioCategory(guild, botName) {
|
|
25
|
-
|
|
27
|
+
// Skip appending bot name if it's already "kimaki" to avoid "Kimaki Audio kimaki"
|
|
28
|
+
const isKimakiBot = botName?.toLowerCase() === 'kimaki';
|
|
29
|
+
const categoryName = botName && !isKimakiBot ? `Kimaki Audio ${botName}` : 'Kimaki Audio';
|
|
26
30
|
const existingCategory = guild.channels.cache.find((channel) => {
|
|
27
31
|
if (channel.type !== ChannelType.GuildCategory) {
|
|
28
32
|
return false;
|
package/dist/cli.js
CHANGED
|
@@ -9,11 +9,13 @@ import { getChannelsWithDescriptions, createDiscordClient, getDatabase, startDis
|
|
|
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';
|
|
12
|
+
import * as errore from 'errore';
|
|
12
13
|
import { createLogger } from './logger.js';
|
|
13
14
|
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
14
15
|
import http from 'node:http';
|
|
15
16
|
import { setDataDir, getDataDir, getLockPort } from './config.js';
|
|
16
17
|
import { extractTagsArrays } from './xml.js';
|
|
18
|
+
import { sanitizeAgentName } from './commands/agent.js';
|
|
17
19
|
const cliLogger = createLogger('CLI');
|
|
18
20
|
const cli = cac('kimaki');
|
|
19
21
|
process.title = 'kimaki';
|
|
@@ -119,7 +121,7 @@ async function startLockServer() {
|
|
|
119
121
|
const EXIT_NO_RESTART = 64;
|
|
120
122
|
// Commands to skip when registering user commands (reserved names)
|
|
121
123
|
const SKIP_USER_COMMANDS = ['init'];
|
|
122
|
-
async function registerCommands(token, appId, userCommands = []) {
|
|
124
|
+
async function registerCommands({ token, appId, userCommands = [], agents = [], }) {
|
|
123
125
|
const commands = [
|
|
124
126
|
new SlashCommandBuilder()
|
|
125
127
|
.setName('resume')
|
|
@@ -254,6 +256,18 @@ async function registerCommands(token, appId, userCommands = []) {
|
|
|
254
256
|
})
|
|
255
257
|
.toJSON());
|
|
256
258
|
}
|
|
259
|
+
// Add agent-specific quick commands like /plan-agent, /build-agent
|
|
260
|
+
// Filter to primary/all mode agents (same as /agent command shows), excluding hidden agents
|
|
261
|
+
const primaryAgents = agents.filter((a) => (a.mode === 'primary' || a.mode === 'all') && !a.hidden);
|
|
262
|
+
for (const agent of primaryAgents) {
|
|
263
|
+
const sanitizedName = sanitizeAgentName(agent.name);
|
|
264
|
+
const commandName = `${sanitizedName}-agent`;
|
|
265
|
+
const description = agent.description || `Switch to ${agent.name} agent`;
|
|
266
|
+
commands.push(new SlashCommandBuilder()
|
|
267
|
+
.setName(commandName.slice(0, 32)) // Discord limits to 32 chars
|
|
268
|
+
.setDescription(description.slice(0, 100))
|
|
269
|
+
.toJSON());
|
|
270
|
+
}
|
|
257
271
|
const rest = new REST().setToken(token);
|
|
258
272
|
try {
|
|
259
273
|
const data = (await rest.put(Routes.applicationCommands(appId), {
|
|
@@ -419,7 +433,12 @@ async function run({ restart, addChannels }) {
|
|
|
419
433
|
// This is the biggest startup bottleneck (can take 1-30 seconds to spawn and wait for ready)
|
|
420
434
|
const currentDir = process.cwd();
|
|
421
435
|
s.start('Starting OpenCode server...');
|
|
422
|
-
const opencodePromise = initializeOpencodeForDirectory(currentDir)
|
|
436
|
+
const opencodePromise = initializeOpencodeForDirectory(currentDir).then((result) => {
|
|
437
|
+
if (errore.isError(result)) {
|
|
438
|
+
throw new Error(result.message);
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
});
|
|
423
442
|
s.message('Connecting to Discord...');
|
|
424
443
|
const discordClient = await createDiscordClient();
|
|
425
444
|
const guilds = [];
|
|
@@ -506,8 +525,8 @@ async function run({ restart, addChannels }) {
|
|
|
506
525
|
const getClient = await opencodePromise;
|
|
507
526
|
s.stop('OpenCode server ready!');
|
|
508
527
|
s.start('Fetching OpenCode data...');
|
|
509
|
-
// Fetch projects and
|
|
510
|
-
const [projects, allUserCommands] = await Promise.all([
|
|
528
|
+
// Fetch projects, commands, and agents in parallel
|
|
529
|
+
const [projects, allUserCommands, allAgents] = await Promise.all([
|
|
511
530
|
getClient()
|
|
512
531
|
.project.list({})
|
|
513
532
|
.then((r) => r.data || [])
|
|
@@ -521,6 +540,10 @@ async function run({ restart, addChannels }) {
|
|
|
521
540
|
.command.list({ query: { directory: currentDir } })
|
|
522
541
|
.then((r) => r.data || [])
|
|
523
542
|
.catch(() => []),
|
|
543
|
+
getClient()
|
|
544
|
+
.app.agents({ query: { directory: currentDir } })
|
|
545
|
+
.then((r) => r.data || [])
|
|
546
|
+
.catch(() => []),
|
|
524
547
|
]);
|
|
525
548
|
s.stop(`Found ${projects.length} OpenCode project(s)`);
|
|
526
549
|
const existingDirs = kimakiChannels.flatMap(({ channels }) => channels
|
|
@@ -611,7 +634,7 @@ async function run({ restart, addChannels }) {
|
|
|
611
634
|
note(`Found ${registrableCommands.length} user-defined command(s):\n${commandList}`, 'OpenCode Commands');
|
|
612
635
|
}
|
|
613
636
|
cliLogger.log('Registering slash commands asynchronously...');
|
|
614
|
-
void registerCommands(token, appId, allUserCommands)
|
|
637
|
+
void registerCommands({ token, appId, userCommands: allUserCommands, agents: allAgents })
|
|
615
638
|
.then(() => {
|
|
616
639
|
cliLogger.log('Slash commands registered!');
|
|
617
640
|
})
|
|
@@ -746,12 +769,6 @@ cli
|
|
|
746
769
|
process.exit(EXIT_NO_RESTART);
|
|
747
770
|
}
|
|
748
771
|
});
|
|
749
|
-
// Magic prefix used to identify bot-initiated sessions.
|
|
750
|
-
// The running bot will recognize this prefix and start a session.
|
|
751
|
-
const BOT_SESSION_PREFIX = '🤖 **Bot-initiated session**';
|
|
752
|
-
// Notify-only prefix - bot won't start a session, just creates thread for notifications.
|
|
753
|
-
// Reply to the thread to start a session with the notification as context.
|
|
754
|
-
const BOT_NOTIFY_PREFIX = '📢 **Notification**';
|
|
755
772
|
cli
|
|
756
773
|
.command('send', 'Send a message to Discord channel, creating a thread. Use --notify-only to skip AI session.')
|
|
757
774
|
.alias('start-session') // backwards compatibility
|
|
@@ -953,9 +970,7 @@ cli
|
|
|
953
970
|
throw new Error(`Channel belongs to a different bot (expected: ${appId}, got: ${channelAppId})`);
|
|
954
971
|
}
|
|
955
972
|
s.message('Creating starter message...');
|
|
956
|
-
// Create starter message with
|
|
957
|
-
// BOT_SESSION_PREFIX triggers AI session, BOT_NOTIFY_PREFIX is notification-only
|
|
958
|
-
const messagePrefix = notifyOnly ? BOT_NOTIFY_PREFIX : BOT_SESSION_PREFIX;
|
|
973
|
+
// Create starter message with just the prompt (no prefix)
|
|
959
974
|
const starterMessageResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
|
|
960
975
|
method: 'POST',
|
|
961
976
|
headers: {
|
|
@@ -963,7 +978,7 @@ cli
|
|
|
963
978
|
'Content-Type': 'application/json',
|
|
964
979
|
},
|
|
965
980
|
body: JSON.stringify({
|
|
966
|
-
content:
|
|
981
|
+
content: prompt,
|
|
967
982
|
}),
|
|
968
983
|
});
|
|
969
984
|
if (!starterMessageResponse.ok) {
|
|
@@ -992,6 +1007,17 @@ cli
|
|
|
992
1007
|
throw new Error(`Discord API error: ${threadResponse.status} - ${error}`);
|
|
993
1008
|
}
|
|
994
1009
|
const threadData = (await threadResponse.json());
|
|
1010
|
+
// Mark thread for auto-start if not notify-only
|
|
1011
|
+
// This is optional - only works if local database exists (for local bot auto-start)
|
|
1012
|
+
if (!notifyOnly) {
|
|
1013
|
+
try {
|
|
1014
|
+
const db = getDatabase();
|
|
1015
|
+
db.prepare('INSERT OR REPLACE INTO pending_auto_start (thread_id) VALUES (?)').run(threadData.id);
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
// Database not available (e.g., CI environment) - skip auto-start marking
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
995
1021
|
s.stop('Thread created!');
|
|
996
1022
|
const threadUrl = `https://discord.com/channels/${channelData.guild_id}/${threadData.id}`;
|
|
997
1023
|
const successMessage = notifyOnly
|
package/dist/commands/abort.js
CHANGED
|
@@ -5,6 +5,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { abortControllers } from '../session-handler.js';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
|
+
import * as errore from 'errore';
|
|
8
9
|
const logger = createLogger('ABORT');
|
|
9
10
|
export async function handleAbortCommand({ command }) {
|
|
10
11
|
const channel = command.channel;
|
|
@@ -51,13 +52,21 @@ export async function handleAbortCommand({ command }) {
|
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
const sessionId = row.session_id;
|
|
55
|
+
const existingController = abortControllers.get(sessionId);
|
|
56
|
+
if (existingController) {
|
|
57
|
+
existingController.abort(new Error('User requested abort'));
|
|
58
|
+
abortControllers.delete(sessionId);
|
|
59
|
+
}
|
|
60
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
61
|
+
if (errore.isError(getClient)) {
|
|
62
|
+
await command.reply({
|
|
63
|
+
content: `Failed to abort: ${getClient.message}`,
|
|
64
|
+
ephemeral: true,
|
|
65
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
54
69
|
try {
|
|
55
|
-
const existingController = abortControllers.get(sessionId);
|
|
56
|
-
if (existingController) {
|
|
57
|
-
existingController.abort(new Error('User requested abort'));
|
|
58
|
-
abortControllers.delete(sessionId);
|
|
59
|
-
}
|
|
60
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
61
70
|
await getClient().session.abort({
|
|
62
71
|
path: { id: sessionId },
|
|
63
72
|
});
|
|
@@ -6,6 +6,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
6
6
|
import { createProjectChannels } from '../channel-management.js';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
8
|
import { abbreviatePath } from '../utils.js';
|
|
9
|
+
import * as errore from 'errore';
|
|
9
10
|
const logger = createLogger('ADD-PROJECT');
|
|
10
11
|
export async function handleAddProjectCommand({ command, appId }) {
|
|
11
12
|
await command.deferReply({ ephemeral: false });
|
|
@@ -18,6 +19,10 @@ export async function handleAddProjectCommand({ command, appId }) {
|
|
|
18
19
|
try {
|
|
19
20
|
const currentDir = process.cwd();
|
|
20
21
|
const getClient = await initializeOpencodeForDirectory(currentDir);
|
|
22
|
+
if (errore.isError(getClient)) {
|
|
23
|
+
await command.editReply(getClient.message);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
21
26
|
const projectsResponse = await getClient().project.list({});
|
|
22
27
|
if (!projectsResponse.data) {
|
|
23
28
|
await command.editReply('Failed to fetch projects');
|
|
@@ -60,6 +65,10 @@ export async function handleAddProjectAutocomplete({ interaction, appId, }) {
|
|
|
60
65
|
try {
|
|
61
66
|
const currentDir = process.cwd();
|
|
62
67
|
const getClient = await initializeOpencodeForDirectory(currentDir);
|
|
68
|
+
if (errore.isError(getClient)) {
|
|
69
|
+
await interaction.respond([]);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
63
72
|
const projectsResponse = await getClient().project.list({});
|
|
64
73
|
if (!projectsResponse.data) {
|
|
65
74
|
await interaction.respond([]);
|
package/dist/commands/agent.js
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
// /agent command - Set the preferred agent for this channel or session.
|
|
2
|
+
// Also provides quick agent commands like /plan-agent, /build-agent that switch instantly.
|
|
2
3
|
import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, } from 'discord.js';
|
|
3
4
|
import crypto from 'node:crypto';
|
|
4
|
-
import { getDatabase, setChannelAgent, setSessionAgent, runModelMigrations } from '../database.js';
|
|
5
|
+
import { getDatabase, setChannelAgent, setSessionAgent, clearSessionModel, runModelMigrations } from '../database.js';
|
|
5
6
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
7
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
7
8
|
import { createLogger } from '../logger.js';
|
|
9
|
+
import * as errore from 'errore';
|
|
8
10
|
const agentLogger = createLogger('AGENT');
|
|
9
11
|
const pendingAgentContexts = new Map();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Sanitize an agent name to be a valid Discord command name component.
|
|
14
|
+
* Lowercase, alphanumeric and hyphens only.
|
|
15
|
+
*/
|
|
16
|
+
export function sanitizeAgentName(name) {
|
|
17
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the context for an agent command (directory, channel, session).
|
|
21
|
+
* Returns null if the command cannot be executed in this context.
|
|
22
|
+
*/
|
|
23
|
+
export async function resolveAgentCommandContext({ interaction, appId, }) {
|
|
13
24
|
const channel = interaction.channel;
|
|
14
25
|
if (!channel) {
|
|
15
26
|
await interaction.editReply({ content: 'This command can only be used in a channel' });
|
|
16
|
-
return;
|
|
27
|
+
return null;
|
|
17
28
|
}
|
|
18
29
|
const isThread = [
|
|
19
30
|
ChannelType.PublicThread,
|
|
@@ -47,41 +58,74 @@ export async function handleAgentCommand({ interaction, appId, }) {
|
|
|
47
58
|
await interaction.editReply({
|
|
48
59
|
content: 'This command can only be used in text channels or threads',
|
|
49
60
|
});
|
|
50
|
-
return;
|
|
61
|
+
return null;
|
|
51
62
|
}
|
|
52
63
|
if (channelAppId && channelAppId !== appId) {
|
|
53
64
|
await interaction.editReply({ content: 'This channel is not configured for this bot' });
|
|
54
|
-
return;
|
|
65
|
+
return null;
|
|
55
66
|
}
|
|
56
67
|
if (!projectDirectory) {
|
|
57
68
|
await interaction.editReply({
|
|
58
69
|
content: 'This channel is not configured with a project directory',
|
|
59
70
|
});
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
dir: projectDirectory,
|
|
75
|
+
channelId: targetChannelId,
|
|
76
|
+
sessionId,
|
|
77
|
+
isThread,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Set the agent preference for a context (session or channel).
|
|
82
|
+
* When switching agents for a session, also clears the session model preference
|
|
83
|
+
* so the new agent's model takes effect.
|
|
84
|
+
*/
|
|
85
|
+
export function setAgentForContext({ context, agentName, }) {
|
|
86
|
+
if (context.isThread && context.sessionId) {
|
|
87
|
+
setSessionAgent(context.sessionId, agentName);
|
|
88
|
+
// Clear session model so the new agent's model takes effect
|
|
89
|
+
clearSessionModel(context.sessionId);
|
|
90
|
+
agentLogger.log(`Set agent ${agentName} for session ${context.sessionId} (cleared model preference)`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
setChannelAgent(context.channelId, agentName);
|
|
94
|
+
agentLogger.log(`Set agent ${agentName} for channel ${context.channelId}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function handleAgentCommand({ interaction, appId, }) {
|
|
98
|
+
await interaction.deferReply({ ephemeral: true });
|
|
99
|
+
runModelMigrations();
|
|
100
|
+
const context = await resolveAgentCommandContext({ interaction, appId });
|
|
101
|
+
if (!context) {
|
|
60
102
|
return;
|
|
61
103
|
}
|
|
62
104
|
try {
|
|
63
|
-
const getClient = await initializeOpencodeForDirectory(
|
|
105
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
106
|
+
if (errore.isError(getClient)) {
|
|
107
|
+
await interaction.editReply({ content: getClient.message });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
64
110
|
const agentsResponse = await getClient().app.agents({
|
|
65
|
-
query: { directory:
|
|
111
|
+
query: { directory: context.dir },
|
|
66
112
|
});
|
|
67
113
|
if (!agentsResponse.data || agentsResponse.data.length === 0) {
|
|
68
114
|
await interaction.editReply({ content: 'No agents available' });
|
|
69
115
|
return;
|
|
70
116
|
}
|
|
71
117
|
const agents = agentsResponse.data
|
|
72
|
-
.filter((
|
|
118
|
+
.filter((agent) => {
|
|
119
|
+
const hidden = agent.hidden;
|
|
120
|
+
return (agent.mode === 'primary' || agent.mode === 'all') && !hidden;
|
|
121
|
+
})
|
|
73
122
|
.slice(0, 25);
|
|
74
123
|
if (agents.length === 0) {
|
|
75
124
|
await interaction.editReply({ content: 'No primary agents available' });
|
|
76
125
|
return;
|
|
77
126
|
}
|
|
78
127
|
const contextHash = crypto.randomBytes(8).toString('hex');
|
|
79
|
-
pendingAgentContexts.set(contextHash,
|
|
80
|
-
dir: projectDirectory,
|
|
81
|
-
channelId: targetChannelId,
|
|
82
|
-
sessionId,
|
|
83
|
-
isThread,
|
|
84
|
-
});
|
|
128
|
+
pendingAgentContexts.set(contextHash, context);
|
|
85
129
|
const options = agents.map((agent) => ({
|
|
86
130
|
label: agent.name.slice(0, 100),
|
|
87
131
|
value: agent.name,
|
|
@@ -128,17 +172,14 @@ export async function handleAgentSelectMenu(interaction) {
|
|
|
128
172
|
return;
|
|
129
173
|
}
|
|
130
174
|
try {
|
|
175
|
+
setAgentForContext({ context, agentName: selectedAgent });
|
|
131
176
|
if (context.isThread && context.sessionId) {
|
|
132
|
-
setSessionAgent(context.sessionId, selectedAgent);
|
|
133
|
-
agentLogger.log(`Set agent ${selectedAgent} for session ${context.sessionId}`);
|
|
134
177
|
await interaction.editReply({
|
|
135
178
|
content: `Agent preference set for this session: **${selectedAgent}**`,
|
|
136
179
|
components: [],
|
|
137
180
|
});
|
|
138
181
|
}
|
|
139
182
|
else {
|
|
140
|
-
setChannelAgent(context.channelId, selectedAgent);
|
|
141
|
-
agentLogger.log(`Set agent ${selectedAgent} for channel ${context.channelId}`);
|
|
142
183
|
await interaction.editReply({
|
|
143
184
|
content: `Agent preference set for this channel: **${selectedAgent}**\n\nAll new sessions in this channel will use this agent.`,
|
|
144
185
|
components: [],
|
|
@@ -154,3 +195,56 @@ export async function handleAgentSelectMenu(interaction) {
|
|
|
154
195
|
});
|
|
155
196
|
}
|
|
156
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Handle quick agent commands like /plan-agent, /build-agent.
|
|
200
|
+
* These instantly switch to the specified agent without showing a dropdown.
|
|
201
|
+
*/
|
|
202
|
+
export async function handleQuickAgentCommand({ command, appId, }) {
|
|
203
|
+
await command.deferReply({ ephemeral: true });
|
|
204
|
+
runModelMigrations();
|
|
205
|
+
// Extract agent name from command: "plan-agent" → "plan"
|
|
206
|
+
const sanitizedAgentName = command.commandName.replace(/-agent$/, '');
|
|
207
|
+
const context = await resolveAgentCommandContext({ interaction: command, appId });
|
|
208
|
+
if (!context) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
213
|
+
if (errore.isError(getClient)) {
|
|
214
|
+
await command.editReply({ content: getClient.message });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const agentsResponse = await getClient().app.agents({
|
|
218
|
+
query: { directory: context.dir },
|
|
219
|
+
});
|
|
220
|
+
if (!agentsResponse.data || agentsResponse.data.length === 0) {
|
|
221
|
+
await command.editReply({ content: 'No agents available in this project' });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// Find the agent matching the sanitized command name
|
|
225
|
+
const matchingAgent = agentsResponse.data.find((a) => sanitizeAgentName(a.name) === sanitizedAgentName);
|
|
226
|
+
if (!matchingAgent) {
|
|
227
|
+
await command.editReply({
|
|
228
|
+
content: `Agent not found. Available agents: ${agentsResponse.data.map((a) => a.name).join(', ')}`,
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
setAgentForContext({ context, agentName: matchingAgent.name });
|
|
233
|
+
if (context.isThread && context.sessionId) {
|
|
234
|
+
await command.editReply({
|
|
235
|
+
content: `Switched to **${matchingAgent.name}** agent for this session`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
await command.editReply({
|
|
240
|
+
content: `Switched to **${matchingAgent.name}** agent for this channel\n\nAll new sessions will use this agent.`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
agentLogger.error('Error in quick agent command:', error);
|
|
246
|
+
await command.editReply({
|
|
247
|
+
content: `Failed to switch agent: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
package/dist/commands/fork.js
CHANGED
|
@@ -5,6 +5,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, sendThreadMessage } from '../discord-utils.js';
|
|
6
6
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
|
+
import * as errore from 'errore';
|
|
8
9
|
const sessionLogger = createLogger('SESSION');
|
|
9
10
|
const forkLogger = createLogger('FORK');
|
|
10
11
|
export async function handleForkCommand(interaction) {
|
|
@@ -50,8 +51,14 @@ export async function handleForkCommand(interaction) {
|
|
|
50
51
|
// Defer reply before API calls to avoid 3-second timeout
|
|
51
52
|
await interaction.deferReply({ ephemeral: true });
|
|
52
53
|
const sessionId = row.session_id;
|
|
54
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
|
+
if (errore.isError(getClient)) {
|
|
56
|
+
await interaction.editReply({
|
|
57
|
+
content: `Failed to load messages: ${getClient.message}`,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
53
61
|
try {
|
|
54
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
62
|
const messagesResponse = await getClient().session.messages({
|
|
56
63
|
path: { id: sessionId },
|
|
57
64
|
});
|
|
@@ -120,8 +127,12 @@ export async function handleForkSelectMenu(interaction) {
|
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
129
|
await interaction.deferReply({ ephemeral: false });
|
|
130
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
131
|
+
if (errore.isError(getClient)) {
|
|
132
|
+
await interaction.editReply(`Failed to fork session: ${getClient.message}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
123
135
|
try {
|
|
124
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
125
136
|
const forkResponse = await getClient().session.fork({
|
|
126
137
|
path: { id: sessionId },
|
|
127
138
|
body: { messageID: selectedMessageId },
|
package/dist/commands/model.js
CHANGED
|
@@ -6,6 +6,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
6
6
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
7
7
|
import { abortAndRetrySession } from '../session-handler.js';
|
|
8
8
|
import { createLogger } from '../logger.js';
|
|
9
|
+
import * as errore from 'errore';
|
|
9
10
|
const modelLogger = createLogger('MODEL');
|
|
10
11
|
// Store context by hash to avoid customId length limits (Discord max: 100 chars)
|
|
11
12
|
const pendingModelContexts = new Map();
|
|
@@ -77,6 +78,10 @@ export async function handleModelCommand({ interaction, appId, }) {
|
|
|
77
78
|
}
|
|
78
79
|
try {
|
|
79
80
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
81
|
+
if (errore.isError(getClient)) {
|
|
82
|
+
await interaction.editReply({ content: getClient.message });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
80
85
|
const providersResponse = await getClient().provider.list({
|
|
81
86
|
query: { directory: projectDirectory },
|
|
82
87
|
});
|
|
@@ -162,6 +167,13 @@ export async function handleProviderSelectMenu(interaction) {
|
|
|
162
167
|
}
|
|
163
168
|
try {
|
|
164
169
|
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
170
|
+
if (errore.isError(getClient)) {
|
|
171
|
+
await interaction.editReply({
|
|
172
|
+
content: getClient.message,
|
|
173
|
+
components: [],
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
165
177
|
const providersResponse = await getClient().provider.list({
|
|
166
178
|
query: { directory: context.dir },
|
|
167
179
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// /remove-project command - Remove Discord channels for a project.
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import * as errore from 'errore';
|
|
3
4
|
import { getDatabase } from '../database.js';
|
|
4
5
|
import { createLogger } from '../logger.js';
|
|
5
6
|
import { abbreviatePath } from '../utils.js';
|
|
@@ -25,20 +26,27 @@ export async function handleRemoveProjectCommand({ command, appId }) {
|
|
|
25
26
|
const deletedChannels = [];
|
|
26
27
|
const failedChannels = [];
|
|
27
28
|
for (const { channel_id, channel_type } of channels) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const channel = await errore.tryAsync({
|
|
30
|
+
try: () => guild.channels.fetch(channel_id),
|
|
31
|
+
catch: (e) => e,
|
|
32
|
+
});
|
|
33
|
+
if (errore.isError(channel)) {
|
|
34
|
+
logger.error(`Failed to fetch channel ${channel_id}:`, channel);
|
|
35
|
+
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (channel) {
|
|
39
|
+
try {
|
|
31
40
|
await channel.delete(`Removed by /remove-project command`);
|
|
32
41
|
deletedChannels.push(`${channel_type}: ${channel_id}`);
|
|
33
42
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.error(`Failed to delete channel ${channel_id}:`, error);
|
|
45
|
+
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
48
|
+
else {
|
|
49
|
+
deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`);
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
// Remove from database
|
|
@@ -76,14 +84,16 @@ export async function handleRemoveProjectAutocomplete({ interaction, appId, }) {
|
|
|
76
84
|
// Filter to only channels that exist in this guild
|
|
77
85
|
const projectsInGuild = [];
|
|
78
86
|
for (const { directory, channel_id } of allChannels) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
87
|
+
const channel = await errore.tryAsync({
|
|
88
|
+
try: () => guild.channels.fetch(channel_id),
|
|
89
|
+
catch: (e) => e,
|
|
90
|
+
});
|
|
91
|
+
if (errore.isError(channel)) {
|
|
86
92
|
// Channel not in this guild, skip
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (channel) {
|
|
96
|
+
projectsInGuild.push({ directory, channelId: channel_id });
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
99
|
const projects = projectsInGuild
|
package/dist/commands/resume.js
CHANGED
|
@@ -7,6 +7,7 @@ import { sendThreadMessage, resolveTextChannel, getKimakiMetadata } from '../dis
|
|
|
7
7
|
import { extractTagsArrays } from '../xml.js';
|
|
8
8
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
9
9
|
import { createLogger } from '../logger.js';
|
|
10
|
+
import * as errore from 'errore';
|
|
10
11
|
const logger = createLogger('RESUME');
|
|
11
12
|
export async function handleResumeCommand({ command, appId }) {
|
|
12
13
|
await command.deferReply({ ephemeral: false });
|
|
@@ -41,6 +42,10 @@ export async function handleResumeCommand({ command, appId }) {
|
|
|
41
42
|
}
|
|
42
43
|
try {
|
|
43
44
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
45
|
+
if (errore.isError(getClient)) {
|
|
46
|
+
await command.editReply(getClient.message);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
44
49
|
const sessionResponse = await getClient().session.get({
|
|
45
50
|
path: { id: sessionId },
|
|
46
51
|
});
|
|
@@ -111,6 +116,10 @@ export async function handleResumeAutocomplete({ interaction, appId, }) {
|
|
|
111
116
|
}
|
|
112
117
|
try {
|
|
113
118
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
119
|
+
if (errore.isError(getClient)) {
|
|
120
|
+
await interaction.respond([]);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
114
123
|
const sessionsResponse = await getClient().session.list();
|
|
115
124
|
if (!sessionsResponse.data) {
|
|
116
125
|
await interaction.respond([]);
|
package/dist/commands/session.js
CHANGED
|
@@ -8,6 +8,7 @@ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
|
8
8
|
import { extractTagsArrays } from '../xml.js';
|
|
9
9
|
import { handleOpencodeSession } from '../session-handler.js';
|
|
10
10
|
import { createLogger } from '../logger.js';
|
|
11
|
+
import * as errore from 'errore';
|
|
11
12
|
const logger = createLogger('SESSION');
|
|
12
13
|
export async function handleSessionCommand({ command, appId }) {
|
|
13
14
|
await command.deferReply({ ephemeral: false });
|
|
@@ -44,6 +45,10 @@ export async function handleSessionCommand({ command, appId }) {
|
|
|
44
45
|
}
|
|
45
46
|
try {
|
|
46
47
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
48
|
+
if (errore.isError(getClient)) {
|
|
49
|
+
await command.editReply(getClient.message);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
47
52
|
const files = filesString
|
|
48
53
|
.split(',')
|
|
49
54
|
.map((f) => f.trim())
|
|
@@ -102,6 +107,10 @@ async function handleAgentAutocomplete({ interaction, appId }) {
|
|
|
102
107
|
}
|
|
103
108
|
try {
|
|
104
109
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
110
|
+
if (errore.isError(getClient)) {
|
|
111
|
+
await interaction.respond([]);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
105
114
|
const agentsResponse = await getClient().app.agents({
|
|
106
115
|
query: { directory: projectDirectory },
|
|
107
116
|
});
|
|
@@ -165,6 +174,10 @@ export async function handleSessionAutocomplete({ interaction, appId, }) {
|
|
|
165
174
|
}
|
|
166
175
|
try {
|
|
167
176
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
177
|
+
if (errore.isError(getClient)) {
|
|
178
|
+
await interaction.respond([]);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
168
181
|
const response = await getClient().find.files({
|
|
169
182
|
query: {
|
|
170
183
|
query: currentQuery || '',
|
package/dist/commands/share.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getDatabase } from '../database.js';
|
|
|
4
4
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { createLogger } from '../logger.js';
|
|
7
|
+
import * as errore from 'errore';
|
|
7
8
|
const logger = createLogger('SHARE');
|
|
8
9
|
export async function handleShareCommand({ command }) {
|
|
9
10
|
const channel = command.channel;
|
|
@@ -50,8 +51,16 @@ export async function handleShareCommand({ command }) {
|
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
const sessionId = row.session_id;
|
|
54
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
|
+
if (errore.isError(getClient)) {
|
|
56
|
+
await command.reply({
|
|
57
|
+
content: `Failed to share session: ${getClient.message}`,
|
|
58
|
+
ephemeral: true,
|
|
59
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
53
63
|
try {
|
|
54
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
64
|
const response = await getClient().session.share({
|
|
56
65
|
path: { id: sessionId },
|
|
57
66
|
});
|