Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
*.pyc
__pycache__
.pytest_cache
!nemoclaw/dist/
128 changes: 128 additions & 0 deletions docs/deployment/set-up-discord-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
title:
page: "Set Up the NemoClaw Discord Bridge for Remote Agent Chat"
nav: "Set Up Discord Bridge"
description: "Forward messages between Discord and the sandboxed OpenClaw agent."
keywords: ["nemoclaw discord bridge", "discord bot openclaw agent"]
topics: ["generative_ai", "ai_agents"]
tags: ["openclaw", "openshell", "discord", "deployment", "nemoclaw"]
content:
type: how_to
difficulty: intermediate
audience: ["developer", "engineer"]
status: published
---

<!--
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# Set Up the NemoClaw Discord Bridge for Remote Agent Chat

Forward messages between a Discord bot and the OpenClaw agent running inside the sandbox.
`nemoclaw start` manages the Discord bridge as an auxiliary service.

## Prerequisites

Before you begin, ensure the following are in place:

- A running NemoClaw sandbox, either local or remote.
- A Discord bot token from the [Discord Developer Portal](https://discord.com/developers/applications).

## Create a Discord Bot

Visit the [Discord Developer Portal](https://discord.com/developers/applications) and create a new application.

1. Click "New Application" and give it a name.
2. Go to the "Bot" tab and click "Add Bot".
3. Under the TOKEN section, click "Copy" to copy your bot token.
4. Keep the token secure and do not share it.

## Add the Bot to Your Server

Configure OAuth2 permissions and invite the bot to your Discord server.

1. In the Developer Portal, go to the "OAuth2" tab.
2. Under "SCOPES", select `bot`.
3. Under "PERMISSIONS", select at least:
- Send Messages
- Read Messages and View Channels
- Read Message History

4. Copy the generated URL and open it in your browser to invite the bot to your Discord server.

## Set the Environment Variable

Export the bot token as an environment variable:

```console
$ export DISCORD_BOT_TOKEN=<your-bot-token>
```

## Start Auxiliary Services

Start the Discord bridge and other auxiliary services:

```console
$ nemoclaw start
```

The `start` command launches the following services:

- The Discord bridge forwards messages between Discord and the agent.
- The cloudflared tunnel provides external access to the sandbox.

The `start` command launches the Discord bridge only when you set the `DISCORD_BOT_TOKEN` environment variable.

## Verify the Services

Check that the Discord bridge is running:

```console
$ nemoclaw status
```

The output shows the status of all auxiliary services.

## Send a Message

Open Discord and send a message to your bot.
You can either mention the bot directly in a channel or send a direct message.
The bridge forwards the message to the OpenClaw agent inside the sandbox.
The agent returns its response to the channel.

## Restrict Access by Channel

To restrict which Discord channels the agent can respond in, set the `DISCORD_CHANNEL_ID` environment variable:

```console
$ export DISCORD_CHANNEL_ID=<your-channel-id>
$ nemoclaw start
```

The bot only responds to messages in the specified channel.

## Restrict Access by User

To restrict which Discord users can interact with the agent, set the `ALLOWED_USER_IDS` environment variable to a comma-separated list of Discord user IDs:

```console
$ export ALLOWED_USER_IDS="123456789,987654321"
$ nemoclaw start
```

## Stop the Services

To stop the Discord bridge and all other auxiliary services:

```console
$ nemoclaw stop
```

## Next Steps

Explore these guides for more advanced configurations:

- [Deploy NemoClaw to a Remote GPU Instance](deploy-to-remote-gpu.md) for remote deployment with Discord support.
- [Commands](../reference/commands.md) for the full `start` and `stop` command reference.
189 changes: 189 additions & 0 deletions scripts/discord-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#!/usr/bin/env node
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// NemoClaw Discord Bridge
//
// Forwards Discord messages to the OpenClaw gateway running inside the
// NemoClaw sandbox. The gateway handles agent sessions, system prompts,
// SOUL.md loading, and memory.
//
// Env:
// DISCORD_BOT_TOKEN - Discord bot token
// SANDBOX_NAME - sandbox name (default: nemoclaw)
// DISCORD_GUILD_ID - allowed guild (optional, accepts all if unset)
// DISCORD_CHANNEL_ID - channel to listen on (optional)
// ALLOWED_USER_IDS - comma-separated Discord user IDs (optional)
// GATEWAY_TOKEN - OpenClaw gateway auth token

const { Client, GatewayIntentBits, Partials } = require("discord.js");
const { spawn } = require("child_process");
const path = require("path");

const TOKEN = process.env.DISCORD_BOT_TOKEN;
const SANDBOX = process.env.SANDBOX_NAME || "nemoclaw";
const GUILD_ID = process.env.DISCORD_GUILD_ID || "";
const CHANNEL_ID = process.env.DISCORD_CHANNEL_ID || "";
const ALLOWED_USERS = process.env.ALLOWED_USER_IDS
? process.env.ALLOWED_USER_IDS.split(",").map((s) => s.trim())
: null;

if (!TOKEN) {
console.error("DISCORD_BOT_TOKEN required");
process.exit(1);
}

const activeLocks = new Set();

const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages,
],
partials: [Partials.Channel],
});

// -- Run agent via gateway inside sandbox --

