Skip to content
Draft
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
52 changes: 52 additions & 0 deletions examples/plugins/cao-discord/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# cao-discord

`cao-discord` is a CAO plugin that forwards inter-agent messages to a Discord channel through a webhook, rendering your CAO workflow as a live group chat of bots in Discord.

## Install

From the repository root, inside the CAO development virtual environment:

```bash
uv pip install -e examples/plugins/cao-discord
```

## Example `.env`

```dotenv
CAO_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1234567890/abcdef...
CAO_DISCORD_TIMEOUT_SECONDS=5.0
```

## Setup

1. Create a webhook in Discord: Channel -> Edit Channel -> Integrations -> Webhooks -> New Webhook -> Copy URL.
2. Install the plugin:
```bash
uv pip install -e examples/plugins/cao-discord

# Or use if you prefer uv tool install
# (from project root)
uv tool install --reinstall . \
--with-editable ./examples/plugins/cao-discord
```
3. Create a `.env` file in the directory where you will run `cao-server`, or export the variables in your shell:
```dotenv
CAO_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1234567890/abcdef...
CAO_DISCORD_TIMEOUT_SECONDS=5.0
```
4. Start the server:
```bash
cao-server
```
5. Launch a multi-agent workflow such as `cao flow ...` and watch the Discord channel for forwarded inter-agent messages.

## Configuration

| Variable | Required | Description |
| --- | --- | --- |
| `CAO_DISCORD_WEBHOOK_URL` | Yes | Full Discord webhook URL in the form `https://discord.com/api/webhooks/{id}/{token}`. |
| `CAO_DISCORD_TIMEOUT_SECONDS` | No | HTTP timeout in seconds for webhook POSTs. Defaults to `5.0`. |

## Troubleshooting

If `CAO_DISCORD_WEBHOOK_URL` is missing, `PluginRegistry.load()` logs a warning during `cao-server` startup and skips registering the plugin for the lifetime of that server process.
Empty file.
74 changes: 74 additions & 0 deletions examples/plugins/cao-discord/cao_discord/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Discord plugin lifecycle, hook handling, and webhook dispatch."""

import logging
import os

import httpx
from dotenv import find_dotenv, load_dotenv

from cli_agent_orchestrator.clients.database import get_terminal_metadata
from cli_agent_orchestrator.plugins import PostSendMessageEvent, hook
from cli_agent_orchestrator.plugins.base import CaoPlugin

logger = logging.getLogger(__name__)


class DiscordPlugin(CaoPlugin):
"""Discord webhook plugin for CAO inter-agent messaging events."""

_webhook_url: str
_client: httpx.AsyncClient

async def setup(self) -> None:
"""Load configuration and initialize the HTTP client."""

load_dotenv(find_dotenv(usecwd=True))

webhook_url = os.environ.get("CAO_DISCORD_WEBHOOK_URL")
if not webhook_url:
raise RuntimeError(
"CAO_DISCORD_WEBHOOK_URL is not set. "
"Set it in the environment or in a .env file before starting cao-server."
)

self._webhook_url = webhook_url
timeout = float(os.environ.get("CAO_DISCORD_TIMEOUT_SECONDS", "5.0"))
self._client = httpx.AsyncClient(timeout=timeout)

async def teardown(self) -> None:
"""Close the HTTP client when setup completed successfully."""

if hasattr(self, "_client"):
await self._client.aclose()

@hook("post_send_message")
async def on_post_send_message(self, event: PostSendMessageEvent) -> None:
"""Forward post-send-message events to the configured Discord webhook."""

display_name = self._resolve_display_name(event.sender)
await self._post(username=display_name, content=event.message)

def _resolve_display_name(self, terminal_id: str) -> str:
"""Resolve a human-friendly sender name from terminal metadata."""

metadata = get_terminal_metadata(terminal_id)
if metadata is None:
return terminal_id
return metadata.get("tmux_window") or terminal_id

async def _post(self, *, username: str, content: str) -> None:
"""Send a Discord webhook payload and swallow all HTTP failures."""

try:
response = await self._client.post(
self._webhook_url,
json={"username": username, "content": content},
)
if response.status_code >= 400:
logger.warning(
"Discord webhook POST failed: %s %s",
response.status_code,
response.text[:200],
)
except httpx.HTTPError as exc:
logger.warning("Discord webhook POST raised: %s", exc)
14 changes: 14 additions & 0 deletions examples/plugins/cao-discord/env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# cao-discord plugin configuration
#
# Copy this file to `.env` in the directory where you launch `cao-server` from
# (or any parent directory — python-dotenv walks upward from CWD), then fill in
# your webhook URL. Real shell environment variables override values set here.
#
# Create a webhook: Discord channel -> Edit Channel -> Integrations -> Webhooks
# -> New Webhook -> Copy Webhook URL.

# Required. Full Discord webhook URL.
CAO_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/REPLACE_ID/REPLACE_TOKEN

# Optional. HTTP timeout (seconds) for webhook POSTs. Default: 5.0
#CAO_DISCORD_TIMEOUT_SECONDS=5.0
17 changes: 17 additions & 0 deletions examples/plugins/cao-discord/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "cao-discord"
version = "0.1.0"
description = "Discord webhook plugin for CLI Agent Orchestrator"
requires-python = ">=3.10"
dependencies = [
"cli-agent-orchestrator",
"httpx>=0.27",
"python-dotenv>=1.0",
]

[project.entry-points."cao.plugins"]
discord = "cao_discord.plugin:DiscordPlugin"
Empty file.
Loading