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.
- package/.github/workflows/publish.yml +5 -7
- package/README.md +54 -90
- package/bin/cord.ts +449 -15
- package/package.json +6 -1
- package/skills/cord/{PRIMITIVES.md → HTTP-API.md} +53 -2
- package/skills/cord/SKILL.md +202 -177
- package/src/api.ts +45 -0
|
@@ -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: '
|
|
25
|
+
node-version: '22'
|
|
29
26
|
registry-url: 'https://registry.npmjs.org'
|
|
30
27
|
|
|
31
|
-
- name:
|
|
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
|
|
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
|
[](https://www.npmjs.com/package/cord-bot)
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## What You Get
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
15
|
+
## Quick Start
|
|
24
16
|
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
88
|
+
Once running, Cord provides CLI commands for interacting with Discord:
|
|
100
89
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
205
|
-
stop
|
|
206
|
-
status
|
|
207
|
-
setup
|
|
208
|
-
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
|
|
212
|
-
cord
|
|
213
|
-
cord
|
|
214
|
-
cord
|
|
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.
|
|
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
|
-
#
|
|
1
|
+
# HTTP API Reference
|
|
2
2
|
|
|
3
|
-
HTTP API for interacting with Discord
|
|
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:
|
package/skills/cord/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cord
|
|
3
|
-
description: Send messages,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## CLI Commands
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
### send
|
|
35
|
+
|
|
36
|
+
Send a text message to a channel or thread.
|
|
34
37
|
|
|
35
38
|
```bash
|
|
36
|
-
|
|
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
|
-
**
|
|
42
|
+
**Example:**
|
|
43
|
+
```bash
|
|
44
|
+
cord send 123456789 "Hello world!"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
48
|
|
|
49
|
-
###
|
|
49
|
+
### embed
|
|
50
50
|
|
|
51
|
-
Send a formatted embed card with optional
|
|
51
|
+
Send a formatted embed card with optional styling.
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
|
|
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
|
-
**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### buttons
|
|
97
111
|
|
|
98
|
-
Send
|
|
112
|
+
Send interactive buttons with optional handlers.
|
|
99
113
|
|
|
100
114
|
```bash
|
|
101
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### edit
|
|
155
162
|
|
|
156
163
|
Edit an existing message.
|
|
157
164
|
|
|
158
165
|
```bash
|
|
159
|
-
|
|
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
|
-
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### delete
|
|
172
172
|
|
|
173
173
|
Delete a message.
|
|
174
174
|
|
|
175
175
|
```bash
|
|
176
|
-
|
|
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
|
-
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### rename
|
|
188
182
|
|
|
189
183
|
Rename a thread.
|
|
190
184
|
|
|
191
185
|
```bash
|
|
192
|
-
|
|
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
|
-
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### reply
|
|
204
192
|
|
|
205
|
-
|
|
193
|
+
Reply to a specific message (shows reply preview).
|
|
206
194
|
|
|
207
195
|
```bash
|
|
208
|
-
|
|
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
|
-
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### thread
|
|
202
|
+
|
|
203
|
+
Create a thread from a message.
|
|
224
204
|
|
|
225
205
|
```bash
|
|
226
|
-
|
|
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
|
-
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### react
|
|
212
|
+
|
|
213
|
+
Add a reaction to a message.
|
|
237
214
|
|
|
238
215
|
```bash
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
+
### Notification with follow-up options
|
|
254
244
|
|
|
255
245
|
```bash
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
+
### Progress updates
|
|
261
261
|
|
|
262
|
-
**"Connection refused"** - Cord bot not running
|
|
263
262
|
```bash
|
|
264
|
-
|
|
265
|
-
cord
|
|
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
|
-
|
|
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
|
-
|
|
291
|
+
### Confirmation flow
|
|
271
292
|
|
|
272
|
-
|
|
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
|
-
|
|
300
|
+
---
|
|
275
301
|
|
|
276
|
-
|
|
277
|
-
2. Or reference it directly from the Cord repo
|
|
302
|
+
## HTTP API
|
|
278
303
|
|
|
279
|
-
|
|
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
|
}
|