cord-bot 1.0.2 → 1.0.5

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/bin/cord.ts CHANGED
@@ -2,22 +2,57 @@
2
2
  /**
3
3
  * Cord CLI - Manage your Discord-Claude bridge
4
4
  *
5
- * Commands:
5
+ * Management Commands:
6
6
  * cord start - Start bot and worker
7
7
  * cord stop - Stop all processes
8
8
  * cord status - Show running status
9
- * cord logs - Show combined logs
10
9
  * cord setup - Interactive setup wizard
10
+ *
11
+ * Discord Commands:
12
+ * cord send <channel> "message"
13
+ * cord embed <channel> "description" [--title, --color, --field, etc.]
14
+ * cord file <channel> <filepath> "message"
15
+ * cord buttons <channel> "prompt" --button label="..." id="..." [style, reply, webhook]
16
+ * cord typing <channel>
17
+ * cord edit <channel> <messageId> "content"
18
+ * cord delete <channel> <messageId>
19
+ * cord rename <threadId> "name"
20
+ * cord reply <channel> <messageId> "message"
21
+ * cord thread <channel> <messageId> "name"
22
+ * cord react <channel> <messageId> "emoji"
11
23
  */
12
24
 
13
25
  import { spawn, spawnSync } from 'bun';
14
- import { existsSync, readFileSync, writeFileSync } from 'fs';
15
- import { join } from 'path';
26
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'fs';
27
+ import { join, dirname } from 'path';
16
28
  import * as readline from 'readline';
29
+ import { homedir } from 'os';
17
30
 
18
31
  const PID_FILE = join(process.cwd(), '.cord.pid');
32
+ const API_BASE = process.env.CORD_API_URL || 'http://localhost:2643';
19
33
 
20
34
  const command = process.argv[2];
35
+ const args = process.argv.slice(3);
36
+
37
+ // Color name to Discord color int mapping
38
+ const COLORS: Record<string, number> = {
39
+ red: 15158332, // 0xE74C3C
40
+ green: 3066993, // 0x2ECC71
41
+ blue: 3447003, // 0x3498DB
42
+ yellow: 16776960, // 0xFFFF00
43
+ purple: 10181046, // 0x9B59B6
44
+ orange: 15105570, // 0xE67E22
45
+ gray: 9807270, // 0x95A5A6
46
+ grey: 9807270, // 0x95A5A6
47
+ };
48
+
49
+ // Button style name to Discord style int mapping
50
+ const BUTTON_STYLES: Record<string, number> = {
51
+ primary: 1,
52
+ secondary: 2,
53
+ success: 3,
54
+ danger: 4,
55
+ };
21
56
 
