@cordbot/agent 1.0.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,359 @@
1
+ import { AttachmentBuilder } from 'discord.js';
2
+ export async function streamToDiscord(queryResult, threadChannel, sessionManager, sessionId, workingDir, messagePrefix) {
3
+ const state = {
4
+ currentToolUse: null,
5
+ toolMessages: [],
6
+ progressMessages: [],
7
+ planContent: null,
8
+ messagePrefix: messagePrefix || null,
9
+ workingDir,
10
+ };
11
+ try {
12
+ // Iterate through SDK messages
13
+ for await (const message of queryResult) {
14
+ await handleSDKMessage(message, threadChannel, state, sessionManager, sessionId);
15
+ }
16
+ // Save session ID for resumption
17
+ await sessionManager.updateSession(sessionId, threadChannel.id);
18
+ // Attach any files queued for sharing
19
+ await attachSharedFiles(threadChannel, sessionManager, sessionId);
20
+ }
21
+ catch (error) {
22
+ console.error('Stream error:', error);
23
+ await threadChannel.send(`āŒ Failed to process: ${error instanceof Error ? error.message : 'Unknown error'}`);
24
+ }
25
+ }
26
+ async function handleSDKMessage(message, channel, state, sessionManager, sessionId) {
27
+ switch (message.type) {
28
+ case 'assistant':
29
+ // Final assistant message with complete response
30
+ const content = extractTextFromMessage(message.message);
31
+ if (content) {
32
+ await sendCompleteMessage(channel, content, state.planContent, state.messagePrefix);
33
+ state.planContent = null; // Clear plan after sending
34
+ state.messagePrefix = null; // Clear prefix after using
35
+ }
36
+ break;
37
+ case 'stream_event':
38
+ // Partial message during streaming
39
+ await handleStreamEvent(message, channel, state);
40
+ break;
41
+ case 'user':
42
+ // User message (echo or replay) - ignore for Discord
43
+ break;
44
+ case 'system':
45
+ // System initialization message
46
+ if (message.subtype === 'init') {
47
+ console.log(`Session ${message.session_id} initialized with model ${message.model}`);
48
+ // Update database with real SDK session ID
49
+ await sessionManager.updateSessionId(sessionId, message.session_id, channel.id);
50
+ }
51
+ else if (message.subtype === 'compact_boundary') {
52
+ console.log(`Conversation compacted: ${message.compact_metadata.trigger}`);
53
+ await channel.send(`šŸ—œļø Conversation history compacted (${message.compact_metadata.pre_tokens} tokens)`);
54
+ }
55
+ break;
56
+ case 'result':
57
+ // Final result message
58
+ if (message.subtype === 'success') {
59
+ console.log(`āœ… Session completed: ${message.num_turns} turns, $${message.total_cost_usd.toFixed(4)}`);
60
+ }
61
+ else {
62
+ // Error result
63
+ const errors = 'errors' in message ? message.errors : [];
64
+ await channel.send(`āŒ **Error**: ${errors.join(', ')}`);
65
+ }
66
+ break;
67
+ }
68
+ }
69
+ async function handleStreamEvent(message, channel, state) {
70
+ const event = message.event;
71
+ switch (event.type) {
72
+ case 'message_start':
73
+ // New message starting
74
+ break;
75
+ case 'content_block_start':
76
+ // New content block starting
77
+ if (event.content_block.type === 'tool_use') {
78
+ // Tool use starting
79
+ state.currentToolUse = {
80
+ name: event.content_block.name,
81
+ input: '',
82
+ id: event.content_block.id,
83
+ };
84
+ }
85
+ break;
86
+ case 'content_block_delta':
87
+ // Content streaming - accumulate tool inputs for display
88
+ if (event.delta.type === 'input_json_delta') {
89
+ // Accumulate tool input JSON
90
+ if (state.currentToolUse) {
91
+ state.currentToolUse.input += event.delta.partial_json;
92
+ }
93
+ }
94
+ // Text deltas are ignored - we'll get the complete text in the 'assistant' message
95
+ break;
96
+ case 'content_block_stop':
97
+ // Content block finished - NOW send complete block to Discord
98
+ if (state.currentToolUse) {
99
+ // Special handling for ExitPlanMode - extract plan for attachment
100
+ if (state.currentToolUse.name === 'ExitPlanMode') {
101
+ try {
102
+ const input = JSON.parse(state.currentToolUse.input);
103
+ if (input.plan) {
104
+ state.planContent = input.plan;
105
+ // Send notification that plan is ready
106
+ const msg = await channel.send('šŸ“‹ **Plan Generated** - see attachment below');
107
+ state.progressMessages.push(msg);
108
+ }
109
+ }
110
+ catch (e) {
111
+ console.error('Failed to parse ExitPlanMode input:', e);
112
+ }
113
+ }
114
+ else {
115
+ // Send complete tool use message for other tools
116
+ const shouldShow = shouldShowToolMessage(state.currentToolUse.name, state.currentToolUse.input);
117
+ if (shouldShow) {
118
+ const emoji = getToolEmoji(state.currentToolUse.name);
119
+ const description = getToolDescription(state.currentToolUse.name, state.currentToolUse.input, state.workingDir);
120
+ // Strip MCP server prefix from tool name for cleaner display
121
+ const displayName = stripMcpPrefix(state.currentToolUse.name);
122
+ const msg = await channel.send(`\`\`\`\n${emoji} ${displayName}: ${description}\n\`\`\``);
123
+ state.toolMessages.push(msg);
124
+ }
125
+ }
126
+ state.currentToolUse = null;
127
+ }
128
+ break;
129
+ case 'message_delta':
130
+ // Message metadata update (stop_reason, usage, etc.)
131
+ if (event.delta.stop_reason === 'max_tokens') {
132
+ await channel.send('āš ļø Response truncated due to token limit');
133
+ }
134
+ break;
135
+ case 'message_stop':
136
+ // Message complete - nothing to do, we'll get the complete text in 'assistant' message
137
+ break;
138
+ }
139
+ }
140
+ function extractTextFromMessage(message) {
141
+ let text = '';
142
+ if (Array.isArray(message.content)) {
143
+ for (const block of message.content) {
144
+ if (block.type === 'text') {
145
+ text += block.text;
146
+ }
147
+ }
148
+ }
149
+ else if (typeof message.content === 'string') {
150
+ text = message.content;
151
+ }
152
+ return text.trim();
153
+ }
154
+ async function sendCompleteMessage(channel, content, planContent, messagePrefix) {
155
+ console.log(`šŸ“¤ Sending message to Discord (${content.length} chars)`);
156
+ // Prepend prefix if provided
157
+ let fullContent = content;
158
+ if (messagePrefix) {
159
+ fullContent = `${messagePrefix}\n\n${content}`;
160
+ }
161
+ // Split message if it exceeds Discord's 2000 character limit
162
+ const chunks = splitMessage(fullContent, 2000);
163
+ for (let i = 0; i < chunks.length; i++) {
164
+ const chunk = chunks[i];
165
+ // Attach plan file only on the last chunk
166
+ if (i === chunks.length - 1 && planContent) {
167
+ const attachment = new AttachmentBuilder(Buffer.from(planContent, 'utf-8'), {
168
+ name: `plan-${Date.now()}.md`,
169
+ description: 'Claude Code Plan',
170
+ });
171
+ await channel.send({
172
+ content: chunk,
173
+ files: [attachment],
174
+ });
175
+ }
176
+ else {
177
+ await channel.send(chunk);
178
+ }
179
+ }
180
+ }
181
+ function splitMessage(text, maxLength) {
182
+ if (text.length <= maxLength) {
183
+ return [text];
184
+ }
185
+ const chunks = [];
186
+ let currentChunk = '';
187
+ const lines = text.split('\n');
188
+ for (const line of lines) {
189
+ if (currentChunk.length + line.length + 1 > maxLength) {
190
+ // Current line would exceed limit
191
+ if (currentChunk) {
192
+ chunks.push(currentChunk);
193
+ currentChunk = '';
194
+ }
195
+ // If single line is too long, split it by words
196
+ if (line.length > maxLength) {
197
+ const words = line.split(' ');
198
+ for (const word of words) {
199
+ if (currentChunk.length + word.length + 1 > maxLength) {
200
+ chunks.push(currentChunk);
201
+ currentChunk = word;
202
+ }
203
+ else {
204
+ currentChunk += (currentChunk ? ' ' : '') + word;
205
+ }
206
+ }
207
+ }
208
+ else {
209
+ currentChunk = line;
210
+ }
211
+ }
212
+ else {
213
+ currentChunk += (currentChunk ? '\n' : '') + line;
214
+ }
215
+ }
216
+ if (currentChunk) {
217
+ chunks.push(currentChunk);
218
+ }
219
+ return chunks;
220
+ }
221
+ function shouldShowToolMessage(toolName, input) {
222
+ try {
223
+ const params = JSON.parse(input);
224
+ // Filter out internal file operations
225
+ if (toolName === 'Read' || toolName === 'Write' || toolName === 'Edit') {
226
+ const filePath = params.file_path || '';
227
+ // Hide operations on internal files
228
+ if (filePath.includes('.claude-cron') ||
229
+ filePath.includes('.claude/') ||
230
+ filePath.includes('CLAUDE.md')) {
231
+ return false;
232
+ }
233
+ }
234
+ // Filter out cron tool usage messages - keep them silent/internal
235
+ if (toolName.startsWith('cron_')) {
236
+ return false;
237
+ }
238
+ return true;
239
+ }
240
+ catch (e) {
241
+ // If we can't parse, show the message
242
+ return true;
243
+ }
244
+ }
245
+ function stripMcpPrefix(toolName) {
246
+ // Remove MCP server prefix like "mcp__cordbot-dynamic-tools__"
247
+ const mcpPrefixRegex = /^mcp__[^_]+__/;
248
+ return toolName.replace(mcpPrefixRegex, '');
249
+ }
250
+ function stripWorkingDirPrefix(filePath, workingDir) {
251
+ // If the file path starts with the working directory, strip it to show relative path
252
+ if (filePath.startsWith(workingDir)) {
253
+ const relative = filePath.slice(workingDir.length);
254
+ // Remove leading slash if present
255
+ return relative.startsWith('/') ? relative.slice(1) : relative;
256
+ }
257
+ return filePath;
258
+ }
259
+ function getToolEmoji(toolName) {
260
+ // Strip MCP prefix for emoji lookup
261
+ const cleanName = stripMcpPrefix(toolName);
262
+ const emojiMap = {
263
+ Bash: 'āš™ļø',
264
+ Read: 'šŸ“„',
265
+ Write: 'āœļø',
266
+ Edit: 'āœļø',
267
+ Glob: 'šŸ”',
268
+ Grep: 'šŸ”Ž',
269
+ WebFetch: '🌐',
270
+ WebSearch: 'šŸ”Ž',
271
+ Task: 'šŸ¤–',
272
+ shareFile: 'šŸ“Ž',
273
+ cron_list_jobs: 'ā°',
274
+ cron_add_job: 'āž•',
275
+ cron_remove_job: 'šŸ—‘ļø',
276
+ cron_update_job: 'āœļø',
277
+ gmail_send_email: 'šŸ“§',
278
+ gmail_list_messages: 'šŸ“¬',
279
+ };
280
+ return emojiMap[cleanName] || 'šŸ”§';
281
+ }
282
+ function getToolDescription(toolName, input, workingDir) {
283
+ try {
284
+ const params = JSON.parse(input);
285
+ switch (toolName) {
286
+ case 'Bash':
287
+ return params.description || params.command || 'Running command';
288
+ case 'Read':
289
+ return stripWorkingDirPrefix(params.file_path || 'Reading file', workingDir);
290
+ case 'Write':
291
+ return stripWorkingDirPrefix(params.file_path || 'Writing file', workingDir);
292
+ case 'Edit':
293
+ return stripWorkingDirPrefix(params.file_path || 'Editing file', workingDir);
294
+ case 'Glob':
295
+ return params.pattern || 'Searching files';
296
+ case 'Grep':
297
+ return `Searching for "${params.pattern}"`;
298
+ case 'WebFetch':
299
+ return params.url || 'Fetching URL';
300
+ case 'WebSearch':
301
+ return `Searching: ${params.query}`;
302
+ case 'Task':
303
+ return params.description || params.prompt?.slice(0, 100) || 'Running task';
304
+ case 'AskUserQuestion':
305
+ return 'Asking question';
306
+ case 'TaskCreate':
307
+ return params.subject || 'Creating task';
308
+ case 'TaskUpdate':
309
+ return `Updating task ${params.taskId}`;
310
+ case 'TaskList':
311
+ return 'Listing tasks';
312
+ case 'EnterPlanMode':
313
+ return 'Entering plan mode';
314
+ case 'shareFile':
315
+ return `Sharing file: ${params.filePath}`;
316
+ case 'cron_list_jobs':
317
+ return 'Listing scheduled jobs';
318
+ case 'cron_add_job':
319
+ return `Adding job: ${params.name}`;
320
+ case 'cron_remove_job':
321
+ return `Removing job: ${params.name}`;
322
+ case 'cron_update_job':
323
+ return `Updating job: ${params.name}`;
324
+ case 'gmail_send_email':
325
+ return `Sending email to ${params.to}`;
326
+ case 'gmail_list_messages':
327
+ return params.query ? `Listing emails: ${params.query}` : 'Listing recent emails';
328
+ default:
329
+ // For unknown tools, try to extract first parameter value
330
+ const firstValue = Object.values(params)[0];
331
+ if (typeof firstValue === 'string') {
332
+ return firstValue.slice(0, 100);
333
+ }
334
+ return 'Running tool';
335
+ }
336
+ }
337
+ catch (e) {
338
+ // If JSON parsing fails, return truncated raw input
339
+ return input.slice(0, 100);
340
+ }
341
+ }
342
+ async function attachSharedFiles(channel, sessionManager, sessionId) {
343
+ const filesToShare = sessionManager.getFilesToShare(sessionId);
344
+ if (filesToShare.length === 0) {
345
+ return;
346
+ }
347
+ try {
348
+ const attachments = filesToShare.map(filePath => new AttachmentBuilder(filePath));
349
+ await channel.send({
350
+ content: 'šŸ“Ž Shared files:',
351
+ files: attachments
352
+ });
353
+ console.log(`šŸ“Ž Attached ${filesToShare.length} shared file(s)`);
354
+ }
355
+ catch (error) {
356
+ console.error('Failed to attach shared files:', error);
357
+ await channel.send(`āŒ Failed to attach shared files: ${error instanceof Error ? error.message : 'Unknown error'}`);
358
+ }
359
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,161 @@
1
+ import express from 'express';
2
+ import open from 'open';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ const WEB_SERVICE_URL = process.env.WEB_SERVICE_URL || 'https://cordbot.io';
6
+ export async function authenticateWithWebService() {
7
+ // Find available port
8
+ const port = await findAvailablePort(3456);
9
+ const callbackUrl = `http://localhost:${port}/callback`;
10
+ console.log(chalk.cyan('\nšŸ” Cordbot Authentication\n'));
11
+ console.log(chalk.gray('To use Cordbot, you need to authenticate with the web service.\n'));
12
+ console.log(chalk.yellow('Press ENTER to open your browser and sign in...'));
13
+ // Set stdin to raw mode temporarily
14
+ if (process.stdin.isTTY) {
15
+ process.stdin.setRawMode(true);
16
+ }
17
+ process.stdin.resume();
18
+ // Wait for user to press ENTER
19
+ await waitForEnter();
20
+ // Restore stdin
21
+ if (process.stdin.isTTY) {
22
+ process.stdin.setRawMode(false);
23
+ }
24
+ // Start local server
25
+ const result = await startCallbackServer(port, callbackUrl);
26
+ return result;
27
+ }
28
+ async function waitForEnter() {
29
+ return new Promise((resolve) => {
30
+ const onData = (key) => {
31
+ // Check for Enter key (carriage return or newline)
32
+ if (key[0] === 13 || key[0] === 10) {
33
+ process.stdin.removeListener('data', onData);
34
+ resolve();
35
+ }
36
+ };
37
+ process.stdin.on('data', onData);
38
+ });
39
+ }
40
+ async function findAvailablePort(startPort) {
41
+ let port = startPort;
42
+ while (port < startPort + 100) {
43
+ try {
44
+ await new Promise((resolve, reject) => {
45
+ const server = express().listen(port)
46
+ .on('listening', () => {
47
+ server.close();
48
+ resolve();
49
+ })
50
+ .on('error', reject);
51
+ });
52
+ return port;
53
+ }
54
+ catch {
55
+ port++;
56
+ }
57
+ }
58
+ throw new Error('Could not find available port');
59
+ }
60
+ async function startCallbackServer(port, callbackUrl) {
61
+ return new Promise((resolve) => {
62
+ const app = express();
63
+ let server;
64
+ const spinner = ora('Waiting for authentication...').start();
65
+ // Timeout after 5 minutes
66
+ const timeout = setTimeout(() => {
67
+ spinner.fail(chalk.red('Authentication timed out'));
68
+ server?.close();
69
+ resolve(null);
70
+ }, 5 * 60 * 1000);
71
+ app.get('/callback', (req, res) => {
72
+ const { token, guildId, error } = req.query;
73
+ if (error) {
74
+ spinner.fail(chalk.red('Authentication failed'));
75
+ if (error === 'no_bot') {
76
+ console.log(chalk.yellow('\nāš ļø No bot configured'));
77
+ console.log(chalk.gray(`\nPlease visit ${WEB_SERVICE_URL} to:`));
78
+ console.log(chalk.gray(' 1. Sign in with Discord'));
79
+ console.log(chalk.gray(' 2. Set up your Discord bot'));
80
+ console.log(chalk.gray(' 3. Configure your bot token'));
81
+ console.log(chalk.gray('\nThen run "npx cordbot" again.\n'));
82
+ }
83
+ else if (error === 'not_authenticated') {
84
+ console.log(chalk.yellow('\nāš ļø Not authenticated'));
85
+ console.log(chalk.gray(`\nPlease visit ${WEB_SERVICE_URL} to sign in first.\n`));
86
+ }
87
+ else {
88
+ console.log(chalk.red(`\nāŒ Error: ${error}\n`));
89
+ }
90
+ res.send(`
91
+ <!DOCTYPE html>
92
+ <html>
93
+ <head>
94
+ <title>Cordbot - Authentication Failed</title>
95
+ <style>
96
+ body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 100px auto; padding: 20px; text-align: center; }
97
+ .error { color: #dc2626; margin: 20px 0; }
98
+ .message { color: #6b7280; line-height: 1.6; }
99
+ </style>
100
+ </head>
101
+ <body>
102
+ <h1>āŒ Authentication Failed</h1>
103
+ <p class="error">${error === 'no_bot' ? 'No bot configured' : error === 'not_authenticated' ? 'Not authenticated' : String(error)}</p>
104
+ <p class="message">Please check the CLI for instructions.</p>
105
+ <p class="message">You can close this window.</p>
106
+ </body>
107
+ </html>
108
+ `);
109
+ clearTimeout(timeout);
110
+ setTimeout(() => {
111
+ server?.close();
112
+ resolve(null);
113
+ }, 2000);
114
+ return;
115
+ }
116
+ if (!token || !guildId) {
117
+ spinner.fail(chalk.red('Invalid response from server'));
118
+ res.send('Invalid response');
119
+ clearTimeout(timeout);
120
+ server?.close();
121
+ resolve(null);
122
+ return;
123
+ }
124
+ spinner.succeed(chalk.green('Successfully authenticated!'));
125
+ res.send(`
126
+ <!DOCTYPE html>
127
+ <html>
128
+ <head>
129
+ <title>Cordbot - Authentication Successful</title>
130
+ <style>
131
+ body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 100px auto; padding: 20px; text-align: center; }
132
+ .success { color: #16a34a; margin: 20px 0; }
133
+ .message { color: #6b7280; line-height: 1.6; }
134
+ </style>
135
+ </head>
136
+ <body>
137
+ <h1>āœ… Authentication Successful!</h1>
138
+ <p class="success">Your bot is now authenticated</p>
139
+ <p class="message">You can close this window and return to the CLI.</p>
140
+ </body>
141
+ </html>
142
+ `);
143
+ clearTimeout(timeout);
144
+ setTimeout(() => {
145
+ server?.close();
146
+ resolve({
147
+ botToken: token,
148
+ guildId: guildId,
149
+ });
150
+ }, 2000);
151
+ });
152
+ server = app.listen(port, () => {
153
+ // Open browser to web service auth page
154
+ const authUrl = `${WEB_SERVICE_URL}/auth/cli?callback=${encodeURIComponent(callbackUrl)}`;
155
+ open(authUrl).catch(err => {
156
+ spinner.warn(chalk.yellow('Could not open browser automatically'));
157
+ console.log(chalk.gray(`\nPlease open this URL in your browser:\n${authUrl}\n`));
158
+ });
159
+ });
160
+ });
161
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,128 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import dotenv from 'dotenv';
7
+ import { startBot } from './index.js';
8
+ import { authenticateWithWebService } from './auth.js';
9
+ export async function run() {
10
+ console.log(chalk.cyan.bold('\nšŸ¤– Cordbot - Discord Bot powered by Claude Code SDK\n'));
11
+ const cwd = process.cwd();
12
+ const envPath = path.join(cwd, '.env');
13
+ // Check if .env exists in current directory
14
+ if (!fs.existsSync(envPath)) {
15
+ console.log(chalk.yellow('No .env file found in current directory.'));
16
+ console.log(chalk.gray(`Current directory: ${cwd}\n`));
17
+ // Use web service authentication flow
18
+ const authResult = await authenticateWithWebService();
19
+ if (!authResult) {
20
+ console.log(chalk.red('\nāŒ Authentication failed. Exiting.\n'));
21
+ process.exit(1);
22
+ }
23
+ // Prompt for Claude API key and other config
24
+ const config = await promptForConfiguration(authResult.botToken, authResult.guildId);
25
+ // Write .env file
26
+ const spinner = ora('Creating .env file...').start();
27
+ writeEnvFile(envPath, config);
28
+ spinner.succeed(chalk.green('.env file created successfully!'));
29
+ console.log(chalk.gray(`\nšŸ“„ Configuration saved to: ${envPath}\n`));
30
+ }
31
+ // Load environment variables
32
+ dotenv.config({ path: envPath });
33
+ // Validate configuration
34
+ const spinner = ora('Validating configuration...').start();
35
+ const validation = validateConfig();
36
+ if (!validation.valid) {
37
+ spinner.fail(chalk.red('Configuration validation failed'));
38
+ console.log(chalk.red('\nāŒ Missing required environment variables:'));
39
+ validation.missing.forEach(key => {
40
+ console.log(chalk.red(` - ${key}`));
41
+ });
42
+ console.log(chalk.yellow(`\nPlease update ${envPath} with the missing values.\n`));
43
+ process.exit(1);
44
+ }
45
+ spinner.succeed(chalk.green('Configuration valid'));
46
+ // Start the bot
47
+ console.log(chalk.cyan('\nšŸš€ Starting Cordbot...\n'));
48
+ await startBot(cwd);
49
+ }
50
+ async function promptForConfiguration(botToken, guildId) {
51
+ console.log(chalk.cyan('\nšŸ“ Please provide your Claude API key:\n'));
52
+ const answers = await inquirer.prompt([
53
+ {
54
+ type: 'password',
55
+ name: 'ANTHROPIC_API_KEY',
56
+ message: 'Anthropic API Key:',
57
+ mask: '*',
58
+ validate: (input) => {
59
+ if (!input || input.trim().length === 0) {
60
+ return 'Anthropic API Key is required';
61
+ }
62
+ if (!input.startsWith('sk-ant-')) {
63
+ return 'Anthropic API Key should start with "sk-ant-"';
64
+ }
65
+ return true;
66
+ },
67
+ },
68
+ {
69
+ type: 'list',
70
+ name: 'LOG_LEVEL',
71
+ message: 'Log Level:',
72
+ choices: ['info', 'debug', 'warn', 'error'],
73
+ default: 'info',
74
+ },
75
+ {
76
+ type: 'input',
77
+ name: 'ARCHIVE_AFTER_DAYS',
78
+ message: 'Archive inactive sessions after (days):',
79
+ default: '30',
80
+ validate: (input) => {
81
+ const num = parseInt(input);
82
+ if (isNaN(num) || num < 1) {
83
+ return 'Must be a positive number';
84
+ }
85
+ return true;
86
+ },
87
+ },
88
+ ]);
89
+ return {
90
+ DISCORD_BOT_TOKEN: botToken,
91
+ DISCORD_GUILD_ID: guildId,
92
+ ...answers,
93
+ };
94
+ }
95
+ function writeEnvFile(envPath, config) {
96
+ const lines = [
97
+ '# Cordbot Configuration',
98
+ '# Generated by cordbot agent',
99
+ '',
100
+ '# Discord Configuration',
101
+ `DISCORD_BOT_TOKEN=${config.DISCORD_BOT_TOKEN}`,
102
+ `DISCORD_GUILD_ID=${config.DISCORD_GUILD_ID}`,
103
+ '',
104
+ '# Anthropic Configuration',
105
+ `ANTHROPIC_API_KEY=${config.ANTHROPIC_API_KEY}`,
106
+ '',
107
+ '# Optional Settings',
108
+ `LOG_LEVEL=${config.LOG_LEVEL || 'info'}`,
109
+ `ARCHIVE_AFTER_DAYS=${config.ARCHIVE_AFTER_DAYS || '30'}`,
110
+ '',
111
+ '# Security Note:',
112
+ '# This file contains sensitive credentials. Never commit it to git!',
113
+ '# It should be in your .gitignore file.',
114
+ ];
115
+ fs.writeFileSync(envPath, lines.join('\n'), 'utf-8');
116
+ }
117
+ function validateConfig() {
118
+ const required = [
119
+ 'DISCORD_BOT_TOKEN',
120
+ 'DISCORD_GUILD_ID',
121
+ 'ANTHROPIC_API_KEY',
122
+ ];
123
+ const missing = required.filter(key => !process.env[key]);
124
+ return {
125
+ valid: missing.length === 0,
126
+ missing,
127
+ };
128
+ }
@@ -0,0 +1,24 @@
1
+ import { Client, GatewayIntentBits } from 'discord.js';
2
+ export async function createDiscordClient(config) {
3
+ const client = new Client({
4
+ intents: [
5
+ GatewayIntentBits.Guilds,
6
+ GatewayIntentBits.GuildMessages,
7
+ GatewayIntentBits.MessageContent,
8
+ ],
9
+ });
10
+ // Wait for client to be ready
11
+ await new Promise((resolve, reject) => {
12
+ client.once('clientReady', () => {
13
+ console.log(`āœ… Discord bot logged in as ${client.user?.tag}`);
14
+ resolve();
15
+ });
16
+ client.once('error', (error) => {
17
+ console.error('āŒ Discord client error:', error);
18
+ reject(error);
19
+ });
20
+ // Connect to Discord
21
+ client.login(config.token).catch(reject);
22
+ });
23
+ return client;
24
+ }