@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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/bin/cordbot.js +10 -0
- package/dist/agent/manager.js +256 -0
- package/dist/agent/stream.js +359 -0
- package/dist/auth.js +161 -0
- package/dist/cli.js +128 -0
- package/dist/discord/client.js +24 -0
- package/dist/discord/events.js +283 -0
- package/dist/discord/sync.js +148 -0
- package/dist/index.js +72 -0
- package/dist/init.js +82 -0
- package/dist/permissions/discord.js +66 -0
- package/dist/scheduler/parser.js +62 -0
- package/dist/scheduler/runner.js +201 -0
- package/dist/service/config.js +2 -0
- package/dist/service/manifest.js +27 -0
- package/dist/service/token-manager.js +143 -0
- package/dist/service/types.js +1 -0
- package/dist/storage/database.js +122 -0
- package/dist/tools/builtin-loader.js +21 -0
- package/dist/tools/cron/add_job.js +96 -0
- package/dist/tools/cron/list_jobs.js +55 -0
- package/dist/tools/cron/remove_job.js +64 -0
- package/dist/tools/cron/update_job.js +97 -0
- package/dist/tools/gmail/list_messages.js +87 -0
- package/dist/tools/gmail/send_email.js +118 -0
- package/dist/tools/loader.js +84 -0
- package/dist/tools/share_file.js +61 -0
- package/package.json +65 -0
- package/templates/.claude-cron.template +3 -0
- package/templates/channel-CLAUDE.md.template +26 -0
- package/templates/cron-skill.md +63 -0
- package/templates/root-CLAUDE.md.template +78 -0
|
@@ -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
|
+
}
|