cord-bot 1.0.2 → 1.0.4
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/.github/workflows/publish.yml +34 -0
- package/README.md +43 -6
- package/bin/cord.ts +20 -2
- package/package.json +1 -1
- package/skills/cord/PRIMITIVES.md +222 -0
- package/skills/cord/SKILL.md +279 -0
- package/src/api.ts +294 -0
- package/src/bot.ts +46 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: oven-sh/setup-bun@v2
|
|
17
|
+
with:
|
|
18
|
+
bun-version: latest
|
|
19
|
+
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: bun install
|
|
22
|
+
|
|
23
|
+
- name: Build
|
|
24
|
+
run: bun run build
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: '20'
|
|
29
|
+
registry-url: 'https://registry.npmjs.org'
|
|
30
|
+
|
|
31
|
+
- name: Publish to npm
|
|
32
|
+
run: npm publish --provenance --access public
|
|
33
|
+
env:
|
|
34
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
CHANGED
|
@@ -4,6 +4,16 @@ A simple bridge that connects Discord to Claude Code CLI.
|
|
|
4
4
|
|
|
5
5
|
> **cord** /kôrd/ — a connection between two things.
|
|
6
6
|
|
|
7
|
+
[](https://www.npmjs.com/package/cord-bot)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install cord-bot
|
|
13
|
+
# or
|
|
14
|
+
bunx cord-bot
|
|
15
|
+
```
|
|
16
|
+
|
|
7
17
|
When someone @mentions your bot, it:
|
|
8
18
|
1. Creates a thread for the conversation
|
|
9
19
|
2. Queues the message for Claude processing
|
|
@@ -48,18 +58,19 @@ Discord Bot → BullMQ Queue → Claude Spawner
|
|
|
48
58
|
## Quick Start
|
|
49
59
|
|
|
50
60
|
```bash
|
|
51
|
-
#
|
|
61
|
+
# Clone and install
|
|
62
|
+
git clone https://github.com/alexknowshtml/cord.git
|
|
63
|
+
cd cord
|
|
52
64
|
bun install
|
|
53
65
|
|
|
54
|
-
#
|
|
55
|
-
|
|
66
|
+
# Run setup wizard (configures .env, checks dependencies, installs skill)
|
|
67
|
+
cord setup
|
|
56
68
|
|
|
57
69
|
# Start Redis (if not already running)
|
|
58
70
|
redis-server &
|
|
59
71
|
|
|
60
|
-
# Start
|
|
61
|
-
|
|
62
|
-
bun run src/worker.ts
|
|
72
|
+
# Start Cord
|
|
73
|
+
cord start
|
|
63
74
|
```
|
|
64
75
|
|
|
65
76
|
## Environment Variables
|
|
@@ -108,6 +119,32 @@ claude --print --resume UUID -p "prompt"
|
|
|
108
119
|
claude --append-system-prompt "Current time: ..."
|
|
109
120
|
```
|
|
110
121
|
|
|
122
|
+
## HTTP API
|
|
123
|
+
|
|
124
|
+
Cord exposes an HTTP API on port 2643 for external tools to interact with Discord:
|
|
125
|
+
|
|
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
|
|
141
|
+
|
|
142
|
+
# Or copy manually
|
|
143
|
+
cp -r skills/cord ~/.claude/skills/
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
See [skills/cord/SKILL.md](./skills/cord/SKILL.md) for skill documentation.
|
|
147
|
+
|
|
111
148
|
## License
|
|
112
149
|
|
|
113
150
|
MIT
|
package/bin/cord.ts
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { spawn, spawnSync } from 'bun';
|
|
14
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
15
|
-
import { join } from 'path';
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'fs';
|
|
15
|
+
import { join, dirname } from 'path';
|
|
16
16
|
import * as readline from 'readline';
|
|
17
|
+
import { homedir } from 'os';
|
|
17
18
|
|
|
18
19
|
const PID_FILE = join(process.cwd(), '.cord.pid');
|
|
19
20
|
|
|
@@ -76,6 +77,23 @@ async function setup() {
|
|
|
76
77
|
console.log('⚠ Claude CLI not found. Install from: https://claude.ai/code');
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// Install Claude Code skill
|
|
81
|
+
const skillsDir = join(homedir(), '.claude', 'skills', 'cord');
|
|
82
|
+
const cordRoot = join(dirname(import.meta.dir));
|
|
83
|
+
const sourceSkillsDir = join(cordRoot, 'skills', 'cord');
|
|
84
|
+
|
|
85
|
+
if (existsSync(sourceSkillsDir)) {
|
|
86
|
+
console.log('\n📚 Claude Code Skill');
|
|
87
|
+
console.log(' Teaches your assistant how to send Discord messages, embeds,');
|
|
88
|
+
console.log(' files, and interactive buttons.');
|
|
89
|
+
const installSkill = await prompt('Install skill? (Y/n): ');
|
|
90
|
+
if (installSkill.toLowerCase() !== 'n') {
|
|
91
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
92
|
+
cpSync(sourceSkillsDir, skillsDir, { recursive: true });
|
|
93
|
+
console.log(`✓ Skill installed to ${skillsDir}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
console.log('\n✨ Setup complete! Run: cord start\n');
|
|
80
98
|
}
|
|
81
99
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Discord Primitives
|
|
2
|
+
|
|
3
|
+
HTTP API for interacting with Discord from scripts and Claude skills.
|
|
4
|
+
|
|
5
|
+
**Port:** `2643` (configurable via `API_PORT` env var)
|
|
6
|
+
|
|
7
|
+
## Health Check
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
curl http://localhost:2643/health
|
|
11
|
+
# {"status":"ok","connected":true,"user":"MyBot#1234"}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## send-message
|
|
15
|
+
|
|
16
|
+
Send a text message to a channel or thread.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
curl -X POST http://localhost:2643/command \
|
|
20
|
+
-H 'Content-Type: application/json' \
|
|
21
|
+
-d '{
|
|
22
|
+
"command": "send-to-thread",
|
|
23
|
+
"args": {
|
|
24
|
+
"thread": "CHANNEL_OR_THREAD_ID",
|
|
25
|
+
"message": "Hello from Cord!"
|
|
26
|
+
}
|
|
27
|
+
}'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## send-embed
|
|
31
|
+
|
|
32
|
+
Send a formatted embed card with optional fields.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
curl -X POST http://localhost:2643/command \
|
|
36
|
+
-H 'Content-Type: application/json' \
|
|
37
|
+
-d '{
|
|
38
|
+
"command": "send-to-thread",
|
|
39
|
+
"args": {
|
|
40
|
+
"thread": "CHANNEL_OR_THREAD_ID",
|
|
41
|
+
"embeds": [{
|
|
42
|
+
"title": "Status Report",
|
|
43
|
+
"description": "Daily summary",
|
|
44
|
+
"color": 3447003,
|
|
45
|
+
"fields": [
|
|
46
|
+
{"name": "Tasks", "value": "5 completed", "inline": true},
|
|
47
|
+
{"name": "Emails", "value": "12 processed", "inline": true}
|
|
48
|
+
],
|
|
49
|
+
"footer": {"text": "Generated by Cord"}
|
|
50
|
+
}]
|
|
51
|
+
}
|
|
52
|
+
}'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Color reference:**
|
|
56
|
+
- Success/Green: `3066993` (0x2ECC71)
|
|
57
|
+
- Info/Blue: `3447003` (0x3498DB)
|
|
58
|
+
- Warning/Yellow: `16776960` (0xFFFF00)
|
|
59
|
+
- Error/Red: `15158332` (0xE74C3C)
|
|
60
|
+
- Purple: `10181046` (0x9B59B6)
|
|
61
|
+
|
|
62
|
+
## send-file
|
|
63
|
+
|
|
64
|
+
Attach content as a file (good for long reports).
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
curl -X POST http://localhost:2643/send-with-file \
|
|
68
|
+
-H 'Content-Type: application/json' \
|
|
69
|
+
-d '{
|
|
70
|
+
"channelId": "CHANNEL_OR_THREAD_ID",
|
|
71
|
+
"fileName": "report.md",
|
|
72
|
+
"fileContent": "# Report\n\nContent here...",
|
|
73
|
+
"content": "Here is the detailed report"
|
|
74
|
+
}'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## send-buttons
|
|
78
|
+
|
|
79
|
+
Send a message with interactive button choices.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
curl -X POST http://localhost:2643/send-with-buttons \
|
|
83
|
+
-H 'Content-Type: application/json' \
|
|
84
|
+
-d '{
|
|
85
|
+
"channelId": "CHANNEL_OR_THREAD_ID",
|
|
86
|
+
"content": "Choose an option:",
|
|
87
|
+
"buttons": [
|
|
88
|
+
{"label": "Approve", "customId": "approve-123", "style": "success"},
|
|
89
|
+
{"label": "Reject", "customId": "reject-123", "style": "danger"}
|
|
90
|
+
]
|
|
91
|
+
}'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Button styles:** `primary`, `secondary`, `success`, `danger`
|
|
95
|
+
|
|
96
|
+
### Buttons with inline handler
|
|
97
|
+
|
|
98
|
+
Register a handler that fires when the button is clicked.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
curl -X POST http://localhost:2643/send-with-buttons \
|
|
102
|
+
-H 'Content-Type: application/json' \
|
|
103
|
+
-d '{
|
|
104
|
+
"channelId": "CHANNEL_OR_THREAD_ID",
|
|
105
|
+
"content": "Click for details",
|
|
106
|
+
"buttons": [{
|
|
107
|
+
"label": "Show Details",
|
|
108
|
+
"customId": "details-123",
|
|
109
|
+
"style": "secondary",
|
|
110
|
+
"handler": {
|
|
111
|
+
"type": "inline",
|
|
112
|
+
"content": "Here are the details...",
|
|
113
|
+
"ephemeral": true
|
|
114
|
+
}
|
|
115
|
+
}]
|
|
116
|
+
}'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Buttons with webhook handler
|
|
120
|
+
|
|
121
|
+
Call an external URL when clicked.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
curl -X POST http://localhost:2643/send-with-buttons \
|
|
125
|
+
-H 'Content-Type: application/json' \
|
|
126
|
+
-d '{
|
|
127
|
+
"channelId": "CHANNEL_OR_THREAD_ID",
|
|
128
|
+
"content": "Approve deployment?",
|
|
129
|
+
"buttons": [{
|
|
130
|
+
"label": "Deploy",
|
|
131
|
+
"customId": "deploy-prod",
|
|
132
|
+
"style": "success",
|
|
133
|
+
"handler": {
|
|
134
|
+
"type": "webhook",
|
|
135
|
+
"url": "http://localhost:8080/deploy",
|
|
136
|
+
"data": {"env": "production"}
|
|
137
|
+
}
|
|
138
|
+
}]
|
|
139
|
+
}'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## typing
|
|
143
|
+
|
|
144
|
+
Show typing indicator (useful before slow operations).
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
curl -X POST http://localhost:2643/command \
|
|
148
|
+
-H 'Content-Type: application/json' \
|
|
149
|
+
-d '{
|
|
150
|
+
"command": "start-typing",
|
|
151
|
+
"args": {"channel": "CHANNEL_OR_THREAD_ID"}
|
|
152
|
+
}'
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## edit-message
|
|
156
|
+
|
|
157
|
+
Edit an existing message.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
curl -X POST http://localhost:2643/command \
|
|
161
|
+
-H 'Content-Type: application/json' \
|
|
162
|
+
-d '{
|
|
163
|
+
"command": "edit-message",
|
|
164
|
+
"args": {
|
|
165
|
+
"channel": "CHANNEL_ID",
|
|
166
|
+
"message": "MESSAGE_ID",
|
|
167
|
+
"content": "Updated content"
|
|
168
|
+
}
|
|
169
|
+
}'
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## delete-message
|
|
173
|
+
|
|
174
|
+
Delete a message.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
curl -X POST http://localhost:2643/command \
|
|
178
|
+
-H 'Content-Type: application/json' \
|
|
179
|
+
-d '{
|
|
180
|
+
"command": "delete-message",
|
|
181
|
+
"args": {
|
|
182
|
+
"channel": "CHANNEL_ID",
|
|
183
|
+
"message": "MESSAGE_ID"
|
|
184
|
+
}
|
|
185
|
+
}'
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## rename-thread
|
|
189
|
+
|
|
190
|
+
Rename a thread.
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
curl -X POST http://localhost:2643/command \
|
|
194
|
+
-H 'Content-Type: application/json' \
|
|
195
|
+
-d '{
|
|
196
|
+
"command": "rename-thread",
|
|
197
|
+
"args": {
|
|
198
|
+
"thread": "THREAD_ID",
|
|
199
|
+
"name": "New Thread Name"
|
|
200
|
+
}
|
|
201
|
+
}'
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Response Format
|
|
205
|
+
|
|
206
|
+
All endpoints return JSON:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{"success": true, "messageId": "1234567890"}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
On error:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{"error": "Description of what went wrong"}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Environment Variables
|
|
219
|
+
|
|
220
|
+
| Variable | Default | Description |
|
|
221
|
+
|----------|---------|-------------|
|
|
222
|
+
| `API_PORT` | `2643` | HTTP API server port |
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
---
|
|
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.
|
|
4
|
+
triggers:
|
|
5
|
+
- "send to discord"
|
|
6
|
+
- "post to discord"
|
|
7
|
+
- "discord message"
|
|
8
|
+
- "notify discord"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Cord - Discord Bridge Skill
|
|
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.
|
|
16
|
+
|
|
17
|
+
**GitHub:** https://github.com/alexknowshtml/cord
|
|
18
|
+
|
|
19
|
+
**API Reference:** [PRIMITIVES.md](./PRIMITIVES.md) - Full HTTP API documentation
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
Ensure Cord is running:
|
|
24
|
+
```bash
|
|
25
|
+
cord start
|
|
26
|
+
# or: bun run src/bot.ts
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Primitives
|
|
30
|
+
|
|
31
|
+
### send-message
|
|
32
|
+
|
|
33
|
+
Send a simple text message to a channel or thread.
|
|
34
|
+
|
|
35
|
+
```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
|
+
}'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Response:** `{"success":true,"messageId":"1234567890"}`
|
|
48
|
+
|
|
49
|
+
### send-embed
|
|
50
|
+
|
|
51
|
+
Send a formatted embed card with optional fields.
|
|
52
|
+
|
|
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
|
+
}'
|
|
72
|
+
```
|
|
73
|
+
|
|
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)
|
|
80
|
+
|
|
81
|
+
### send-file
|
|
82
|
+
|
|
83
|
+
Attach content as a file (good for long reports).
|
|
84
|
+
|
|
85
|
+
```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
|
+
}'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### send-buttons
|
|
97
|
+
|
|
98
|
+
Send a message with interactive button choices.
|
|
99
|
+
|
|
100
|
+
```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
|
+
}'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Button styles:** `primary`, `secondary`, `success`, `danger`
|
|
114
|
+
|
|
115
|
+
### send-buttons with inline handler
|
|
116
|
+
|
|
117
|
+
Register a handler that fires when the button is clicked.
|
|
118
|
+
|
|
119
|
+
```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
|
+
}'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### typing
|
|
142
|
+
|
|
143
|
+
Show typing indicator (useful before slow operations).
|
|
144
|
+
|
|
145
|
+
```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
|
+
}'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### edit-message
|
|
155
|
+
|
|
156
|
+
Edit an existing message.
|
|
157
|
+
|
|
158
|
+
```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
|
+
}'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### delete-message
|
|
172
|
+
|
|
173
|
+
Delete a message.
|
|
174
|
+
|
|
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
|
+
}'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### rename-thread
|
|
188
|
+
|
|
189
|
+
Rename a thread.
|
|
190
|
+
|
|
191
|
+
```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
|
+
}'
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Usage Examples
|
|
204
|
+
|
|
205
|
+
### Post a notification
|
|
206
|
+
|
|
207
|
+
```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
|
+
}'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Send a file attachment
|
|
224
|
+
|
|
225
|
+
```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
|
+
}'
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Ask for confirmation with buttons
|
|
237
|
+
|
|
238
|
+
```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
|
+
}'
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Health Check
|
|
252
|
+
|
|
253
|
+
Verify Cord is running:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
curl -s http://localhost:2643/health
|
|
257
|
+
# {"status":"ok","connected":true,"user":"YourBot#1234"}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Troubleshooting
|
|
261
|
+
|
|
262
|
+
**"Connection refused"** - Cord bot not running
|
|
263
|
+
```bash
|
|
264
|
+
cord status
|
|
265
|
+
cord start
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**"Thread not found"** - Invalid channel/thread ID or bot doesn't have access
|
|
269
|
+
|
|
270
|
+
**Button shows "expired"** - Handler was never registered or bot restarted since registration
|
|
271
|
+
|
|
272
|
+
## Installation
|
|
273
|
+
|
|
274
|
+
To use this skill with Claude Code:
|
|
275
|
+
|
|
276
|
+
1. Copy this file to your `.claude/skills/cord/SKILL.md`
|
|
277
|
+
2. Or reference it directly from the Cord repo
|
|
278
|
+
|
|
279
|
+
Claude Code will automatically detect the skill and use it when you ask to send Discord messages.
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP API Server - Discord primitives for external tools
|
|
3
|
+
*
|
|
4
|
+
* Provides HTTP endpoints for sending messages, embeds, files, buttons,
|
|
5
|
+
* and managing threads. Useful for scripts, automation, and Claude skills.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Client, TextChannel, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
|
|
9
|
+
|
|
10
|
+
const log = (msg: string) => process.stdout.write(`[api] ${msg}\n`);
|
|
11
|
+
|
|
12
|
+
// Button handler registry for dynamic button responses
|
|
13
|
+
type ButtonHandler = {
|
|
14
|
+
type: 'inline';
|
|
15
|
+
content: string;
|
|
16
|
+
ephemeral?: boolean;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'webhook';
|
|
19
|
+
url: string;
|
|
20
|
+
data?: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const buttonHandlers = new Map<string, ButtonHandler>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Start the HTTP API server
|
|
27
|
+
*/
|
|
28
|
+
export function startApiServer(client: Client, port: number = 2643) {
|
|
29
|
+
const server = Bun.serve({
|
|
30
|
+
port,
|
|
31
|
+
async fetch(req) {
|
|
32
|
+
const url = new URL(req.url);
|
|
33
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
34
|
+
|
|
35
|
+
// Health check
|
|
36
|
+
if (url.pathname === '/health' && req.method === 'GET') {
|
|
37
|
+
return new Response(JSON.stringify({
|
|
38
|
+
status: 'ok',
|
|
39
|
+
connected: client.isReady(),
|
|
40
|
+
user: client.user?.tag || null,
|
|
41
|
+
}), { headers });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Send message to thread/channel
|
|
45
|
+
if (url.pathname === '/command' && req.method === 'POST') {
|
|
46
|
+
try {
|
|
47
|
+
const body = await req.json() as {
|
|
48
|
+
command: string;
|
|
49
|
+
args: Record<string, unknown>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const result = await handleCommand(client, body.command, body.args);
|
|
53
|
+
return new Response(JSON.stringify(result), { headers });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
log(`Command error: ${error}`);
|
|
56
|
+
return new Response(JSON.stringify({ error: String(error) }), {
|
|
57
|
+
status: 500,
|
|
58
|
+
headers,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Send file attachment
|
|
64
|
+
if (url.pathname === '/send-with-file' && req.method === 'POST') {
|
|
65
|
+
try {
|
|
66
|
+
const body = await req.json() as {
|
|
67
|
+
channelId: string;
|
|
68
|
+
fileName: string;
|
|
69
|
+
fileContent: string;
|
|
70
|
+
content?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const channel = await client.channels.fetch(body.channelId);
|
|
74
|
+
if (!channel?.isTextBased()) {
|
|
75
|
+
return new Response(JSON.stringify({ error: 'Invalid channel' }), {
|
|
76
|
+
status: 400,
|
|
77
|
+
headers,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const buffer = Buffer.from(body.fileContent, 'utf-8');
|
|
82
|
+
const message = await (channel as TextChannel).send({
|
|
83
|
+
content: body.content || undefined,
|
|
84
|
+
files: [{
|
|
85
|
+
attachment: buffer,
|
|
86
|
+
name: body.fileName,
|
|
87
|
+
}],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return new Response(JSON.stringify({
|
|
91
|
+
success: true,
|
|
92
|
+
messageId: message.id,
|
|
93
|
+
}), { headers });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
log(`Send file error: ${error}`);
|
|
96
|
+
return new Response(JSON.stringify({ error: String(error) }), {
|
|
97
|
+
status: 500,
|
|
98
|
+
headers,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Send message with buttons
|
|
104
|
+
if (url.pathname === '/send-with-buttons' && req.method === 'POST') {
|
|
105
|
+
try {
|
|
106
|
+
const body = await req.json() as {
|
|
107
|
+
channelId: string;
|
|
108
|
+
content?: string;
|
|
109
|
+
embeds?: Array<{
|
|
110
|
+
title?: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
color?: number;
|
|
113
|
+
fields?: Array<{ name: string; value: string; inline?: boolean }>;
|
|
114
|
+
footer?: { text: string };
|
|
115
|
+
}>;
|
|
116
|
+
buttons: Array<{
|
|
117
|
+
label: string;
|
|
118
|
+
customId: string;
|
|
119
|
+
style: 'primary' | 'secondary' | 'success' | 'danger';
|
|
120
|
+
handler?: ButtonHandler;
|
|
121
|
+
}>;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const channel = await client.channels.fetch(body.channelId);
|
|
125
|
+
if (!channel?.isTextBased()) {
|
|
126
|
+
return new Response(JSON.stringify({ error: 'Invalid channel' }), {
|
|
127
|
+
status: 400,
|
|
128
|
+
headers,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Build embed if provided
|
|
133
|
+
const embeds = body.embeds?.map(e => {
|
|
134
|
+
const embed = new EmbedBuilder();
|
|
135
|
+
if (e.title) embed.setTitle(e.title);
|
|
136
|
+
if (e.description) embed.setDescription(e.description);
|
|
137
|
+
if (e.color) embed.setColor(e.color);
|
|
138
|
+
if (e.fields) embed.addFields(e.fields);
|
|
139
|
+
if (e.footer) embed.setFooter(e.footer);
|
|
140
|
+
return embed;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Build button row
|
|
144
|
+
const styleMap: Record<string, ButtonStyle> = {
|
|
145
|
+
primary: ButtonStyle.Primary,
|
|
146
|
+
secondary: ButtonStyle.Secondary,
|
|
147
|
+
success: ButtonStyle.Success,
|
|
148
|
+
danger: ButtonStyle.Danger,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const buttons = body.buttons.map(b => {
|
|
152
|
+
// Register handler if provided
|
|
153
|
+
if (b.handler) {
|
|
154
|
+
buttonHandlers.set(b.customId, b.handler);
|
|
155
|
+
}
|
|
156
|
+
return new ButtonBuilder()
|
|
157
|
+
.setCustomId(b.customId)
|
|
158
|
+
.setLabel(b.label)
|
|
159
|
+
.setStyle(styleMap[b.style] || ButtonStyle.Primary);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(buttons);
|
|
163
|
+
|
|
164
|
+
const message = await (channel as TextChannel).send({
|
|
165
|
+
content: body.content || undefined,
|
|
166
|
+
embeds: embeds || undefined,
|
|
167
|
+
components: [row],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return new Response(JSON.stringify({
|
|
171
|
+
success: true,
|
|
172
|
+
messageId: message.id,
|
|
173
|
+
}), { headers });
|
|
174
|
+
} catch (error) {
|
|
175
|
+
log(`Send buttons error: ${error}`);
|
|
176
|
+
return new Response(JSON.stringify({ error: String(error) }), {
|
|
177
|
+
status: 500,
|
|
178
|
+
headers,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 404 for unknown routes
|
|
184
|
+
return new Response(JSON.stringify({ error: 'Not found' }), {
|
|
185
|
+
status: 404,
|
|
186
|
+
headers,
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
log(`HTTP API server listening on port ${port}`);
|
|
192
|
+
return server;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Handle a command from the /command endpoint
|
|
197
|
+
*/
|
|
198
|
+
async function handleCommand(
|
|
199
|
+
client: Client,
|
|
200
|
+
command: string,
|
|
201
|
+
args: Record<string, unknown>
|
|
202
|
+
): Promise<Record<string, unknown>> {
|
|
203
|
+
switch (command) {
|
|
204
|
+
case 'send-to-thread': {
|
|
205
|
+
const threadId = args.thread as string;
|
|
206
|
+
const message = args.message as string | undefined;
|
|
207
|
+
const embeds = args.embeds as Array<{
|
|
208
|
+
title?: string;
|
|
209
|
+
description?: string;
|
|
210
|
+
color?: number;
|
|
211
|
+
fields?: Array<{ name: string; value: string; inline?: boolean }>;
|
|
212
|
+
footer?: { text: string };
|
|
213
|
+
}> | undefined;
|
|
214
|
+
|
|
215
|
+
const channel = await client.channels.fetch(threadId);
|
|
216
|
+
if (!channel?.isTextBased()) {
|
|
217
|
+
throw new Error('Invalid thread/channel');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Build embeds if provided
|
|
221
|
+
const discordEmbeds = embeds?.map(e => {
|
|
222
|
+
const embed = new EmbedBuilder();
|
|
223
|
+
if (e.title) embed.setTitle(e.title);
|
|
224
|
+
if (e.description) embed.setDescription(e.description);
|
|
225
|
+
if (e.color) embed.setColor(e.color);
|
|
226
|
+
if (e.fields) embed.addFields(e.fields);
|
|
227
|
+
if (e.footer) embed.setFooter(e.footer);
|
|
228
|
+
return embed;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const sent = await (channel as TextChannel).send({
|
|
232
|
+
content: message || undefined,
|
|
233
|
+
embeds: discordEmbeds || undefined,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return { success: true, messageId: sent.id };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case 'start-typing': {
|
|
240
|
+
const channelId = args.channel as string;
|
|
241
|
+
const channel = await client.channels.fetch(channelId);
|
|
242
|
+
if (!channel?.isTextBased()) {
|
|
243
|
+
throw new Error('Invalid channel');
|
|
244
|
+
}
|
|
245
|
+
await (channel as TextChannel).sendTyping();
|
|
246
|
+
return { success: true };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case 'edit-message': {
|
|
250
|
+
const channelId = args.channel as string;
|
|
251
|
+
const messageId = args.message as string;
|
|
252
|
+
const content = args.content as string;
|
|
253
|
+
|
|
254
|
+
const channel = await client.channels.fetch(channelId);
|
|
255
|
+
if (!channel?.isTextBased()) {
|
|
256
|
+
throw new Error('Invalid channel');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const message = await (channel as TextChannel).messages.fetch(messageId);
|
|
260
|
+
await message.edit(content);
|
|
261
|
+
return { success: true };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case 'delete-message': {
|
|
265
|
+
const channelId = args.channel as string;
|
|
266
|
+
const messageId = args.message as string;
|
|
267
|
+
|
|
268
|
+
const channel = await client.channels.fetch(channelId);
|
|
269
|
+
if (!channel?.isTextBased()) {
|
|
270
|
+
throw new Error('Invalid channel');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const message = await (channel as TextChannel).messages.fetch(messageId);
|
|
274
|
+
await message.delete();
|
|
275
|
+
return { success: true };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case 'rename-thread': {
|
|
279
|
+
const threadId = args.thread as string;
|
|
280
|
+
const name = args.name as string;
|
|
281
|
+
|
|
282
|
+
const channel = await client.channels.fetch(threadId);
|
|
283
|
+
if (!channel?.isThread()) {
|
|
284
|
+
throw new Error('Invalid thread');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await channel.setName(name);
|
|
288
|
+
return { success: true };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
default:
|
|
292
|
+
throw new Error(`Unknown command: ${command}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
package/src/bot.ts
CHANGED
|
@@ -14,10 +14,12 @@ import {
|
|
|
14
14
|
Events,
|
|
15
15
|
Message,
|
|
16
16
|
TextChannel,
|
|
17
|
-
ThreadAutoArchiveDuration
|
|
17
|
+
ThreadAutoArchiveDuration,
|
|
18
|
+
Interaction,
|
|
18
19
|
} from 'discord.js';
|
|
19
20
|
import { claudeQueue } from './queue.js';
|
|
20
21
|
import { db } from './db.js';
|
|
22
|
+
import { startApiServer, buttonHandlers } from './api.js';
|
|
21
23
|
|
|
22
24
|
// Force unbuffered logging
|
|
23
25
|
const log = (msg: string) => process.stdout.write(`[bot] ${msg}\n`);
|
|
@@ -32,6 +34,49 @@ const client = new Client({
|
|
|
32
34
|
|
|
33
35
|
client.once(Events.ClientReady, (c) => {
|
|
34
36
|
log(`Logged in as ${c.user.tag}`);
|
|
37
|
+
|
|
38
|
+
// Start HTTP API server
|
|
39
|
+
const apiPort = parseInt(process.env.API_PORT || '2643');
|
|
40
|
+
startApiServer(client, apiPort);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Handle button interactions
|
|
44
|
+
client.on(Events.InteractionCreate, async (interaction: Interaction) => {
|
|
45
|
+
if (!interaction.isButton()) return;
|
|
46
|
+
|
|
47
|
+
const handler = buttonHandlers.get(interaction.customId);
|
|
48
|
+
if (!handler) {
|
|
49
|
+
await interaction.reply({ content: 'This button has expired.', ephemeral: true });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
if (handler.type === 'inline') {
|
|
55
|
+
await interaction.reply({
|
|
56
|
+
content: handler.content,
|
|
57
|
+
ephemeral: handler.ephemeral ?? false,
|
|
58
|
+
});
|
|
59
|
+
} else if (handler.type === 'webhook') {
|
|
60
|
+
await interaction.deferReply({ ephemeral: true });
|
|
61
|
+
const response = await fetch(handler.url, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
customId: interaction.customId,
|
|
66
|
+
userId: interaction.user.id,
|
|
67
|
+
channelId: interaction.channelId,
|
|
68
|
+
data: handler.data,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
const result = await response.json() as { content?: string };
|
|
72
|
+
await interaction.editReply({ content: result.content || 'Done.' });
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
log(`Button handler error: ${error}`);
|
|
76
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
77
|
+
await interaction.reply({ content: 'An error occurred.', ephemeral: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
35
80
|
});
|
|
36
81
|
|
|
37
82
|
client.on(Events.MessageCreate, async (message: Message) => {
|