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.
@@ -0,0 +1,166 @@
1
+ import {
2
+ SlashCommandBuilder,
3
+ REST,
4
+ Routes,
5
+ type RESTPostAPIChatInputApplicationCommandsJSONBody,
6
+ } from 'discord.js';
7
+ import { config } from './config.ts';
8
+
9
+ export function getCommandDefinitions(): RESTPostAPIChatInputApplicationCommandsJSONBody[] {
10
+ const claude = new SlashCommandBuilder()
11
+ .setName('claude')
12
+ .setDescription('Manage Claude Code sessions')
13
+ .addSubcommand(sub =>
14
+ sub.setName('new')
15
+ .setDescription('Create a new Claude Code session')
16
+ .addStringOption(opt =>
17
+ opt.setName('name').setDescription('Session name').setRequired(true))
18
+ .addStringOption(opt =>
19
+ opt.setName('directory').setDescription('Working directory (default: configured default)')))
20
+ .addSubcommand(sub =>
21
+ sub.setName('list').setDescription('List active sessions'))
22
+ .addSubcommand(sub =>
23
+ sub.setName('end').setDescription('End the session in this channel'))
24
+ .addSubcommand(sub =>
25
+ sub.setName('continue').setDescription('Continue the last conversation'))
26
+ .addSubcommand(sub =>
27
+ sub.setName('stop').setDescription('Stop current generation'))
28
+ .addSubcommand(sub =>
29
+ sub.setName('output')
30
+ .setDescription('Show recent conversation output')
31
+ .addIntegerOption(opt =>
32
+ opt.setName('lines').setDescription('Number of lines (default 50)').setMinValue(1).setMaxValue(500)))
33
+ .addSubcommand(sub =>
34
+ sub.setName('attach').setDescription('Show tmux attach command for terminal access'))
35
+ .addSubcommand(sub =>
36
+ sub.setName('sync').setDescription('Reconnect orphaned tmux sessions'))
37
+ .addSubcommand(sub =>
38
+ sub.setName('model')
39
+ .setDescription('Change the model for this session')
40
+ .addStringOption(opt =>
41
+ opt.setName('model').setDescription('Model name (e.g. claude-sonnet-4-5-20250929)').setRequired(true)))
42
+ .addSubcommand(sub =>
43
+ sub.setName('verbose').setDescription('Toggle showing tool calls and results in this session'));
44
+
45
+ const shell = new SlashCommandBuilder()
46
+ .setName('shell')
47
+ .setDescription('Run shell commands in the session directory')
48
+ .addSubcommand(sub =>
49
+ sub.setName('run')
50
+ .setDescription('Execute a shell command')
51
+ .addStringOption(opt =>
52
+ opt.setName('command').setDescription('Command to run').setRequired(true)))
53
+ .addSubcommand(sub =>
54
+ sub.setName('processes').setDescription('List running processes'))
55
+ .addSubcommand(sub =>
56
+ sub.setName('kill')
57
+ .setDescription('Kill a running process')
58
+ .addIntegerOption(opt =>
59
+ opt.setName('pid').setDescription('Process ID to kill').setRequired(true)));
60
+
61
+ const agent = new SlashCommandBuilder()
62
+ .setName('agent')
63
+ .setDescription('Manage agent personas')
64
+ .addSubcommand(sub =>
65
+ sub.setName('use')
66
+ .setDescription('Switch to an agent persona')
67
+ .addStringOption(opt =>
68
+ opt.setName('persona')
69
+ .setDescription('Agent persona name')
70
+ .setRequired(true)
71
+ .addChoices(
72
+ { name: 'Code Reviewer', value: 'code-reviewer' },
73
+ { name: 'Architect', value: 'architect' },
74
+ { name: 'Debugger', value: 'debugger' },
75
+ { name: 'Security Analyst', value: 'security' },
76
+ { name: 'Performance Engineer', value: 'performance' },
77
+ { name: 'DevOps Engineer', value: 'devops' },
78
+ { name: 'General', value: 'general' },
79
+ )))
80
+ .addSubcommand(sub =>
81
+ sub.setName('list').setDescription('List available agent personas'))
82
+ .addSubcommand(sub =>
83
+ sub.setName('clear').setDescription('Clear agent persona'));
84
+
85
+ const project = new SlashCommandBuilder()
86
+ .setName('project')
87
+ .setDescription('Configure project settings')
88
+ .addSubcommand(sub =>
89
+ sub.setName('personality')
90
+ .setDescription('Set a custom personality for this project')
91
+ .addStringOption(opt =>
92
+ opt.setName('prompt').setDescription('System prompt for the project').setRequired(true)))
93
+ .addSubcommand(sub =>
94
+ sub.setName('personality-show').setDescription('Show the current project personality'))
95
+ .addSubcommand(sub =>
96
+ sub.setName('personality-clear').setDescription('Clear the project personality'))
97
+ .addSubcommand(sub =>
98
+ sub.setName('skill-add')
99
+ .setDescription('Add a skill (prompt template) to this project')
100
+ .addStringOption(opt =>
101
+ opt.setName('name').setDescription('Skill name').setRequired(true))
102
+ .addStringOption(opt =>
103
+ opt.setName('prompt').setDescription('Prompt template (use {input} for placeholder)').setRequired(true)))
104
+ .addSubcommand(sub =>
105
+ sub.setName('skill-remove')
106
+ .setDescription('Remove a skill')
107
+ .addStringOption(opt =>
108
+ opt.setName('name').setDescription('Skill name').setRequired(true)))
109
+ .addSubcommand(sub =>
110
+ sub.setName('skill-list').setDescription('List all skills for this project'))
111
+ .addSubcommand(sub =>
112
+ sub.setName('skill-run')
113
+ .setDescription('Execute a skill')
114
+ .addStringOption(opt =>
115
+ opt.setName('name').setDescription('Skill name').setRequired(true))
116
+ .addStringOption(opt =>
117
+ opt.setName('input').setDescription('Input to pass to the skill template')))
118
+ .addSubcommand(sub =>
119
+ sub.setName('mcp-add')
120
+ .setDescription('Add an MCP server to this project')
121
+ .addStringOption(opt =>
122
+ opt.setName('name').setDescription('Server name').setRequired(true))
123
+ .addStringOption(opt =>
124
+ opt.setName('command').setDescription('Command to run (e.g. npx my-mcp-server)').setRequired(true))
125
+ .addStringOption(opt =>
126
+ opt.setName('args').setDescription('Arguments (comma-separated)')))
127
+ .addSubcommand(sub =>
128
+ sub.setName('mcp-remove')
129
+ .setDescription('Remove an MCP server')
130
+ .addStringOption(opt =>
131
+ opt.setName('name').setDescription('Server name').setRequired(true)))
132
+ .addSubcommand(sub =>
133
+ sub.setName('mcp-list').setDescription('List configured MCP servers'))
134
+ .addSubcommand(sub =>
135
+ sub.setName('info').setDescription('Show project configuration'));
136
+
137
+ return [
138
+ claude.toJSON(),
139
+ shell.toJSON(),
140
+ agent.toJSON(),
141
+ project.toJSON(),
142
+ ];
143
+ }
144
+
145
+ export async function registerCommands(): Promise<void> {
146
+ const rest = new REST().setToken(config.token);
147
+ const commands = getCommandDefinitions();
148
+
149
+ try {
150
+ if (config.guildId) {
151
+ await rest.put(
152
+ Routes.applicationGuildCommands(config.clientId, config.guildId),
153
+ { body: commands },
154
+ );
155
+ console.log(`Registered ${commands.length} guild commands`);
156
+ } else {
157
+ await rest.put(
158
+ Routes.applicationCommands(config.clientId),
159
+ { body: commands },
160
+ );
161
+ console.log(`Registered ${commands.length} global commands (may take ~1hr to propagate)`);
162
+ }
163
+ } catch (err) {
164
+ console.error('Failed to register commands:', err);
165
+ }
166
+ }
package/src/config.ts ADDED
@@ -0,0 +1,45 @@
1
+ import 'dotenv/config';
2
+ import type { Config } from './types.ts';
3
+
4
+ function getEnvOrExit(name: string): string {
5
+ const value = process.env[name];
6
+ if (!value) {
7
+ console.error(`ERROR: ${name} environment variable is required`);
8
+ console.error('Set it in your .env file or export it before running');
9
+ process.exit(1);
10
+ }
11
+ return value;
12
+ }
13
+
14
+ export const config: Config = {
15
+ token: getEnvOrExit('DISCORD_TOKEN'),
16
+ clientId: getEnvOrExit('DISCORD_CLIENT_ID'),
17
+ guildId: process.env.DISCORD_GUILD_ID || null,
18
+ allowedUsers: process.env.ALLOWED_USERS?.split(',').map(id => id.trim()).filter(Boolean) || [],
19
+ allowAllUsers: process.env.ALLOW_ALL_USERS === 'true',
20
+ allowedPaths: process.env.ALLOWED_PATHS?.split(',').map(p => p.trim()).filter(Boolean) || [],
21
+ defaultDirectory: process.env.DEFAULT_DIRECTORY || process.cwd(),
22
+ messageRetentionDays: process.env.MESSAGE_RETENTION_DAYS
23
+ ? parseInt(process.env.MESSAGE_RETENTION_DAYS, 10)
24
+ : null,
25
+ rateLimitMs: process.env.RATE_LIMIT_MS
26
+ ? parseInt(process.env.RATE_LIMIT_MS, 10)
27
+ : 1000,
28
+ };
29
+
30
+ if (config.allowedUsers.length > 0) {
31
+ console.log(`User whitelist: ${config.allowedUsers.length} user(s) allowed`);
32
+ } else if (config.allowAllUsers) {
33
+ console.warn('WARNING: ALLOW_ALL_USERS=true — anyone in the guild can use this bot');
34
+ } else {
35
+ console.error('ERROR: Set ALLOWED_USERS or ALLOW_ALL_USERS=true');
36
+ process.exit(1);
37
+ }
38
+
39
+ if (config.allowedPaths.length > 0) {
40
+ console.log(`Path restrictions: ${config.allowedPaths.join(', ')}`);
41
+ }
42
+
43
+ if (config.messageRetentionDays) {
44
+ console.log(`Message retention: ${config.messageRetentionDays} day(s)`);
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ const envPath = resolve(process.cwd(), '.env');
5
+
6
+ if (!existsSync(envPath)) {
7
+ console.log('\x1b[33mNo .env file found in the current directory.\x1b[0m');
8
+ console.log('Run \x1b[36magentcord setup\x1b[0m to configure.\n');
9
+ process.exit(1);
10
+ }
11
+
12
+ const { startBot } = await import('./bot.ts');
13
+
14
+ console.log('agentcord starting...');
15
+ startBot().catch(err => {
16
+ console.error('Fatal error:', err);
17
+ process.exit(1);
18
+ });
@@ -0,0 +1,60 @@
1
+ import type { Message, TextChannel } from 'discord.js';
2
+ import { config } from './config.ts';
3
+ import * as sessions from './session-manager.ts';
4
+ import { handleOutputStream } from './output-handler.ts';
5
+ import { isUserAllowed } from './utils.ts';
6
+
7
+ const userLastMessage = new Map<string, number>();
8
+
9
+ export async function handleMessage(message: Message): Promise<void> {
10
+ if (message.author.bot) return;
11
+
12
+ // Only handle messages in session channels
13
+ const session = sessions.getSessionByChannel(message.channelId);
14
+ if (!session) return;
15
+
16
+ // Auth check
17
+ if (!isUserAllowed(message.author.id, config.allowedUsers, config.allowAllUsers)) {
18
+ return;
19
+ }
20
+
21
+ // Rate limit
22
+ const now = Date.now();
23
+ const lastMsg = userLastMessage.get(message.author.id) || 0;
24
+ if (now - lastMsg < config.rateLimitMs) {
25
+ await message.react('⏳');
26
+ return;
27
+ }
28
+ userLastMessage.set(message.author.id, now);
29
+
30
+ // Interrupt current generation if active
31
+ if (session.isGenerating) {
32
+ sessions.abortSession(session.id);
33
+ // Wait for the abort to finish (up to 5s)
34
+ const deadline = Date.now() + 5000;
35
+ while (session.isGenerating && Date.now() < deadline) {
36
+ await new Promise(r => setTimeout(r, 100));
37
+ }
38
+ if (session.isGenerating) {
39
+ await message.reply({
40
+ content: 'Could not interrupt the current generation. Try `/claude stop`.',
41
+ allowedMentions: { repliedUser: false },
42
+ });
43
+ return;
44
+ }
45
+ }
46
+
47
+ const content = message.content.trim();
48
+ if (!content) return;
49
+
50
+ try {
51
+ const channel = message.channel as TextChannel;
52
+ const stream = sessions.sendPrompt(session.id, content);
53
+ await handleOutputStream(stream, channel, session.id, session.verbose);
54
+ } catch (err: unknown) {
55
+ await message.reply({
56
+ content: `Error: ${(err as Error).message}`,
57
+ allowedMentions: { repliedUser: false },
58
+ });
59
+ }
60
+ }