function runAgentViaGateway(message, sessionId) {
return new Promise((resolve) => {
const escaped = message.replace(/'/g, "'\\''");

// Use nemoclaw-start to ensure gateway is running, then run agent through it
const cmd = `nemoclaw-start openclaw agent --agent main --local -m '${escaped}' --session-id '${sessionId}'`;

const proc = spawn("ssh", ["-T", "openshell-nemoclaw", cmd], {
timeout: 180000,
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, PATH: `${process.env.HOME}/.local/bin:${process.env.PATH}` },
});

let stdout = "";
let stderr = "";

proc.stdout.on("data", (d) => (stdout += d.toString()));
proc.stderr.on("data", (d) => (stderr += d.toString()));

proc.on("close", (code) => {
const lines = stdout.split("\n");
const responseLines = lines.filter(
(l) =>
!l.startsWith("Setting up NemoClaw") &&
!l.startsWith("[plugins]") &&
!l.startsWith("[gateway]") &&
!l.startsWith("[auto-pair]") &&
!l.startsWith("(node:") &&
!l.includes("NemoClaw registered") &&
!l.includes("UNDICI-EHPA") &&
!l.includes("openclaw gateway") &&
!l.includes("auto-pair watcher") &&
!l.includes("Local UI:") &&
!l.includes("Remote UI:") &&
!/^[\s\u2502\u250c\u2514\u2500\u2501]+$/.test(l) &&
!/^\s*\|/.test(l) &&
l.trim() !== "",
);

const response = responseLines.join("\n").trim();

if (response) {
resolve(response);
} else if (code !== 0) {
resolve(`Agent exited with code ${code}. ${stderr.trim().slice(0, 500)}`);
} else {
resolve("(no response)");
}
});

proc.on("error", (err) => resolve(`Error: ${err.message}`));
});
}

// -- Discord message handler --

client.on("messageCreate", async (msg) => {
if (msg.author.bot) return;
if (GUILD_ID && msg.guild && msg.guild.id !== GUILD_ID) return;
if (CHANNEL_ID && msg.channel.id !== CHANNEL_ID) return;
if (ALLOWED_USERS && !ALLOWED_USERS.includes(msg.author.id)) return;

const isMention = msg.mentions.has(client.user);
const isDM = !msg.guild;
const isDesignatedChannel = CHANNEL_ID && msg.channel.id === CHANNEL_ID;

if (!isMention && !isDM && !isDesignatedChannel) return;

let content = msg.content.replace(/<@!?\d+>/g, "").trim();
if (!content) {
await msg.reply("Send me a message and I'll run it through the NemoClaw agent.");
return;
}

const userId = msg.author.id;
if (activeLocks.has(userId)) {
await msg.reply("Still working on your last message. Hang tight.");
return;
}

activeLocks.add(userId);
// Use a persistent session per user so context carries across messages
const sessionId = `discord-${userId}`;

console.log(`[${msg.channel.name || "DM"}] ${msg.author.username}: ${content.slice(0, 80)}`);

const typingInterval = setInterval(() => {
msg.channel.sendTyping().catch(() => {});
}, 5000);
msg.channel.sendTyping().catch(() => {});

try {
const response = await runAgentViaGateway(content, sessionId);
clearInterval(typingInterval);

console.log(`[${msg.channel.name || "DM"}] agent: ${response.slice(0, 80)}...`);

const chunks = [];
for (let i = 0; i < response.length; i += 1950) {
chunks.push(response.slice(i, i + 1950));
}

for (let i = 0; i < chunks.length; i++) {
if (i === 0) {
await msg.reply(chunks[i]);
} else {
await msg.channel.send(chunks[i]);
}
}
} catch (err) {
clearInterval(typingInterval);
console.error(`Error: ${err.message}`);
await msg.reply(`Error: ${err.message}`).catch(() => {});
} finally {
activeLocks.delete(userId);
}
});

client.once("ready", () => {
console.log("");
console.log(" +---------------------------------------------------------+");
console.log(" | NemoClaw Discord Bridge |");
console.log(" | |");
console.log(` | Bot: ${(client.user.tag + " ").slice(0, 44)}|`);
console.log(` | Sandbox: ${(SANDBOX + " ").slice(0, 44)}|`);
console.log(" | Mode: gateway (nemoclaw-start + openclaw agent) |");
console.log(" | Guild: " + (GUILD_ID ? "restricted" : "all") + " |");
console.log(" | |");
console.log(" | Messages forwarded through OpenClaw gateway with |");
console.log(" | full session context, SOUL.md, and memory support. |");
console.log(" +---------------------------------------------------------+");
console.log("");

client.user.setPresence({
status: "online",
activities: [{ name: "NemoClaw on DGX Spark", type: 0 }],
});
});

client.login(TOKEN);
15 changes: 15 additions & 0 deletions scripts/start-discord-bridge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash
# Load NemoClaw Discord bot token from pass and start the bridge

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

token="$(pass show api-keys/nemoclaw-discord-token 2>/dev/null | head -1)"
if [ $? -ne 0 ] || [ -z "$token" ]; then
echo "ERROR: Could not load NemoClaw Discord bot token from pass"
exit 1
fi

export DISCORD_BOT_TOKEN="$token"
export PATH="$HOME/.local/bin:$PATH"

exec node "$SCRIPT_DIR/discord-bridge.js"
Loading