@mulmobridge/discord 0.1.0
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/README.md +53 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +107 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @mulmobridge/discord
|
|
2
|
+
|
|
3
|
+
> **Experimental** — please test and [report issues](https://github.com/receptron/mulmoclaude/issues/new).
|
|
4
|
+
|
|
5
|
+
Discord bridge for [MulmoClaude](https://github.com/receptron/mulmoclaude). The bot responds to messages in channels it has access to.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
### 1. Create a Discord Application
|
|
10
|
+
|
|
11
|
+
1. Go to [discord.com/developers/applications](https://discord.com/developers/applications) → **New Application**
|
|
12
|
+
2. Name it (e.g. "MulmoClaude")
|
|
13
|
+
|
|
14
|
+
### 2. Create the Bot
|
|
15
|
+
|
|
16
|
+
1. **Bot** tab → **Add Bot**
|
|
17
|
+
2. Copy the **Token** (this is your `DISCORD_BOT_TOKEN`)
|
|
18
|
+
3. Enable **Message Content Intent** under Privileged Gateway Intents
|
|
19
|
+
|
|
20
|
+
### 3. Invite the bot to your server
|
|
21
|
+
|
|
22
|
+
**OAuth2** → **URL Generator**:
|
|
23
|
+
- Scopes: `bot`
|
|
24
|
+
- Permissions: `Send Messages`, `Read Message History`
|
|
25
|
+
|
|
26
|
+
Copy the generated URL and open it in your browser to invite the bot.
|
|
27
|
+
|
|
28
|
+
### 4. Run the bridge
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# With mock server (testing)
|
|
32
|
+
npx @mulmobridge/mock-server &
|
|
33
|
+
DISCORD_BOT_TOKEN=... \
|
|
34
|
+
MULMOCLAUDE_AUTH_TOKEN=mock-test-token \
|
|
35
|
+
npx @mulmobridge/discord
|
|
36
|
+
|
|
37
|
+
# With real MulmoClaude
|
|
38
|
+
DISCORD_BOT_TOKEN=... \
|
|
39
|
+
npx @mulmobridge/discord
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Environment Variables
|
|
43
|
+
|
|
44
|
+
| Variable | Required | Description |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `DISCORD_BOT_TOKEN` | Yes | Bot token from Developer Portal |
|
|
47
|
+
| `DISCORD_ALLOWED_CHANNELS` | No | CSV of channel IDs to restrict (empty = all) |
|
|
48
|
+
| `MULMOCLAUDE_API_URL` | No | Default `http://localhost:3001` |
|
|
49
|
+
| `MULMOCLAUDE_AUTH_TOKEN` | No | Bearer token (auto-read from workspace) |
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @mulmobridge/discord — Discord bridge for MulmoClaude.
|
|
3
|
+
//
|
|
4
|
+
// Required env vars:
|
|
5
|
+
// DISCORD_BOT_TOKEN — Bot token from Discord Developer Portal
|
|
6
|
+
//
|
|
7
|
+
// Optional:
|
|
8
|
+
// DISCORD_ALLOWED_CHANNELS — CSV of channel IDs (empty = allow all)
|
|
9
|
+
// MULMOCLAUDE_API_URL — default http://localhost:3001
|
|
10
|
+
// MULMOCLAUDE_AUTH_TOKEN — bearer token (or read from workspace)
|
|
11
|
+
import "dotenv/config";
|
|
12
|
+
import { Client, GatewayIntentBits, Partials } from "discord.js";
|
|
13
|
+
import { createBridgeClient } from "@mulmobridge/client";
|
|
14
|
+
const TRANSPORT_ID = "discord";
|
|
15
|
+
const MAX_DISCORD_LENGTH = 2000;
|
|
16
|
+
const token = process.env.DISCORD_BOT_TOKEN;
|
|
17
|
+
if (!token) {
|
|
18
|
+
console.error("DISCORD_BOT_TOKEN is required.\nSee README for setup instructions.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const allowedChannels = new Set((process.env.DISCORD_ALLOWED_CHANNELS ?? "")
|
|
22
|
+
.split(",")
|
|
23
|
+
.map((s) => s.trim())
|
|
24
|
+
.filter(Boolean));
|
|
25
|
+
const allowAll = allowedChannels.size === 0;
|
|
26
|
+
const discord = new Client({
|
|
27
|
+
intents: [
|
|
28
|
+
GatewayIntentBits.Guilds,
|
|
29
|
+
GatewayIntentBits.GuildMessages,
|
|
30
|
+
GatewayIntentBits.DirectMessages,
|
|
31
|
+
GatewayIntentBits.MessageContent,
|
|
32
|
+
],
|
|
33
|
+
// Partials.Channel is required to receive DM messageCreate events —
|
|
34
|
+
// DM channels are not cached by default in discord.js v14.
|
|
35
|
+
partials: [Partials.Channel],
|
|
36
|
+
});
|
|
37
|
+
const mulmo = createBridgeClient({ transportId: TRANSPORT_ID });
|
|
38
|
+
mulmo.onPush(async (ev) => {
|
|
39
|
+
try {
|
|
40
|
+
const channel = discord.channels.cache.get(ev.chatId) ??
|
|
41
|
+
(await discord.channels.fetch(ev.chatId).catch(() => null));
|
|
42
|
+
if (channel?.isTextBased() && "send" in channel) {
|
|
43
|
+
const sendable = channel;
|
|
44
|
+
await sendChunkedToSendable(sendable, ev.message);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.warn(`[discord] push: channel ${ev.chatId} not found or not text-based`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.error(`[discord] push send failed: ${err}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
discord.on("messageCreate", async (msg) => {
|
|
55
|
+
if (msg.author.bot)
|
|
56
|
+
return;
|
|
57
|
+
const channelId = msg.channelId;
|
|
58
|
+
const text = msg.content.trim();
|
|
59
|
+
if (!text)
|
|
60
|
+
return;
|
|
61
|
+
if (!allowAll && !allowedChannels.has(channelId))
|
|
62
|
+
return;
|
|
63
|
+
console.log(`[discord] message channel=${channelId} user=${msg.author.tag} len=${text.length}`);
|
|
64
|
+
try {
|
|
65
|
+
const ack = await mulmo.send(channelId, text);
|
|
66
|
+
if (ack.ok) {
|
|
67
|
+
await sendChunked(msg, ack.reply ?? "");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const status = ack.status ? ` (${ack.status})` : "";
|
|
71
|
+
await msg.reply(`Error${status}: ${ack.error ?? "unknown"}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error(`[discord] message handling failed: ${err}`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
async function sendChunked(msg, text) {
|
|
79
|
+
if (text.length === 0) {
|
|
80
|
+
await msg.reply("(empty reply)");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (let i = 0; i < text.length; i += MAX_DISCORD_LENGTH) {
|
|
84
|
+
const chunk = text.slice(i, i + MAX_DISCORD_LENGTH);
|
|
85
|
+
if (i === 0) {
|
|
86
|
+
await msg.reply(chunk);
|
|
87
|
+
}
|
|
88
|
+
else if ("send" in msg.channel) {
|
|
89
|
+
await msg.channel.send(chunk);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function sendChunkedToSendable(channel, text) {
|
|
94
|
+
const content = text.length === 0 ? "(empty reply)" : text;
|
|
95
|
+
for (let i = 0; i < content.length; i += MAX_DISCORD_LENGTH) {
|
|
96
|
+
await channel.send(content.slice(i, i + MAX_DISCORD_LENGTH));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
discord.once("ready", () => {
|
|
100
|
+
console.log("MulmoClaude Discord bridge");
|
|
101
|
+
console.log(`Logged in as ${discord.user?.tag}`);
|
|
102
|
+
console.log(`Channels: ${allowAll ? "(all)" : [...allowedChannels].join(", ")}`);
|
|
103
|
+
});
|
|
104
|
+
discord.login(token).catch((err) => {
|
|
105
|
+
console.error("Fatal:", err);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mulmobridge/discord",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Discord bridge for MulmoBridge — connect a Discord bot to MulmoClaude",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mulmobridge-discord": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"prepack": "yarn build",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"test": "echo no tests yet"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Receptron Team",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@mulmobridge/client": "^0.1.0",
|
|
25
|
+
"@mulmobridge/protocol": "^0.1.0",
|
|
26
|
+
"discord.js": "^14.26.3",
|
|
27
|
+
"dotenv": "^17.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^25.6.0",
|
|
31
|
+
"typescript": "^6.0.3"
|
|
32
|
+
}
|
|
33
|
+
}
|