cord-bot 1.0.4 → 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.
@@ -20,15 +20,13 @@ jobs:
20
20
  - name: Install dependencies
21
21
  run: bun install
22
22
 
23
- - name: Build
24
- run: bun run build
25
-
26
23
  - uses: actions/setup-node@v4
27
24
  with:
28
- node-version: '20'
25
+ node-version: '22'
29
26
  registry-url: 'https://registry.npmjs.org'
30
27
 
31
- - name: Publish to npm
28
+ - name: Update npm for OIDC support
29
+ run: npm install -g npm@latest
30
+
31
+ - name: Publish to npm with provenance
32
32
  run: npm publish --provenance --access public
33
- env:
34
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -1,37 +1,42 @@
1
1
  # Cord
2
2
 
3
- A simple bridge that connects Discord to Claude Code CLI.
3
+ A Discord bot that connects to Claude Code CLI, plus a Claude Code skill that lets your assistant control the bot.
4
4
 
5
5
  > **cord** /kôrd/ — a connection between two things.
6
6
 
7
7
  [![npm version](https://badge.fury.io/js/cord-bot.svg)](https://www.npmjs.com/package/cord-bot)
8
8
 
9
- ## Install
9
+ ## What You Get
10
10
 
11
- ```bash
12
- npm install cord-bot
13
- # or
14
- bunx cord-bot
15
- ```
11
+ **Discord Bot** — @mention the bot in Discord, it spawns Claude Code to respond. Conversations happen in threads with full context preserved across messages.
16
12
 
17
- When someone @mentions your bot, it:
18
- 1. Creates a thread for the conversation
19
- 2. Queues the message for Claude processing
20
- 3. Posts Claude's response back to the thread
21
- 4. Remembers context for follow-up messages
13
+ **Claude Code Skill** — Teaches your assistant to use the bot to send messages, embeds, files, and interactive buttons. Installed automatically during setup. No MCP server needed.
22
14
 
23
- ## Architecture
15
+ ## Quick Start
24
16
 
25
- ```
26
- Discord Bot → BullMQ Queue → Claude Spawner
27
- (Node.js) (Redis) (Bun)
17
+ ```bash
18
+ # Create a new Cord project
19
+ bunx create-cord my-bot
20
+ cd my-bot
21
+
22
+ # Start Redis (if not already running)
23
+ redis-server &
24
+
25
+ # Start Cord
26
+ cord start
28
27
  ```
29
28
 
30
- - **Bot** (`src/bot.ts`): Catches @mentions, creates threads, sends to queue
31
- - **Queue** (`src/queue.ts`): BullMQ job queue for reliable processing
32
- - **Worker** (`src/worker.ts`): Pulls jobs, spawns Claude, posts responses
33
- - **Spawner** (`src/spawner.ts`): The Claude CLI integration (the core)
34
- - **DB** (`src/db.ts`): SQLite for thread→session mapping
29
+ <details>
30
+ <summary>Alternative: Clone from GitHub</summary>
31
+
32
+ ```bash
33
+ git clone https://github.com/alexknowshtml/cord.git my-bot
34
+ cd my-bot
35
+ bun install
36
+ cord setup
37
+ cord start
38
+ ```
39
+ </details>
35
40
 
36
41
  ## Prerequisites
37
42
 
@@ -55,24 +60,19 @@ Discord Bot → BullMQ Queue → Claude Spawner
55
60
 
56
61
  **Note:** This runs 100% locally. The bot connects *outbound* to Discord's gateway - no need to expose ports or use ngrok.
57
62
 
58
- ## Quick Start
59
-
60
- ```bash
61
- # Clone and install
62
- git clone https://github.com/alexknowshtml/cord.git
63
- cd cord
64
- bun install
65
-
66
- # Run setup wizard (configures .env, checks dependencies, installs skill)
67
- cord setup
68
-
69
- # Start Redis (if not already running)
70
- redis-server &
63
+ ## Architecture
71
64
 
72
- # Start Cord
73
- cord start
65
+ ```
66
+ Discord Bot → BullMQ Queue → Claude Spawner
67
+ (Node.js) (Redis) (Bun)
74
68
  ```
75
69
 
70
+ - **Bot** (`src/bot.ts`): Catches @mentions, creates threads, sends to queue
71
+ - **Queue** (`src/queue.ts`): BullMQ job queue for reliable processing
72
+ - **Worker** (`src/worker.ts`): Pulls jobs, spawns Claude, posts responses
73
+ - **Spawner** (`src/spawner.ts`): The Claude CLI integration (the core)
74
+ - **DB** (`src/db.ts`): SQLite for thread→session mapping
75
+
76
76
  ## Environment Variables
77
77
 
78
78
  | Variable | Required | Default | Description |
@@ -83,67 +83,31 @@ cord start
83
83
  | `CLAUDE_WORKING_DIR` | No | `cwd` | Working directory for Claude |
84
84
  | `DB_PATH` | No | `./data/threads.db` | SQLite database path |
85
85
 
86
- ## How It Works
87
-
88
- ### New Mentions
89
-
90
- 1. User @mentions the bot with a question
91
- 2. Bot creates a thread from the message
92
- 3. Bot generates a UUID session ID
93
- 4. Bot stores thread_id → session_id in SQLite
94
- 5. Bot queues a job with the prompt and session ID
95
- 6. Worker picks up the job
96
- 7. Worker spawns Claude with `--session-id UUID`
97
- 8. Worker posts Claude's response to the thread
86
+ ## CLI Commands
98
87
 
99
- ### Follow-up Messages
88
+ Once running, Cord provides CLI commands for interacting with Discord:
100
89
 
101
- 1. User sends another message in the thread
102
- 2. Bot looks up the session ID from SQLite
103
- 3. Bot queues a job with `resume: true`
104
- 4. Worker spawns Claude with `--resume UUID`
105
- 5. Claude has full context from previous messages
106
-
107
- ## Key CLI Flags
108
-
109
- The magic is in `src/spawner.ts`:
110
-
111
- ```typescript
112
- // For new sessions:
113
- claude --print --session-id UUID -p "prompt"
114
-
115
- // For follow-ups:
116
- claude --print --resume UUID -p "prompt"
117
-
118
- // Inject context that survives compaction:
119
- claude --append-system-prompt "Current time: ..."
90
+ ```bash
91
+ cord send <channel> "message" # Send a message
92
+ cord embed <channel> "text" --title "T" # Send formatted embed
93
+ cord file <channel> ./report.md # Send file attachment
94
+ cord buttons <channel> "Pick:" --button label="Yes" id="yes" style="success"
95
+ cord typing <channel> # Show typing indicator
96
+ cord edit <channel> <msgId> "new text" # Edit message
97
+ cord delete <channel> <msgId> # Delete message
98
+ cord reply <channel> <msgId> "reply" # Reply to message
99
+ cord react <channel> <msgId> "emoji" # Add reaction
100
+ cord thread <channel> <msgId> "name" # Create thread
101
+ cord rename <threadId> "new name" # Rename thread
120
102
  ```
121
103
 
122
- ## HTTP API
123
-
124
- Cord exposes an HTTP API on port 2643 for external tools to interact with Discord:
104
+ See [skills/cord/SKILL.md](./skills/cord/SKILL.md) for full CLI documentation.
125
105
 
126
- - **Send messages** - Text, embeds, file attachments
127
- - **Interactive buttons** - With inline or webhook handlers
128
- - **Typing indicators** - Show typing before slow operations
129
- - **Edit/delete messages** - Modify existing messages
130
- - **Rename threads** - Update thread names
131
-
132
- See [skills/cord/PRIMITIVES.md](./skills/cord/PRIMITIVES.md) for full API documentation.
133
-
134
- ## Claude Code Skill
135
-
136
- Cord includes a Claude Code skill that teaches your assistant how to send Discord messages, embeds, files, and interactive buttons.
137
-
138
- ```bash
139
- # Installed automatically during setup
140
- cord setup
106
+ ## HTTP API
141
107
 
142
- # Or copy manually
143
- cp -r skills/cord ~/.claude/skills/
144
- ```
108
+ Cord also exposes an HTTP API on port 2643 for external scripts and webhooks.
145
109
 
146
- See [skills/cord/SKILL.md](./skills/cord/SKILL.md) for skill documentation.
110
+ See [skills/cord/HTTP-API.md](./skills/cord/HTTP-API.md) for API documentation.
147
111
 
148
112
  ## License
149
113
 
package/bin/cord.ts CHANGED
@@ -2,12 +2,24 @@
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';
@@ -17,8 +29,30 @@ import * as readline from 'readline';
17
29
  import { homedir } from 'os';
18
30
 
19
31
  const PID_FILE = join(process.cwd(), '.cord.pid');
32
+ const API_BASE = process.env.CORD_API_URL || 'http://localhost:2643';
20
33
 
21
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
+ };
22
56
 
23
57
  async function prompt(question: string): Promise<string> {
24
58
  const rl = readline.createInterface({
@@ -33,6 +67,316 @@ async function prompt(question: string): Promise<string> {
33
67
  });
34
68
  }
35
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
+
36
380
  async function setup() {
37
381
  console.log('\n🔌 Cord Setup\n');
38
382
 
@@ -198,25 +542,79 @@ function showHelp() {
198
542
  console.log(`
199
543
  Cord - Discord to Claude Code bridge
200
544
 
201
- Usage: cord <command>
202
-
203
- Commands:
204
- start Start bot and worker
205
- stop Stop all processes
206
- status Show running status
207
- setup Interactive setup wizard
208
- 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
209
605
 
210
606
  Examples:
211
- cord setup # First-time configuration
212
- cord start # Start the bot
213
- cord status # Check if running
214
- 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"
215
611
  `);
216
612
  }
217
613
 
218
- // Main
614
+ // ============ Main ============
615
+
219
616
  switch (command) {
617
+ // Management
220
618
  case 'start':
221
619
  start();
222
620
  break;
@@ -235,6 +633,42 @@ switch (command) {
235
633
  case undefined:
236
634
  showHelp();
237
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
+
238
672
  default:
239
673
  console.log(`Unknown command: ${command}`);
240
674
  showHelp();
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "cord-bot",
3
- "version": "1.0.4",
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": {
@@ -1,6 +1,6 @@
1
- # Discord Primitives
1
+ # HTTP API Reference
2
2
 
3
- HTTP API for interacting with Discord from scripts and Claude skills.
3
+ Low-level HTTP API for interacting with Discord. For Claude Code, use the CLI commands in [SKILL.md](./SKILL.md) instead.
4
4
 
5
5
  **Port:** `2643` (configurable via `API_PORT` env var)
6
6
 
@@ -201,6 +201,57 @@ curl -X POST http://localhost:2643/command \
201
201
  }'
202
202
  ```
203
203
 
204
+ ## reply-to-message
205
+
206
+ Reply to a specific message (shows reply preview).
207
+
208
+ ```bash
209
+ curl -X POST http://localhost:2643/command \
210
+ -H 'Content-Type: application/json' \
211
+ -d '{
212
+ "command": "reply-to-message",
213
+ "args": {
214
+ "channel": "CHANNEL_ID",
215
+ "message": "MESSAGE_ID",
216
+ "content": "This is a reply"
217
+ }
218
+ }'
219
+ ```
220
+
221
+ ## create-thread
222
+
223
+ Create a thread from a message.
224
+
225
+ ```bash
226
+ curl -X POST http://localhost:2643/command \
227
+ -H 'Content-Type: application/json' \
228
+ -d '{
229
+ "command": "create-thread",
230
+ "args": {
231
+ "channel": "CHANNEL_ID",
232
+ "message": "MESSAGE_ID",
233
+ "name": "Thread Name"
234
+ }
235
+ }'
236
+ ```
237
+
238
+ ## add-reaction
239
+
240
+ Add a reaction to a message.
241
+
242
+ ```bash
243
+ curl -X POST http://localhost:2643/command \
244
+ -H 'Content-Type: application/json' \
245
+ -d '{
246
+ "command": "add-reaction",
247
+ "args": {
248
+ "channel": "CHANNEL_ID",
249
+ "message": "MESSAGE_ID",
250
+ "emoji": "👍"
251
+ }
252
+ }'
253
+ ```
254
+
204
255
  ## Response Format
205
256
 
206
257
  All endpoints return JSON:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cord
3
- description: Send messages, files, embeds, and buttons to Discord via Cord's HTTP API. Use for notifications, reports, interactive choices, and dynamic Discord interactions.
3
+ description: Send messages, embeds, files, and interactive buttons to Discord via the Cord CLI. Use for notifications, reports, interactive choices, and dynamic Discord interactions.
4
4
  triggers:
5
5
  - "send to discord"
6
6
  - "post to discord"
@@ -10,270 +10,295 @@ triggers:
10
10
 
11
11
  # Cord - Discord Bridge Skill
12
12
 
13
- ## Overview
14
-
15
- Interact with Discord through Cord's HTTP API (default port 2643). This skill teaches Claude Code how to use the local Cord bot for Discord messaging, embeds, file attachments, and interactive buttons.
13
+ Interact with Discord through Cord's CLI commands. This skill teaches Claude Code how to send messages, embeds, files, and interactive buttons.
16
14
 
17
15
  **GitHub:** https://github.com/alexknowshtml/cord
18
16
 
19
- **API Reference:** [PRIMITIVES.md](./PRIMITIVES.md) - Full HTTP API documentation
20
-
21
17
  ## Setup
22
18
 
23
19
  Ensure Cord is running:
24
20
  ```bash
25
21
  cord start
26
- # or: bun run src/bot.ts
27
22
  ```
28
23
 
29
- ## Primitives
24
+ Verify it's connected:
25
+ ```bash
26
+ curl -s http://localhost:2643/health
27
+ # {"status":"ok","connected":true,"user":"MyBot#1234"}
28
+ ```
30
29
 
31
- ### send-message
30
+ ---
31
+
32
+ ## CLI Commands
32
33
 
33
- Send a simple text message to a channel or thread.
34
+ ### send
35
+
36
+ Send a text message to a channel or thread.
34
37
 
35
38
  ```bash
36
- curl -s -X POST http://localhost:2643/command \
37
- -H 'Content-Type: application/json' \
38
- -d '{
39
- "command": "send-to-thread",
40
- "args": {
41
- "thread": "CHANNEL_OR_THREAD_ID",
42
- "message": "Hello from Cord!"
43
- }
44
- }'
39
+ cord send <channel> "message"
45
40
  ```
46
41
 
47
- **Response:** `{"success":true,"messageId":"1234567890"}`
42
+ **Example:**
43
+ ```bash
44
+ cord send 123456789 "Hello world!"
45
+ ```
46
+
47
+ ---
48
48
 
49
- ### send-embed
49
+ ### embed
50
50
 
51
- Send a formatted embed card with optional fields.
51
+ Send a formatted embed card with optional styling.
52
52
 
53
53
  ```bash
54
- curl -s -X POST http://localhost:2643/command \
55
- -H 'Content-Type: application/json' \
56
- -d '{
57
- "command": "send-to-thread",
58
- "args": {
59
- "thread": "CHANNEL_OR_THREAD_ID",
60
- "embeds": [{
61
- "title": "Status Report",
62
- "description": "Daily summary",
63
- "color": 3447003,
64
- "fields": [
65
- {"name": "Tasks", "value": "5 completed", "inline": true},
66
- {"name": "Emails", "value": "12 processed", "inline": true}
67
- ],
68
- "footer": {"text": "Generated by Cord"}
69
- }]
70
- }
71
- }'
54
+ cord embed <channel> "description" [options]
72
55
  ```
73
56
 
74
- **Color reference:**
75
- - Success/Green: `3066993` (0x2ECC71)
76
- - Info/Blue: `3447003` (0x3498DB)
77
- - Warning/Yellow: `16776960` (0xFFFF00)
78
- - Error/Red: `15158332` (0xE74C3C)
79
- - Purple: `10181046` (0x9B59B6)
57
+ **Options:**
58
+ | Flag | Description |
59
+ |------|-------------|
60
+ | `--title "..."` | Embed title |
61
+ | `--url "..."` | Title link URL |
62
+ | `--color <name\|hex>` | red, green, blue, yellow, purple, orange, or 0xHEX |
63
+ | `--author "..."` | Author name |
64
+ | `--author-url "..."` | Author link |
65
+ | `--author-icon "..."` | Author icon URL |
66
+ | `--thumbnail "..."` | Small image (top right) |
67
+ | `--image "..."` | Large image (bottom) |
68
+ | `--footer "..."` | Footer text |
69
+ | `--footer-icon "..."` | Footer icon URL |
70
+ | `--timestamp` | Add current timestamp |
71
+ | `--field "Name:Value"` | Add field (append `:inline` for inline) |
72
+
73
+ **Examples:**
74
+
75
+ Simple embed:
76
+ ```bash
77
+ cord embed 123456789 "Daily status update" --title "Status Report" --color green
78
+ ```
80
79
 
81
- ### send-file
80
+ Embed with fields:
81
+ ```bash
82
+ cord embed 123456789 "Build completed successfully" \
83
+ --title "CI/CD Pipeline" \
84
+ --color green \
85
+ --field "Branch:main:inline" \
86
+ --field "Duration:2m 34s:inline" \
87
+ --field "Tests:142 passed" \
88
+ --footer "Deployed by Cord" \
89
+ --timestamp
90
+ ```
82
91
 
83
- Attach content as a file (good for long reports).
92
+ ---
84
93
 
94
+ ### file
95
+
96
+ Send a file attachment.
97
+
98
+ ```bash
99
+ cord file <channel> <filepath> ["message"]
100
+ ```
101
+
102
+ **Examples:**
85
103
  ```bash
86
- curl -s -X POST http://localhost:2643/send-with-file \
87
- -H 'Content-Type: application/json' \
88
- -d '{
89
- "channelId": "CHANNEL_OR_THREAD_ID",
90
- "fileName": "report.md",
91
- "fileContent": "# Report\n\nContent here...",
92
- "content": "Here is the detailed report"
93
- }'
104
+ cord file 123456789 ./report.md "Here's the weekly report"
105
+ cord file 123456789 ./logs.txt
94
106
  ```
95
107
 
96
- ### send-buttons
108
+ ---
109
+
110
+ ### buttons
97
111
 
98
- Send a message with interactive button choices.
112
+ Send interactive buttons with optional handlers.
99
113
 
100
114
  ```bash
101
- curl -s -X POST http://localhost:2643/send-with-buttons \
102
- -H 'Content-Type: application/json' \
103
- -d '{
104
- "channelId": "CHANNEL_OR_THREAD_ID",
105
- "content": "Choose an option:",
106
- "buttons": [
107
- {"label": "Approve", "customId": "approve-123", "style": "success"},
108
- {"label": "Reject", "customId": "reject-123", "style": "danger"}
109
- ]
110
- }'
115
+ cord buttons <channel> "prompt" --button label="..." id="..." [options]
111
116
  ```
112
117
 
113
- **Button styles:** `primary`, `secondary`, `success`, `danger`
118
+ **Button options:**
119
+ | Option | Description |
120
+ |--------|-------------|
121
+ | `label="..."` | Button text (required) |
122
+ | `id="..."` | Custom ID for tracking (required) |
123
+ | `style="..."` | primary, secondary, success, danger |
124
+ | `reply="..."` | Ephemeral reply when clicked |
125
+ | `webhook="..."` | URL to POST click data to |
126
+
127
+ **Examples:**
114
128
 
115
- ### send-buttons with inline handler
129
+ Simple confirmation:
130
+ ```bash
131
+ cord buttons 123456789 "Deploy to production?" \
132
+ --button label="Deploy" id="deploy-prod" style="success" \
133
+ --button label="Cancel" id="cancel-deploy" style="secondary"
134
+ ```
116
135
 
117
- Register a handler that fires when the button is clicked.
136
+ With inline responses:
137
+ ```bash
138
+ cord buttons 123456789 "Approve this PR?" \
139
+ --button label="Approve" id="approve" style="success" reply="Approved! Merging now." \
140
+ --button label="Reject" id="reject" style="danger" reply="Rejected. Please revise."
141
+ ```
118
142
 
143
+ With webhook callback:
119
144
  ```bash
120
- curl -s -X POST http://localhost:2643/command \
121
- -H 'Content-Type: application/json' \
122
- -d '{
123
- "command": "send-to-thread",
124
- "args": {
125
- "thread": "CHANNEL_OR_THREAD_ID",
126
- "message": "Click for details",
127
- "buttons": [{
128
- "label": "Show Details",
129
- "customId": "details-123",
130
- "style": "secondary",
131
- "handler": {
132
- "type": "inline",
133
- "content": "Here are the details...",
134
- "ephemeral": true
135
- }
136
- }]
137
- }
138
- }'
145
+ cord buttons 123456789 "Start backup?" \
146
+ --button label="Start Backup" id="backup-start" style="primary" webhook="http://localhost:8080/backup"
139
147
  ```
140
148
 
149
+ ---
150
+
141
151
  ### typing
142
152
 
143
153
  Show typing indicator (useful before slow operations).
144
154
 
145
155
  ```bash
146
- curl -s -X POST http://localhost:2643/command \
147
- -H 'Content-Type: application/json' \
148
- -d '{
149
- "command": "start-typing",
150
- "args": {"channel": "CHANNEL_OR_THREAD_ID"}
151
- }'
156
+ cord typing <channel>
152
157
  ```
153
158
 
154
- ### edit-message
159
+ ---
160
+
161
+ ### edit
155
162
 
156
163
  Edit an existing message.
157
164
 
158
165
  ```bash
159
- curl -s -X POST http://localhost:2643/command \
160
- -H 'Content-Type: application/json' \
161
- -d '{
162
- "command": "edit-message",
163
- "args": {
164
- "channel": "CHANNEL_ID",
165
- "message": "MESSAGE_ID",
166
- "content": "Updated content"
167
- }
168
- }'
166
+ cord edit <channel> <messageId> "new content"
169
167
  ```
170
168
 
171
- ### delete-message
169
+ ---
170
+
171
+ ### delete
172
172
 
173
173
  Delete a message.
174
174
 
175
175
  ```bash
176
- curl -s -X POST http://localhost:2643/command \
177
- -H 'Content-Type: application/json' \
178
- -d '{
179
- "command": "delete-message",
180
- "args": {
181
- "channel": "CHANNEL_ID",
182
- "message": "MESSAGE_ID"
183
- }
184
- }'
176
+ cord delete <channel> <messageId>
185
177
  ```
186
178
 
187
- ### rename-thread
179
+ ---
180
+
181
+ ### rename
188
182
 
189
183
  Rename a thread.
190
184
 
191
185
  ```bash
192
- curl -s -X POST http://localhost:2643/command \
193
- -H 'Content-Type: application/json' \
194
- -d '{
195
- "command": "rename-thread",
196
- "args": {
197
- "thread": "THREAD_ID",
198
- "name": "New Thread Name"
199
- }
200
- }'
186
+ cord rename <threadId> "new name"
201
187
  ```
202
188
 
203
- ## Usage Examples
189
+ ---
190
+
191
+ ### reply
204
192
 
205
- ### Post a notification
193
+ Reply to a specific message (shows reply preview).
206
194
 
207
195
  ```bash
208
- curl -s -X POST http://localhost:2643/command \
209
- -H 'Content-Type: application/json' \
210
- -d '{
211
- "command": "send-to-thread",
212
- "args": {
213
- "thread": "YOUR_CHANNEL_ID",
214
- "embeds": [{
215
- "title": "Deploy Complete",
216
- "description": "Production deployment finished successfully",
217
- "color": 3066993
218
- }]
219
- }
220
- }'
196
+ cord reply <channel> <messageId> "message"
221
197
  ```
222
198
 
223
- ### Send a file attachment
199
+ ---
200
+
201
+ ### thread
202
+
203
+ Create a thread from a message.
224
204
 
225
205
  ```bash
226
- curl -s -X POST http://localhost:2643/send-with-file \
227
- -H 'Content-Type: application/json' \
228
- -d '{
229
- "channelId": "YOUR_CHANNEL_ID",
230
- "fileName": "weekly-report.md",
231
- "fileContent": "# Weekly Report\n\nContent here...",
232
- "content": "Weekly report attached"
233
- }'
206
+ cord thread <channel> <messageId> "thread name"
234
207
  ```
235
208
 
236
- ### Ask for confirmation with buttons
209
+ ---
210
+
211
+ ### react
212
+
213
+ Add a reaction to a message.
237
214
 
238
215
  ```bash
239
- curl -s -X POST http://localhost:2643/send-with-buttons \
240
- -H 'Content-Type: application/json' \
241
- -d '{
242
- "channelId": "YOUR_CHANNEL_ID",
243
- "content": "Ready to proceed?",
244
- "buttons": [
245
- {"label": "Yes", "customId": "confirm-action", "style": "success"},
246
- {"label": "No", "customId": "cancel-action", "style": "secondary"}
247
- ]
248
- }'
216
+ cord react <channel> <messageId> "emoji"
249
217
  ```
250
218
 
251
- ## Health Check
219
+ **Example:**
220
+ ```bash
221
+ cord react 123456789 987654321 "👍"
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Choosing the Right Command
227
+
228
+ | Use Case | Command |
229
+ |----------|---------|
230
+ | Simple notification | `cord send` |
231
+ | Formatted status update | `cord embed` |
232
+ | Long content (logs, reports) | `cord file` |
233
+ | User needs to make a choice | `cord buttons` |
234
+ | Indicate processing | `cord typing` |
235
+ | Update previous message | `cord edit` |
236
+ | Start a focused discussion | `cord thread` |
237
+ | Quick acknowledgment | `cord react` |
238
+
239
+ ---
240
+
241
+ ## Assembly Patterns
252
242
 
253
- Verify Cord is running:
243
+ ### Notification with follow-up options
254
244
 
255
245
  ```bash
256
- curl -s http://localhost:2643/health
257
- # {"status":"ok","connected":true,"user":"YourBot#1234"}
246
+ # Send the notification
247
+ cord embed 123456789 "Build failed on main branch" \
248
+ --title "CI Alert" \
249
+ --color red \
250
+ --field "Error:Test suite timeout" \
251
+ --field "Commit:abc1234:inline"
252
+
253
+ # Offer actions
254
+ cord buttons 123456789 "What would you like to do?" \
255
+ --button label="View Logs" id="view-logs" style="primary" reply="Fetching logs..." \
256
+ --button label="Retry Build" id="retry" style="success" webhook="http://ci/retry" \
257
+ --button label="Ignore" id="ignore" style="secondary" reply="Acknowledged"
258
258
  ```
259
259
 
260
- ## Troubleshooting
260
+ ### Progress updates
261
261
 
262
- **"Connection refused"** - Cord bot not running
263
262
  ```bash
264
- cord status
265
- cord start
263
+ # Start with typing indicator
264
+ cord typing 123456789
265
+
266
+ # Send initial status
267
+ MSGID=$(cord send 123456789 "Processing... 0%" | grep -o '[0-9]*$')
268
+
269
+ # Update as progress continues
270
+ cord edit 123456789 $MSGID "Processing... 50%"
271
+ cord edit 123456789 $MSGID "Processing... 100% Complete!"
272
+
273
+ # Add completion reaction
274
+ cord react 123456789 $MSGID "✅"
266
275
  ```
267
276
 
268
- **"Thread not found"** - Invalid channel/thread ID or bot doesn't have access
277
+ ### Report delivery
278
+
279
+ ```bash
280
+ # Send summary embed
281
+ cord embed 123456789 "Weekly metrics compiled" \
282
+ --title "Weekly Report Ready" \
283
+ --color blue \
284
+ --field "Period:Jan 15-21:inline" \
285
+ --field "Pages:12:inline"
286
+
287
+ # Attach the full report
288
+ cord file 123456789 ./weekly-report.pdf "Full report attached"
289
+ ```
269
290
 
270
- **Button shows "expired"** - Handler was never registered or bot restarted since registration
291
+ ### Confirmation flow
271
292
 
272
- ## Installation
293
+ ```bash
294
+ # Ask for confirmation
295
+ cord buttons 123456789 "Delete all archived items older than 30 days?" \
296
+ --button label="Yes, Delete" id="confirm-delete" style="danger" reply="Deleting..." \
297
+ --button label="Cancel" id="cancel-delete" style="secondary" reply="Cancelled"
298
+ ```
273
299
 
274
- To use this skill with Claude Code:
300
+ ---
275
301
 
276
- 1. Copy this file to your `.claude/skills/cord/SKILL.md`
277
- 2. Or reference it directly from the Cord repo
302
+ ## HTTP API
278
303
 
279
- Claude Code will automatically detect the skill and use it when you ask to send Discord messages.
304
+ For advanced use cases (webhooks, external scripts), see [HTTP-API.md](./HTTP-API.md).
package/src/api.ts CHANGED
@@ -288,6 +288,51 @@ async function handleCommand(
288
288
  return { success: true };
289
289
  }
290
290
 
291
+ case 'reply-to-message': {
292
+ const channelId = args.channel as string;
293
+ const messageId = args.message as string;
294
+ const content = args.content as string;
295
+
296
+ const channel = await client.channels.fetch(channelId);
297
+ if (!channel?.isTextBased()) {
298
+ throw new Error('Invalid channel');
299
+ }
300
+
301
+ const targetMessage = await (channel as TextChannel).messages.fetch(messageId);
302
+ const sent = await targetMessage.reply(content);
303
+ return { success: true, messageId: sent.id };
304
+ }
305
+
306
+ case 'create-thread': {
307
+ const channelId = args.channel as string;
308
+ const messageId = args.message as string;
309
+ const name = args.name as string;
310
+
311
+ const channel = await client.channels.fetch(channelId);
312
+ if (!channel?.isTextBased()) {
313
+ throw new Error('Invalid channel');
314
+ }
315
+
316
+ const message = await (channel as TextChannel).messages.fetch(messageId);
317
+ const thread = await message.startThread({ name });
318
+ return { success: true, threadId: thread.id };
319
+ }
320
+
321
+ case 'add-reaction': {
322
+ const channelId = args.channel as string;
323
+ const messageId = args.message as string;
324
+ const emoji = args.emoji as string;
325
+
326
+ const channel = await client.channels.fetch(channelId);
327
+ if (!channel?.isTextBased()) {
328
+ throw new Error('Invalid channel');
329
+ }
330
+
331
+ const message = await (channel as TextChannel).messages.fetch(messageId);
332
+ await message.react(emoji);
333
+ return { success: true };
334
+ }
335
+
291
336
  default:
292
337
  throw new Error(`Unknown command: ${command}`);
293
338
  }