English | Traditional Chinese
Docs: https://pi-roundtable.wayneh.tw Source and issues: https://github.com/wayne930242/pi-roundtable
A Discord agent server on Pi. Each AI agent has its own channel and conversation in your Discord server. They share tools and memory. You can extend the bot with TypeScript plugins.
- Built for one owner and one Discord server. You can allow others to talk to the agents, but that setup and its risks are the operator's.
- Bun only. The package ships its TypeScript source, so there is no build step.
- MIT licensed.
- Bun 1.3 or newer.
- PostgreSQL.
The project
initcreates has adocker-compose.ymlthat runs one. - A Discord bot: an application with a bot user, the Message Content intent turned on, and an invitation to your server.
- A model login: the API key of the provider of the model you choose (
ANTHROPIC_API_KEYforanthropic/...), or a login made with Pi. - An address that reaches the process from the internet, such as a tunnel, because Discord fetches the agents' avatars from it.
npx pi-roundtable init my-bot # or: bunx pi-roundtable init my-bot
cd my-bot
bun install
docker compose up -d # PostgreSQL, matching .env.example
cp .env.example .env # then fill it in
bunx roundtable doctor
bunx roundtable startinit writes a working project without asking for secrets.
It refuses to write anything when Bun is missing or too old, or when a file it would create already exists.
.env.example says where each value comes from.
Bun loads .env by itself, and .gitignore keeps it out of Git.
| Variable | What it is |
|---|---|
DISCORD_TOKEN |
The bot's token, from the application's Bot page |
DISCORD_GUILD_ID, DISCORD_ENTRY_CHANNEL_ID |
The server and the channel where the coordinating agent lives (turn on Developer Mode, then right-click to copy ids) |
OWNER_ID, OWNER_NAME |
You: the one person who can change everything |
DATABASE_URL |
PostgreSQL; the default matches docker-compose.yml |
MODEL |
The agents' model, <provider>/<id> |
PUBLIC_URL |
The address that reaches this process from the internet |
bunx roundtable doctor checks, in order, and prints each check as passed or failed with how to fix it:
- Bun's version.
.envhas a value for every variable.env.examplelists.roundtable.config.tsagainst its schema, naming the failing key.- Every plugin loads, and no two share a name.
- Whether a plugin fills the
imagesslot. The check passes with or without one; agents without an image provider get avatars generated from their display names. - PostgreSQL is reachable and migratable.
- The Discord token is valid, the bot is in your server, the Message Content intent is on, and the bot has the permissions it needs in the entry channel (including Pin Messages). When the bot is not in the server, the fix is an invitation link that asks for exactly those permissions.
- The model login exists.
PUBLIC_URLis a well-formed address; with--reachableit also has to answer, which is only true while the bot runs.
It exits non-zero on any failure and changes nothing it checked. A fresh project fails only on the credentials you have not entered yet, and says which.
bunx roundtable start runs the checks that need no network, stops with the same message doctor prints when one fails, and otherwise starts the bot.
The running bot gives the agents in agents.ts their channels, and /roundtable schedule list shows their schedules.
On SIGTERM or SIGINT it finishes running work before it stops.
roundtable add plugin <name> creates plugins/<name>.ts and its test and lists it in roundtable.config.ts.
roundtable add package <spec> does the same for a Pi package from npm: it installs the package and writes a plugin that loads its extensions and selects its tools.
A plugin is an object with a name and a setup function that returns what it adds; this one gives every agent a tool:
import { definePlugin, defineTool } from "pi-roundtable";
import { Type } from "typebox";
export const hello = definePlugin({
name: "hello",
setup: () => ({
tools: [
defineTool({
name: "hello_greet",
description: "Greet someone by name. Call it when asked to say hello.",
parameters: Type.Object({ who: Type.String() }),
minTier: "member",
run: ({ who }) => `Hello, ${who}!`,
}),
],
}),
});You can test it without Discord or PostgreSQL:
import { expect, test } from "bun:test";
import { testPlugin } from "pi-roundtable/testing";
import { hello } from "./hello.ts";
test("hello greets", async () => {
const harness = await testPlugin(hello);
expect(await harness.runTool("hello_greet", { who: "Ada" })).toBe("Hello, Ada!");
await harness.stop();
});The plugin guide explains every part a plugin can add (tools, prompt sections, agents, events, services, migrations, providers, slash commands, HTTP routes, and more), the order things start and stop in, and every startup error with its fix.
Its examples live in examples/, and the test suite runs each of them.
pi-roundtable/kit supplies claim, tool and presentation helpers and type-only names for the context’s existing services.
pi-roundtable/discord supplies the slash-command registrar, owner-command and panel helpers, and the agent panel.
It is the entry that names discord.js types (pi-roundtable/testing names a few, through testHost's composed commands).
Both follow the main entry's versioning: before 1.0, breaking changes come in minor releases and are listed in the changelog.
The separate package pi-roundtable-mcp connects the bot to the MCP ecosystem in both directions, with two plugins:
mcpConnectors: you add an MCP server in Discord with a private form, such as Notion, a calendar, or anything that speaks MCP over HTTP. Your code then gives its tools to the agents you choose. A ContextForge gateway that you run keeps each server and its token.remoteMcp: an agent outside Discord sends your agent a message over MCP and reads the answer. It can also use the Discord channels you grant, with only the operations you choose.
bun add pi-roundtable-mcpIt works with pi-roundtable 0.4 and 0.5, and its README lists every option.
roundtable.config.ts holds the settings and the list of plugins.
An unknown key is an error that names the closest known one.
locale sets the language of the bot's Discord text: en by default, or zh-TW.
export default {
// ...
locale: "zh-TW",
timeZone: "Europe/Berlin", // the zone schedules and time stamps use; default UTC
plugins: [hello],
} satisfies RoundtableConfig;| Command | What it does |
|---|---|
roundtable init [dir] |
Creates a project in dir (default: the current directory) |
roundtable doctor [--reachable] |
Checks the setup and says how to fix what is wrong |
roundtable start |
Runs the checks that need no network, then the bot |
roundtable add plugin <name> |
Adds plugins/<name>.ts and its test, and lists it in the config |
roundtable add package <spec> |
Installs a Pi package with bun add and adds a plugin that loads it and gives its tools to every agent turn |
CHANGELOG.md lists every change to the package's exported names. MIT.