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.
@@ -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
+ [![npm version](https://badge.fury.io/js/cord-bot.svg)](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
- # Install dependencies
61
+ # Clone and install
62
+ git clone https://github.com/alexknowshtml/cord.git
63
+ cd cord
52
64
  bun install
53
65
 
54
- # Set environment variables
55
- export DISCORD_BOT_TOKEN="your-bot-token"
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 the bot and worker
61
- bun run src/bot.ts &
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cord-bot",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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) => {