agentcord 0.1.0
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/.env.example +23 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/bin/agentcord.js +21 -0
- package/package.json +60 -0
- package/src/agents.ts +90 -0
- package/src/bot.ts +193 -0
- package/src/button-handler.ts +153 -0
- package/src/cli.ts +50 -0
- package/src/command-handlers.ts +623 -0
- package/src/commands.ts +166 -0
- package/src/config.ts +45 -0
- package/src/index.ts +18 -0
- package/src/message-handler.ts +60 -0
- package/src/output-handler.ts +515 -0
- package/src/persistence.ts +33 -0
- package/src/project-manager.ts +165 -0
- package/src/session-manager.ts +407 -0
- package/src/setup.ts +381 -0
- package/src/shell-handler.ts +91 -0
- package/src/types.ts +80 -0
- package/src/utils.ts +112 -0
- package/tsconfig.json +17 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const command = process.argv[2];
|
|
2
|
+
|
|
3
|
+
switch (command) {
|
|
4
|
+
case 'setup': {
|
|
5
|
+
const { runSetup } = await import('./setup.ts');
|
|
6
|
+
await runSetup();
|
|
7
|
+
break;
|
|
8
|
+
}
|
|
9
|
+
case 'start':
|
|
10
|
+
case undefined: {
|
|
11
|
+
const { existsSync } = await import('node:fs');
|
|
12
|
+
const { resolve } = await import('node:path');
|
|
13
|
+
|
|
14
|
+
const envPath = resolve(process.cwd(), '.env');
|
|
15
|
+
if (!existsSync(envPath)) {
|
|
16
|
+
console.log('\x1b[33mNo .env file found in the current directory.\x1b[0m');
|
|
17
|
+
console.log('Run \x1b[36magentcord setup\x1b[0m to configure.\n');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { startBot } = await import('./bot.ts');
|
|
22
|
+
console.log('agentcord starting...');
|
|
23
|
+
await startBot();
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
case 'help':
|
|
27
|
+
case '--help':
|
|
28
|
+
case '-h': {
|
|
29
|
+
console.log(`
|
|
30
|
+
\x1b[1magentcord\x1b[0m — Discord bot for managing Claude Code sessions
|
|
31
|
+
|
|
32
|
+
\x1b[1mUsage:\x1b[0m
|
|
33
|
+
agentcord Start the bot
|
|
34
|
+
agentcord setup Interactive configuration wizard
|
|
35
|
+
agentcord help Show this help message
|
|
36
|
+
|
|
37
|
+
\x1b[1mQuick start:\x1b[0m
|
|
38
|
+
1. agentcord setup Configure Discord app, token, permissions
|
|
39
|
+
2. agentcord Start the bot
|
|
40
|
+
3. /claude new <name> <dir> Create a session in Discord
|
|
41
|
+
|
|
42
|
+
\x1b[2mhttps://github.com/manuelatravajo/agentcord\x1b[0m
|
|
43
|
+
`);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
console.error(`Unknown command: ${command}`);
|
|
48
|
+
console.error('Run \x1b[36magentcord help\x1b[0m for usage.');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EmbedBuilder,
|
|
3
|
+
ChannelType,
|
|
4
|
+
type ChatInputCommandInteraction,
|
|
5
|
+
type TextChannel,
|
|
6
|
+
type Guild,
|
|
7
|
+
type CategoryChannel,
|
|
8
|
+
} from 'discord.js';
|
|
9
|
+
import { config } from './config.ts';
|
|
10
|
+
import * as sessions from './session-manager.ts';
|
|
11
|
+
import * as projectMgr from './project-manager.ts';
|
|
12
|
+
import { listAgents, getAgent } from './agents.ts';
|
|
13
|
+
import { handleOutputStream } from './output-handler.ts';
|
|
14
|
+
import { executeShellCommand, listProcesses, killProcess } from './shell-handler.ts';
|
|
15
|
+
import {
|
|
16
|
+
isUserAllowed,
|
|
17
|
+
projectNameFromDir,
|
|
18
|
+
formatUptime,
|
|
19
|
+
formatLastActivity,
|
|
20
|
+
truncate,
|
|
21
|
+
} from './utils.ts';
|
|
22
|
+
|
|
23
|
+
// Logging callback (set by bot.ts)
|
|
24
|
+
let logFn: (msg: string) => void = console.log;
|
|
25
|
+
export function setLogger(fn: (msg: string) => void): void {
|
|
26
|
+
logFn = fn;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function log(msg: string): void {
|
|
30
|
+
logFn(msg);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Get or create project category + log channel
|
|
34
|
+
async function ensureProjectCategory(
|
|
35
|
+
guild: Guild,
|
|
36
|
+
projectName: string,
|
|
37
|
+
directory: string,
|
|
38
|
+
): Promise<{ category: CategoryChannel; logChannel: TextChannel }> {
|
|
39
|
+
let project = projectMgr.getProject(projectName);
|
|
40
|
+
|
|
41
|
+
// Try to find existing category
|
|
42
|
+
let category: CategoryChannel | undefined;
|
|
43
|
+
if (project) {
|
|
44
|
+
category = guild.channels.cache.get(project.categoryId) as CategoryChannel | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!category) {
|
|
48
|
+
// Look by name
|
|
49
|
+
category = guild.channels.cache.find(
|
|
50
|
+
ch => ch.type === ChannelType.GuildCategory && ch.name === projectName,
|
|
51
|
+
) as CategoryChannel | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!category) {
|
|
55
|
+
category = await guild.channels.create({
|
|
56
|
+
name: projectName,
|
|
57
|
+
type: ChannelType.GuildCategory,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ensure project exists in store
|
|
62
|
+
project = projectMgr.getOrCreateProject(projectName, directory, category.id);
|
|
63
|
+
|
|
64
|
+
// Find or create log channel
|
|
65
|
+
let logChannel: TextChannel | undefined;
|
|
66
|
+
if (project.logChannelId) {
|
|
67
|
+
logChannel = guild.channels.cache.get(project.logChannelId) as TextChannel | undefined;
|
|
68
|
+
}
|
|
69
|
+
if (!logChannel) {
|
|
70
|
+
logChannel = category.children.cache.find(
|
|
71
|
+
ch => ch.name === 'project-logs' && ch.type === ChannelType.GuildText,
|
|
72
|
+
) as TextChannel | undefined;
|
|
73
|
+
}
|
|
74
|
+
if (!logChannel) {
|
|
75
|
+
logChannel = await guild.channels.create({
|
|
76
|
+
name: 'project-logs',
|
|
77
|
+
type: ChannelType.GuildText,
|
|
78
|
+
parent: category.id,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
projectMgr.updateProjectCategory(projectName, category.id, logChannel.id);
|
|
83
|
+
|
|
84
|
+
return { category, logChannel };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// /claude commands
|
|
88
|
+
|
|
89
|
+
export async function handleClaude(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
90
|
+
if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
|
|
91
|
+
await interaction.reply({ content: 'You are not authorized to use this bot.', ephemeral: true });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sub = interaction.options.getSubcommand();
|
|
96
|
+
|
|
97
|
+
switch (sub) {
|
|
98
|
+
case 'new': return handleClaudeNew(interaction);
|
|
99
|
+
case 'list': return handleClaudeList(interaction);
|
|
100
|
+
case 'end': return handleClaudeEnd(interaction);
|
|
101
|
+
case 'continue': return handleClaudeContinue(interaction);
|
|
102
|
+
case 'stop': return handleClaudeStop(interaction);
|
|
103
|
+
case 'output': return handleClaudeOutput(interaction);
|
|
104
|
+
case 'attach': return handleClaudeAttach(interaction);
|
|
105
|
+
case 'sync': return handleClaudeSync(interaction);
|
|
106
|
+
case 'model': return handleClaudeModel(interaction);
|
|
107
|
+
case 'verbose': return handleClaudeVerbose(interaction);
|
|
108
|
+
default:
|
|
109
|
+
await interaction.reply({ content: `Unknown subcommand: ${sub}`, ephemeral: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function handleClaudeNew(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
114
|
+
const name = interaction.options.getString('name', true);
|
|
115
|
+
const directory = interaction.options.getString('directory') || config.defaultDirectory;
|
|
116
|
+
|
|
117
|
+
await interaction.deferReply();
|
|
118
|
+
|
|
119
|
+
let channel: TextChannel | undefined;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const guild = interaction.guild!;
|
|
123
|
+
const projectName = projectNameFromDir(directory);
|
|
124
|
+
|
|
125
|
+
const { category } = await ensureProjectCategory(guild, projectName, directory);
|
|
126
|
+
|
|
127
|
+
// Create session first (handles name deduplication)
|
|
128
|
+
// Use a temp channel ID, we'll update it after creating the channel
|
|
129
|
+
const session = await sessions.createSession(name, directory, 'pending', projectName);
|
|
130
|
+
|
|
131
|
+
// Create Discord channel with the deduplicated session ID
|
|
132
|
+
channel = await guild.channels.create({
|
|
133
|
+
name: `claude-${session.id}`,
|
|
134
|
+
type: ChannelType.GuildText,
|
|
135
|
+
parent: category.id,
|
|
136
|
+
topic: `Claude session | Dir: ${directory}`,
|
|
137
|
+
}) as TextChannel;
|
|
138
|
+
|
|
139
|
+
// Link the real channel ID
|
|
140
|
+
sessions.linkChannel(session.id, channel.id);
|
|
141
|
+
|
|
142
|
+
const embed = new EmbedBuilder()
|
|
143
|
+
.setColor(0x2ecc71)
|
|
144
|
+
.setTitle(`Session Created: ${session.id}`)
|
|
145
|
+
.addFields(
|
|
146
|
+
{ name: 'Channel', value: `#claude-${session.id}`, inline: true },
|
|
147
|
+
{ name: 'Directory', value: session.directory, inline: true },
|
|
148
|
+
{ name: 'Project', value: projectName, inline: true },
|
|
149
|
+
{ name: 'Terminal', value: `\`tmux attach -t ${session.tmuxName}\``, inline: false },
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
await interaction.editReply({ embeds: [embed] });
|
|
153
|
+
log(`Session "${session.id}" created by ${interaction.user.tag} in ${directory}`);
|
|
154
|
+
|
|
155
|
+
// Welcome message in the new channel
|
|
156
|
+
await channel.send({
|
|
157
|
+
embeds: [
|
|
158
|
+
new EmbedBuilder()
|
|
159
|
+
.setColor(0x3498db)
|
|
160
|
+
.setTitle('Claude Code Session')
|
|
161
|
+
.setDescription('Type a message to send it to Claude. Use `/claude stop` to cancel generation.')
|
|
162
|
+
.addFields(
|
|
163
|
+
{ name: 'Directory', value: `\`${session.directory}\``, inline: false },
|
|
164
|
+
{ name: 'Terminal Access', value: `\`tmux attach -t ${session.tmuxName}\``, inline: false },
|
|
165
|
+
),
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
} catch (err: unknown) {
|
|
169
|
+
// Clean up on failure
|
|
170
|
+
if (channel) {
|
|
171
|
+
try { await channel.delete(); } catch { /* best effort */ }
|
|
172
|
+
}
|
|
173
|
+
await interaction.editReply(`Failed to create session: ${(err as Error).message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function handleClaudeList(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
178
|
+
const allSessions = sessions.getAllSessions();
|
|
179
|
+
|
|
180
|
+
if (allSessions.length === 0) {
|
|
181
|
+
await interaction.reply({ content: 'No active sessions.', ephemeral: true });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Group by project
|
|
186
|
+
const grouped = new Map<string, typeof allSessions>();
|
|
187
|
+
for (const s of allSessions) {
|
|
188
|
+
const arr = grouped.get(s.projectName) || [];
|
|
189
|
+
arr.push(s);
|
|
190
|
+
grouped.set(s.projectName, arr);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const embed = new EmbedBuilder()
|
|
194
|
+
.setColor(0x3498db)
|
|
195
|
+
.setTitle(`Active Sessions (${allSessions.length})`);
|
|
196
|
+
|
|
197
|
+
for (const [project, projectSessions] of grouped) {
|
|
198
|
+
const lines = projectSessions.map(s => {
|
|
199
|
+
const status = s.isGenerating ? '🟢 generating' : '⚪ idle';
|
|
200
|
+
return `**${s.id}** — ${status} | ${formatUptime(s.createdAt)} uptime | ${s.messageCount} msgs | $${s.totalCost.toFixed(4)} | ${formatLastActivity(s.lastActivity)}`;
|
|
201
|
+
});
|
|
202
|
+
embed.addFields({ name: `📁 ${project}`, value: lines.join('\n') });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await interaction.reply({ embeds: [embed] });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function handleClaudeEnd(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
209
|
+
const session = sessions.getSessionByChannel(interaction.channelId);
|
|
210
|
+
if (!session) {
|
|
211
|
+
await interaction.reply({ content: 'No session in this channel.', ephemeral: true });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await interaction.deferReply();
|
|
216
|
+
try {
|
|
217
|
+
await sessions.endSession(session.id);
|
|
218
|
+
await interaction.editReply(`Session "${session.id}" ended. You can delete this channel.`);
|
|
219
|
+
log(`Session "${session.id}" ended by ${interaction.user.tag}`);
|
|
220
|
+
} catch (err: unknown) {
|
|
221
|
+
await interaction.editReply(`Failed to end session: ${(err as Error).message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function handleClaudeContinue(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
226
|
+
const session = sessions.getSessionByChannel(interaction.channelId);
|
|
227
|
+
if (!session) {
|
|
228
|
+
await interaction.reply({ content: 'No session in this channel.', ephemeral: true });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (session.isGenerating) {
|
|
232
|
+
await interaction.reply({ content: 'Session is already generating.', ephemeral: true });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await interaction.deferReply();
|
|
237
|
+
try {
|
|
238
|
+
const channel = interaction.channel as TextChannel;
|
|
239
|
+
const stream = sessions.continueSession(session.id);
|
|
240
|
+
await interaction.editReply('Continuing...');
|
|
241
|
+
await handleOutputStream(stream, channel, session.id, session.verbose);
|
|
242
|
+
} catch (err: unknown) {
|
|
243
|
+
await interaction.editReply(`Error: ${(err as Error).message}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function handleClaudeStop(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
248
|
+
const session = sessions.getSessionByChannel(interaction.channelId);
|
|
249
|
+
if (!session) {
|
|
250
|
+
await interaction.reply({ content: 'No session in this channel.', ephemeral: true });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const stopped = sessions.abortSession(session.id);
|
|
255
|
+
await interaction.reply({
|
|
256
|
+
content: stopped ? 'Generation stopped.' : 'Session was not generating.',
|
|
257
|
+
ephemeral: true,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function handleClaudeOutput(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
262
|
+
const session = sessions.getSessionByChannel(interaction.channelId);
|
|
263
|
+
if (!session) {
|
|
264
|
+
await interaction.reply({ content: 'No session in this channel.', ephemeral: true });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await interaction.reply({
|
|
269
|
+
content: 'Conversation history is managed by the Claude Code SDK. Use `/claude attach` to view the full terminal history.',
|
|
270
|
+
ephemeral: true,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function handleClaudeAttach(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
275
|
+
const session = sessions.getSessionByChannel(interaction.channelId);
|
|
276
|
+
if (!session) {
|
|
277
|
+
await interaction.reply({ content: 'No session in this channel.', ephemeral: true });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const info = sessions.getAttachInfo(session.id);
|
|
282
|
+
if (!info) {
|
|
283
|
+
await interaction.reply({ content: 'Session not found.', ephemeral: true });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const embed = new EmbedBuilder()
|
|
288
|
+
.setColor(0x9b59b6)
|
|
289
|
+
.setTitle('Terminal Access')
|
|
290
|
+
.addFields(
|
|
291
|
+
{ name: 'Attach to tmux', value: `\`\`\`\n${info.command}\n\`\`\`` },
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (info.sessionId) {
|
|
295
|
+
embed.addFields({
|
|
296
|
+
name: 'Resume Claude in terminal',
|
|
297
|
+
value: `\`\`\`\ncd ${session.directory} && claude --resume ${info.sessionId}\n\`\`\``,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function handleClaudeSync(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
305
|
+
await interaction.deferReply();
|
|
306
|
+
|
|
307
|
+
const guild = interaction.guild!;
|
|
308
|
+
const tmuxSessions = await sessions.listTmuxSessions();
|
|
309
|
+
const currentSessions = sessions.getAllSessions();
|
|
310
|
+
const currentIds = new Set(currentSessions.map(s => s.id));
|
|
311
|
+
|
|
312
|
+
let synced = 0;
|
|
313
|
+
for (const tmuxSession of tmuxSessions) {
|
|
314
|
+
if (currentIds.has(tmuxSession.id)) continue;
|
|
315
|
+
|
|
316
|
+
const projectName = projectNameFromDir(tmuxSession.directory);
|
|
317
|
+
const { category } = await ensureProjectCategory(guild, projectName, tmuxSession.directory);
|
|
318
|
+
|
|
319
|
+
const channel = await guild.channels.create({
|
|
320
|
+
name: `claude-${tmuxSession.id}`,
|
|
321
|
+
type: ChannelType.GuildText,
|
|
322
|
+
parent: category.id,
|
|
323
|
+
topic: `Claude session (synced) | Dir: ${tmuxSession.directory}`,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await sessions.createSession(tmuxSession.id, tmuxSession.directory, channel.id, projectName);
|
|
327
|
+
synced++;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await interaction.editReply(
|
|
331
|
+
synced > 0
|
|
332
|
+
? `Synced ${synced} orphaned session(s).`
|
|
333
|
+
: 'No orphaned sessions found.',
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function handleClaudeModel(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
338
|
+
const session = sessions.getSessionByChannel(interaction.channelId);
|
|
339
|
+
if (!session) {
|
|
340
|
+
await interaction.reply({ content: 'No session in this channel.', ephemeral: true });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const model = interaction.options.getString('model', true);
|
|
345
|
+
sessions.setModel(session.id, model);
|
|
346
|
+
await interaction.reply({ content: `Model set to \`${model}\` for this session.`, ephemeral: true });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function handleClaudeVerbose(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
350
|
+
const session = sessions.getSessionByChannel(interaction.channelId);
|
|
351
|
+
if (!session) {
|
|
352
|
+
await interaction.reply({ content: 'No session in this channel.', ephemeral: true });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const newValue = !session.verbose;
|
|
357
|
+
sessions.setVerbose(session.id, newValue);
|
|
358
|
+
await interaction.reply({
|
|
359
|
+
content: newValue
|
|
360
|
+
? 'Verbose mode **enabled** — tool calls and results will be shown.'
|
|
361
|
+
: 'Verbose mode **disabled** — tool calls and results are now hidden.',
|
|
362
|
+
ephemeral: true,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// /shell commands
|
|
367
|
+
|
|
368
|
+
export async function handleShell(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
369
|
+
if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
|
|
370
|
+
await interaction.reply({ content: 'You are not authorized.', ephemeral: true });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const session = sessions.getSessionByChannel(interaction.channelId);
|
|
375
|
+
if (!session) {
|
|
376
|
+
await interaction.reply({ content: 'No session in this channel. Shell commands run in the session directory.', ephemeral: true });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const sub = interaction.options.getSubcommand();
|
|
381
|
+
|
|
382
|
+
switch (sub) {
|
|
383
|
+
case 'run': {
|
|
384
|
+
const command = interaction.options.getString('command', true);
|
|
385
|
+
await interaction.deferReply();
|
|
386
|
+
await interaction.editReply(`Running: \`${truncate(command, 100)}\``);
|
|
387
|
+
await executeShellCommand(command, session.directory, interaction.channel as TextChannel);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
case 'processes': {
|
|
391
|
+
const procs = listProcesses();
|
|
392
|
+
if (procs.length === 0) {
|
|
393
|
+
await interaction.reply({ content: 'No running processes.', ephemeral: true });
|
|
394
|
+
} else {
|
|
395
|
+
const lines = procs.map(p =>
|
|
396
|
+
`PID ${p.pid}: \`${truncate(p.command, 60)}\` (${formatUptime(p.startedAt)})`
|
|
397
|
+
);
|
|
398
|
+
await interaction.reply({ content: lines.join('\n'), ephemeral: true });
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case 'kill': {
|
|
403
|
+
const pid = interaction.options.getInteger('pid', true);
|
|
404
|
+
const killed = killProcess(pid);
|
|
405
|
+
await interaction.reply({
|
|
406
|
+
content: killed ? `Process ${pid} killed.` : `Process ${pid} not found.`,
|
|
407
|
+
ephemeral: true,
|
|
408
|
+
});
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// /agent commands
|
|
415
|
+
|
|
416
|
+
export async function handleAgent(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
417
|
+
if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
|
|
418
|
+
await interaction.reply({ content: 'You are not authorized.', ephemeral: true });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const sub = interaction.options.getSubcommand();
|
|
423
|
+
|
|
424
|
+
switch (sub) {
|
|
425
|
+
case 'use': {
|
|
426
|
+
const session = sessions.getSessionByChannel(interaction.channelId);
|
|
427
|
+
if (!session) {
|
|
428
|
+
await interaction.reply({ content: 'No session in this channel.', ephemeral: true });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const persona = interaction.options.getString('persona', true);
|
|
432
|
+
const agent = getAgent(persona);
|
|
433
|
+
if (!agent) {
|
|
434
|
+
await interaction.reply({ content: `Unknown persona: ${persona}`, ephemeral: true });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
sessions.setAgentPersona(session.id, persona === 'general' ? undefined : persona);
|
|
438
|
+
await interaction.reply({
|
|
439
|
+
content: persona === 'general'
|
|
440
|
+
? 'Agent persona cleared.'
|
|
441
|
+
: `${agent.emoji} Agent set to **${agent.name}**: ${agent.description}`,
|
|
442
|
+
ephemeral: true,
|
|
443
|
+
});
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
case 'list': {
|
|
447
|
+
const agents = listAgents();
|
|
448
|
+
const embed = new EmbedBuilder()
|
|
449
|
+
.setColor(0x9b59b6)
|
|
450
|
+
.setTitle('Agent Personas')
|
|
451
|
+
.setDescription(agents.map(a => `${a.emoji} **${a.name}** — ${a.description}`).join('\n'));
|
|
452
|
+
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case 'clear': {
|
|
456
|
+
const session = sessions.getSessionByChannel(interaction.channelId);
|
|
457
|
+
if (!session) {
|
|
458
|
+
await interaction.reply({ content: 'No session in this channel.', ephemeral: true });
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
sessions.setAgentPersona(session.id, undefined);
|
|
462
|
+
await interaction.reply({ content: 'Agent persona cleared.', ephemeral: true });
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// /project commands
|
|
469
|
+
|
|
470
|
+
export async function handleProject(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
471
|
+
if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
|
|
472
|
+
await interaction.reply({ content: 'You are not authorized.', ephemeral: true });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const session = sessions.getSessionByChannel(interaction.channelId);
|
|
477
|
+
if (!session) {
|
|
478
|
+
await interaction.reply({ content: 'No session in this channel. Run this in a session channel.', ephemeral: true });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const sub = interaction.options.getSubcommand();
|
|
483
|
+
const projectName = session.projectName;
|
|
484
|
+
|
|
485
|
+
switch (sub) {
|
|
486
|
+
case 'personality': {
|
|
487
|
+
const prompt = interaction.options.getString('prompt', true);
|
|
488
|
+
projectMgr.setPersonality(projectName, prompt);
|
|
489
|
+
await interaction.reply({ content: `Project personality set for **${projectName}**.`, ephemeral: true });
|
|
490
|
+
log(`Project "${projectName}" personality set by ${interaction.user.tag}`);
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
case 'personality-show': {
|
|
494
|
+
const personality = projectMgr.getPersonality(projectName);
|
|
495
|
+
await interaction.reply({
|
|
496
|
+
content: personality
|
|
497
|
+
? `**${projectName}** personality:\n\`\`\`\n${personality}\n\`\`\``
|
|
498
|
+
: `No personality set for **${projectName}**.`,
|
|
499
|
+
ephemeral: true,
|
|
500
|
+
});
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
case 'personality-clear': {
|
|
504
|
+
projectMgr.clearPersonality(projectName);
|
|
505
|
+
await interaction.reply({ content: `Personality cleared for **${projectName}**.`, ephemeral: true });
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
case 'skill-add': {
|
|
509
|
+
const name = interaction.options.getString('name', true);
|
|
510
|
+
const prompt = interaction.options.getString('prompt', true);
|
|
511
|
+
projectMgr.addSkill(projectName, name, prompt);
|
|
512
|
+
await interaction.reply({ content: `Skill **${name}** added to **${projectName}**.`, ephemeral: true });
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
case 'skill-remove': {
|
|
516
|
+
const name = interaction.options.getString('name', true);
|
|
517
|
+
const removed = projectMgr.removeSkill(projectName, name);
|
|
518
|
+
await interaction.reply({
|
|
519
|
+
content: removed ? `Skill **${name}** removed.` : `Skill **${name}** not found.`,
|
|
520
|
+
ephemeral: true,
|
|
521
|
+
});
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case 'skill-list': {
|
|
525
|
+
const skills = projectMgr.getSkills(projectName);
|
|
526
|
+
const entries = Object.entries(skills);
|
|
527
|
+
if (entries.length === 0) {
|
|
528
|
+
await interaction.reply({ content: `No skills configured for **${projectName}**.`, ephemeral: true });
|
|
529
|
+
} else {
|
|
530
|
+
const list = entries.map(([name, prompt]) => `**${name}**: ${truncate(prompt, 100)}`).join('\n');
|
|
531
|
+
await interaction.reply({ content: `Skills for **${projectName}**:\n${list}`, ephemeral: true });
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
case 'skill-run': {
|
|
536
|
+
const name = interaction.options.getString('name', true);
|
|
537
|
+
const input = interaction.options.getString('input') || undefined;
|
|
538
|
+
const expanded = projectMgr.executeSkill(projectName, name, input);
|
|
539
|
+
if (!expanded) {
|
|
540
|
+
await interaction.reply({ content: `Skill **${name}** not found.`, ephemeral: true });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
await interaction.deferReply();
|
|
544
|
+
try {
|
|
545
|
+
const channel = interaction.channel as TextChannel;
|
|
546
|
+
await interaction.editReply(`Running skill **${name}**...`);
|
|
547
|
+
const stream = sessions.sendPrompt(session.id, expanded);
|
|
548
|
+
await handleOutputStream(stream, channel, session.id, session.verbose);
|
|
549
|
+
} catch (err: unknown) {
|
|
550
|
+
await interaction.editReply(`Error: ${(err as Error).message}`);
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case 'mcp-add': {
|
|
555
|
+
const name = interaction.options.getString('name', true);
|
|
556
|
+
const command = interaction.options.getString('command', true);
|
|
557
|
+
const argsStr = interaction.options.getString('args');
|
|
558
|
+
const args = argsStr ? argsStr.split(',').map(a => a.trim()) : undefined;
|
|
559
|
+
|
|
560
|
+
await projectMgr.addMcpServer(session.directory, projectName, { name, command, args });
|
|
561
|
+
await interaction.reply({ content: `MCP server **${name}** added to **${projectName}**.`, ephemeral: true });
|
|
562
|
+
log(`MCP server "${name}" added to project "${projectName}" by ${interaction.user.tag}`);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
case 'mcp-remove': {
|
|
566
|
+
const name = interaction.options.getString('name', true);
|
|
567
|
+
const removed = await projectMgr.removeMcpServer(session.directory, projectName, name);
|
|
568
|
+
await interaction.reply({
|
|
569
|
+
content: removed ? `MCP server **${name}** removed.` : `MCP server **${name}** not found.`,
|
|
570
|
+
ephemeral: true,
|
|
571
|
+
});
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case 'mcp-list': {
|
|
575
|
+
const servers = projectMgr.listMcpServers(projectName);
|
|
576
|
+
if (servers.length === 0) {
|
|
577
|
+
await interaction.reply({ content: `No MCP servers configured for **${projectName}**.`, ephemeral: true });
|
|
578
|
+
} else {
|
|
579
|
+
const list = servers.map(s => {
|
|
580
|
+
const args = s.args?.length ? ` ${s.args.join(' ')}` : '';
|
|
581
|
+
return `**${s.name}**: \`${s.command}${args}\``;
|
|
582
|
+
}).join('\n');
|
|
583
|
+
await interaction.reply({ content: `MCP servers for **${projectName}**:\n${list}`, ephemeral: true });
|
|
584
|
+
}
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case 'info': {
|
|
588
|
+
const project = projectMgr.getProject(projectName);
|
|
589
|
+
if (!project) {
|
|
590
|
+
await interaction.reply({ content: 'Project not found.', ephemeral: true });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const embed = new EmbedBuilder()
|
|
595
|
+
.setColor(0xf39c12)
|
|
596
|
+
.setTitle(`Project: ${projectName}`)
|
|
597
|
+
.addFields(
|
|
598
|
+
{ name: 'Directory', value: `\`${project.directory}\``, inline: false },
|
|
599
|
+
{
|
|
600
|
+
name: 'Personality',
|
|
601
|
+
value: project.personality ? truncate(project.personality, 200) : 'None',
|
|
602
|
+
inline: false,
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
name: 'Skills',
|
|
606
|
+
value: Object.keys(project.skills).length > 0
|
|
607
|
+
? Object.keys(project.skills).join(', ')
|
|
608
|
+
: 'None',
|
|
609
|
+
inline: true,
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: 'MCP Servers',
|
|
613
|
+
value: project.mcpServers.length > 0
|
|
614
|
+
? project.mcpServers.map(s => s.name).join(', ')
|
|
615
|
+
: 'None',
|
|
616
|
+
inline: true,
|
|
617
|
+
},
|
|
618
|
+
);
|
|
619
|
+
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|