22
57
  async function prompt(question: string): Promise<string> {
23
58
  const rl = readline.createInterface({
@@ -32,6 +67,316 @@ async function prompt(question: string): Promise<string> {
32
67
  });
33
68
  }
34
69
 
70
+ // ============ API Helper ============
71
+
72
+ async function apiCall(endpoint: string, body: any): Promise<any> {
73
+ try {
74
+ const response = await fetch(`${API_BASE}${endpoint}`, {
75
+ method: 'POST',
76
+ headers: { 'Content-Type': 'application/json' },
77
+ body: JSON.stringify(body),
78
+ });
79
+ const data = await response.json();
80
+ if (!response.ok || data.error) {
81
+ console.error('Error:', data.error || 'Request failed');
82
+ process.exit(1);
83
+ }
84
+ return data;
85
+ } catch (error: any) {
86
+ if (error.code === 'ECONNREFUSED') {
87
+ console.error('Error: Cannot connect to Cord API. Is the bot running? (cord start)');
88
+ } else {
89
+ console.error('Error:', error.message);
90
+ }
91
+ process.exit(1);
92
+ }
93
+ }
94
+
95
+ // ============ Discord Commands ============
96
+
97
+ async function sendMessage() {
98
+ const channel = args[0];
99
+ const message = args[1];
100
+ if (!channel || !message) {
101
+ console.error('Usage: cord send <channel> "message"');
102
+ process.exit(1);
103
+ }
104
+ const result = await apiCall('/command', {
105
+ command: 'send-to-thread',
106
+ args: { thread: channel, message },
107
+ });
108
+ console.log(`Sent message: ${result.messageId}`);
109
+ }
110
+
111
+ async function sendEmbed() {
112
+ const channel = args[0];
113
+ if (!channel) {
114
+ console.error('Usage: cord embed <channel> "description" [--title "..." --color green ...]');
115
+ process.exit(1);
116
+ }
117
+
118
+ // Parse flags and find positional description
119
+ const embed: any = {};
120
+ const fields: any[] = [];
121
+ let description = '';
122
+ let i = 1;
123
+
124
+ while (i < args.length) {
125
+ const arg = args[i];
126
+ if (arg === '--title' && args[i + 1]) {
127
+ embed.title = args[++i];
128
+ } else if (arg === '--url' && args[i + 1]) {
129
+ embed.url = args[++i];
130
+ } else if (arg === '--color' && args[i + 1]) {
131
+ const colorArg = args[++i].toLowerCase();
132
+ embed.color = COLORS[colorArg] || parseInt(colorArg.replace('0x', ''), 16) || 0;
133
+ } else if (arg === '--author' && args[i + 1]) {
134
+ embed.author = embed.author || {};
135
+ embed.author.name = args[++i];
136
+ } else if (arg === '--author-url' && args[i + 1]) {
137
+ embed.author = embed.author || {};
138
+ embed.author.url = args[++i];
139
+ } else if (arg === '--author-icon' && args[i + 1]) {
140
+ embed.author = embed.author || {};
141
+ embed.author.icon_url = args[++i];
142
+ } else if (arg === '--thumbnail' && args[i + 1]) {
143
+ embed.thumbnail = { url: args[++i] };
144
+ } else if (arg === '--image' && args[i + 1]) {
145
+ embed.image = { url: args[++i] };
146
+ } else if (arg === '--footer' && args[i + 1]) {
147
+ embed.footer = embed.footer || {};
148
+ embed.footer.text = args[++i];
149
+ } else if (arg === '--footer-icon' && args[i + 1]) {
150
+ embed.footer = embed.footer || {};
151
+ embed.footer.icon_url = args[++i];
152
+ } else if (arg === '--timestamp') {
153
+ embed.timestamp = new Date().toISOString();
154
+ } else if (arg === '--field' && args[i + 1]) {
155
+ const fieldStr = args[++i];
156
+ const parts = fieldStr.split(':');
157
+ if (parts.length >= 2) {
158
+ fields.push({
159
+ name: parts[0],
160
+ value: parts[1],
161
+ inline: parts[2]?.toLowerCase() === 'inline',
162
+ });
163
+ }
164
+ } else if (!arg.startsWith('--')) {
165
+ description = arg;
166
+ }
167
+ i++;
168
+ }
169
+
170
+ if (description) embed.description = description;
171
+ if (fields.length > 0) embed.fields = fields;
172
+
173
+ const result = await apiCall('/command', {
174
+ command: 'send-to-thread',
175
+ args: { thread: channel, embeds: [embed] },
176
+ });
177
+ console.log(`Sent embed: ${result.messageId}`);
178
+ }
179
+
180
+ async function sendFile() {
181
+ const channel = args[0];
182
+ const filepath = args[1];
183
+ const message = args[2] || '';
184
+
185
+ if (!channel || !filepath) {
186
+ console.error('Usage: cord file <channel> <filepath> ["message"]');
187
+ process.exit(1);
188
+ }
189
+
190
+ if (!existsSync(filepath)) {
191
+ console.error(`Error: File not found: ${filepath}`);
192
+ process.exit(1);
193
+ }
194
+
195
+ const fileContent = readFileSync(filepath, 'utf-8');
196
+ const fileName = filepath.split('/').pop() || 'file.txt';
197
+
198
+ const result = await apiCall('/send-with-file', {
199
+ channelId: channel,
200
+ fileName,
201
+ fileContent,
202
+ content: message,
203
+ });
204
+ console.log(`Sent file: ${result.messageId}`);
205
+ }
206
+
207
+ async function sendButtons() {
208
+ const channel = args[0];
209
+ if (!channel) {
210
+ console.error('Usage: cord buttons <channel> "prompt" --button label="..." id="..." [style="success"] [reply="..."] [webhook="..."]');
211
+ process.exit(1);
212
+ }
213
+
214
+ let promptText = '';
215
+ const buttons: any[] = [];
216
+ let i = 1;
217
+
218
+ while (i < args.length) {
219
+ const arg = args[i];
220
+ if (arg === '--button') {
221
+ // Collect all following key=value pairs until next flag or end
222
+ const button: any = {};
223
+ i++;
224
+ while (i < args.length && !args[i].startsWith('--')) {
225
+ const kvMatch = args[i].match(/^(\w+)=(.*)$/);
226
+ if (kvMatch) {
227
+ const [, key, value] = kvMatch;
228
+ if (key === 'style') {
229
+ button.style = BUTTON_STYLES[value.toLowerCase()] || 1;
230
+ } else {
231
+ button[key] = value;
232
+ }
233
+ }
234
+ i++;
235
+ }
236
+ if (button.label && button.id) {
237
+ // Convert to API format
238
+ const apiButton: any = {
239
+ label: button.label,
240
+ customId: button.id,
241
+ style: button.style || 1,
242
+ };
243
+ if (button.reply || button.webhook) {
244
+ apiButton.handler = {};
245
+ if (button.reply) {
246
+ apiButton.handler.type = 'inline';
247
+ apiButton.handler.content = button.reply;
248
+ apiButton.handler.ephemeral = true;
249
+ }
250
+ if (button.webhook) {
251
+ apiButton.handler.type = button.reply ? 'inline' : 'webhook';
252
+ apiButton.handler.webhookUrl = button.webhook;
253
+ }
254
+ }
255
+ buttons.push(apiButton);
256
+ }
257
+ continue; // Don't increment i again
258
+ } else if (!arg.startsWith('--')) {
259
+ promptText = arg;
260
+ }
261
+ i++;
262
+ }
263
+
264
+ if (buttons.length === 0) {
265
+ console.error('Error: At least one --button is required');
266
+ process.exit(1);
267
+ }
268
+
269
+ const result = await apiCall('/send-with-buttons', {
270
+ channelId: channel,
271
+ content: promptText,
272
+ buttons,
273
+ });
274
+ console.log(`Sent buttons: ${result.messageId}`);
275
+ }
276
+
277
+ async function startTyping() {
278
+ const channel = args[0];
279
+ if (!channel) {
280
+ console.error('Usage: cord typing <channel>');
281
+ process.exit(1);
282
+ }
283
+ await apiCall('/command', {
284
+ command: 'start-typing',
285
+ args: { channel },
286
+ });
287
+ console.log('Typing indicator sent');
288
+ }
289
+
290
+ async function editMessage() {
291
+ const channel = args[0];
292
+ const messageId = args[1];
293
+ const content = args[2];
294
+ if (!channel || !messageId || !content) {
295
+ console.error('Usage: cord edit <channel> <messageId> "new content"');
296
+ process.exit(1);
297
+ }
298
+ await apiCall('/command', {
299
+ command: 'edit-message',
300
+ args: { channel, message: messageId, content },
301
+ });
302
+ console.log(`Edited message: ${messageId}`);
303
+ }
304
+
305
+ async function deleteMessage() {
306
+ const channel = args[0];
307
+ const messageId = args[1];
308
+ if (!channel || !messageId) {
309
+ console.error('Usage: cord delete <channel> <messageId>');
310
+ process.exit(1);
311
+ }
312
+ await apiCall('/command', {
313
+ command: 'delete-message',
314
+ args: { channel, message: messageId },
315
+ });
316
+ console.log(`Deleted message: ${messageId}`);
317
+ }
318
+
319
+ async function renameThread() {
320
+ const threadId = args[0];
321
+ const name = args[1];
322
+ if (!threadId || !name) {
323
+ console.error('Usage: cord rename <threadId> "new name"');
324
+ process.exit(1);
325
+ }
326
+ await apiCall('/command', {
327
+ command: 'rename-thread',
328
+ args: { thread: threadId, name },
329
+ });
330
+ console.log(`Renamed thread: ${threadId}`);
331
+ }
332
+
333
+ async function replyToMessage() {
334
+ const channel = args[0];
335
+ const messageId = args[1];
336
+ const message = args[2];
337
+ if (!channel || !messageId || !message) {
338
+ console.error('Usage: cord reply <channel> <messageId> "message"');
339
+ process.exit(1);
340
+ }
341
+ const result = await apiCall('/command', {
342
+ command: 'reply-to-message',
343
+ args: { channel, message: messageId, content: message },
344
+ });
345
+ console.log(`Replied to message: ${result.messageId}`);
346
+ }
347
+
348
+ async function createThread() {
349
+ const channel = args[0];
350
+ const messageId = args[1];
351
+ const name = args[2];
352
+ if (!channel || !messageId || !name) {
353
+ console.error('Usage: cord thread <channel> <messageId> "thread name"');
354
+ process.exit(1);
355
+ }
356
+ const result = await apiCall('/command', {
357
+ command: 'create-thread',
358
+ args: { channel, message: messageId, name },
359
+ });
360
+ console.log(`Created thread: ${result.threadId}`);
361
+ }
362
+
363
+ async function addReaction() {
364
+ const channel = args[0];
365
+ const messageId = args[1];
366
+ const emoji = args[2];
367
+ if (!channel || !messageId || !emoji) {
368
+ console.error('Usage: cord react <channel> <messageId> "emoji"');
369
+ process.exit(1);
370
+ }
371
+ await apiCall('/command', {
372
+ command: 'add-reaction',
373
+ args: { channel, message: messageId, emoji },
374
+ });
375
+ console.log(`Added reaction: ${emoji}`);
376
+ }
377
+
378
+ // ============ Management Commands ============
379
+
35
380
  async function setup() {
36
381
  console.log('\n🔌 Cord Setup\n');
37
382
 
@@ -76,6 +421,23 @@ async function setup() {
76
421
  console.log('⚠ Claude CLI not found. Install from: https://claude.ai/code');
77
422
  }
78
423
 
424
+ // Install Claude Code skill
425
+ const skillsDir = join(homedir(), '.claude', 'skills', 'cord');
426
+ const cordRoot = join(dirname(import.meta.dir));
427
+ const sourceSkillsDir = join(cordRoot, 'skills', 'cord');
428
+
429
+ if (existsSync(sourceSkillsDir)) {
430
+ console.log('\n📚 Claude Code Skill');
431
+ console.log(' Teaches your assistant how to send Discord messages, embeds,');
432
+ console.log(' files, and interactive buttons.');
433
+ const installSkill = await prompt('Install skill? (Y/n): ');
434
+ if (installSkill.toLowerCase() !== 'n') {
435
+ mkdirSync(skillsDir, { recursive: true });
436
+ cpSync(sourceSkillsDir, skillsDir, { recursive: true });
437
+ console.log(`✓ Skill installed to ${skillsDir}`);
438
+ }
439
+ }
440
+
79
441
  console.log('\n✨ Setup complete! Run: cord start\n');
80
442
  }
81
443
 
@@ -180,25 +542,79 @@ function showHelp() {
180
542
  console.log(`
181
543
  Cord - Discord to Claude Code bridge
182
544
 
183
- Usage: cord <command>
184
-
185
- Commands:
186
- start Start bot and worker
187
- stop Stop all processes
188
- status Show running status
189
- setup Interactive setup wizard
190
- help Show this help
545
+ Usage: cord <command> [options]
546
+
547
+ Management Commands:
548
+ start Start bot and worker
549
+ stop Stop all processes
550
+ status Show running status
551
+ setup Interactive setup wizard
552
+ help Show this help
553
+
554
+ Discord Commands:
555
+ send <channel> "message"
556
+ Send a text message
557
+
558
+ embed <channel> "description" [options]
559
+ Send an embed with optional formatting
560
+ --title "..." Embed title
561
+ --url "..." Title link URL
562
+ --color <name|hex> red, green, blue, yellow, purple, orange, or 0xHEX
563
+ --author "..." Author name
564
+ --author-url "..." Author link
565
+ --author-icon "..." Author icon URL
566
+ --thumbnail "..." Small image (top right)
567
+ --image "..." Large image (bottom)
568
+ --footer "..." Footer text
569
+ --footer-icon "..." Footer icon URL
570
+ --timestamp Add current timestamp
571
+ --field "Name:Value" Add field (use :inline for inline)
572
+
573
+ file <channel> <filepath> ["message"]
574
+ Send a file attachment
575
+
576
+ buttons <channel> "prompt" --button label="..." id="..." [options]
577
+ Send interactive buttons
578
+ Button options:
579
+ label="..." Button text (required)
580
+ id="..." Custom ID (required)
581
+ style="..." primary, secondary, success, danger
582
+ reply="..." Ephemeral reply when clicked
583
+ webhook="..." URL to POST click data to
584
+
585
+ typing <channel>
586
+ Show typing indicator
587
+
588
+ edit <channel> <messageId> "content"
589
+ Edit an existing message
590
+
591
+ delete <channel> <messageId>
592
+ Delete a message
593
+
594
+ rename <threadId> "name"
595
+ Rename a thread
596
+
597
+ reply <channel> <messageId> "message"
598
+ Reply to a specific message
599
+
600
+ thread <channel> <messageId> "name"
601
+ Create a thread from a message
602
+
603
+ react <channel> <messageId> "emoji"
604
+ Add a reaction to a message
191
605
 
192
606
  Examples:
193
- cord setup # First-time configuration
194
- cord start # Start the bot
195
- cord status # Check if running
196
- cord stop # Stop everything
607
+ cord send 123456789 "Hello world!"
608
+ cord embed 123456789 "Status update" --title "Daily Report" --color green --field "Tasks:5 done:inline"
609
+ cord buttons 123456789 "Approve?" --button label="Yes" id="approve" style="success" reply="Approved!"
610
+ cord file 123456789 ./report.md "Here's the report"
197
611
  `);
198
612
  }
199
613
 
200
- // Main
614
+ // ============ Main ============
615
+
201
616
  switch (command) {
617
+ // Management
202
618
  case 'start':
203
619
  start();
204
620
  break;
@@ -217,6 +633,42 @@ switch (command) {
217
633
  case undefined:
218
634
  showHelp();
219
635
  break;
636
+
637
+ // Discord commands
638
+ case 'send':
639
+ sendMessage();
640
+ break;
641
+ case 'embed':
642
+ sendEmbed();
643
+ break;
644
+ case 'file':
645
+ sendFile();
646
+ break;
647
+ case 'buttons':
648
+ sendButtons();
649
+ break;
650
+ case 'typing':
651
+ startTyping();
652
+ break;
653
+ case 'edit':
654
+ editMessage();
655
+ break;
656
+ case 'delete':
657
+ deleteMessage();
658
+ break;
659
+ case 'rename':
660
+ renameThread();
661
+ break;
662
+ case 'reply':
663
+ replyToMessage();
664
+ break;
665
+ case 'thread':
666
+ createThread();
667
+ break;
668
+ case 'react':
669
+ addReaction();
670
+ break;
671
+
220
672
  default:
221
673
  console.log(`Unknown command: ${command}`);
222
674
  showHelp();
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "cord-bot",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
+ "description": "Discord bot that bridges messages to Claude Code sessions",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/alexknowshtml/cord"
8
+ },
4
9
  "module": "index.ts",
5
10
  "type": "module",
6
11
  "bin": {