Skip to content

Commit 4c02e80

Browse files
DevRohit06claude
andcommitted
docs: add serve command docs, serve_bot example, --profile usage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5a2b7e4 commit 4c02e80

3 files changed

Lines changed: 197 additions & 9 deletions

File tree

README.md

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,13 @@ graph TD
4141

4242
```mermaid
4343
graph LR
44-
A[discli listen --json] --> B[Events\nJSONL stream]
45-
B --> C[AI Agent\ne.g. Claude]
46-
C --> D[discli message reply]
47-
D --> E[Discord]
48-
A --> E
44+
A[discli serve] -->|stdout JSONL| B[AI Agent\ne.g. Claude]
45+
B -->|stdin JSONL| A
46+
A <-->|persistent| C[Discord Bot API]
4947
5048
style A fill:#5865F2,color:#fff
51-
style C fill:#D97706,color:#fff
52-
style E fill:#5865F2,color:#fff
49+
style B fill:#D97706,color:#fff
50+
style C fill:#5865F2,color:#fff
5351
```
5452

5553
```mermaid
@@ -229,6 +227,49 @@ discli --json listen --events messages
229227

230228
Supported event types: `messages`, `reactions`, `members`, `edits`, `deletes`
231229

230+
### Persistent Bot (serve)
231+
232+
`discli serve` keeps a persistent connection and communicates via stdin/stdout JSONL — ideal for building full Discord bots.
233+
234+
```bash
235+
# Start with slash commands and presence
236+
discli serve --slash-commands commands.json --status online --activity playing --activity-text "Helping"
237+
238+
# Filter by server
239+
discli serve --server "My Server"
240+
```
241+
242+
**Events (stdout):**
243+
```json
244+
{"event": "ready", "bot_id": "123", "bot_name": "MyBot#1234"}
245+
{"event": "message", "channel_id": "456", "author": "alice", "content": "hello", "mentions_bot": true, ...}
246+
{"event": "slash_command", "command": "paw", "args": {"message": "hi"}, "interaction_token": "abc123", ...}
247+
```
248+
249+
**Commands (stdin):**
250+
```json
251+
{"action": "send", "channel_id": "456", "content": "Hello!", "req_id": "1"}
252+
{"action": "reply", "channel_id": "456", "message_id": "789", "content": "Hi!", "req_id": "2"}
253+
{"action": "typing_start", "channel_id": "456"}
254+
{"action": "typing_stop", "channel_id": "456"}
255+
{"action": "presence", "status": "idle", "activity_type": "watching", "activity_text": "the logs"}
256+
```
257+
258+
**Streaming edits** (bot response builds in real-time, edited every 1.5s):
259+
```json
260+
{"action": "stream_start", "channel_id": "456", "reply_to": "789"}
261+
{"action": "stream_chunk", "stream_id": "s1", "content": "new tokens..."}
262+
{"action": "stream_end", "stream_id": "s1"}
263+
```
264+
265+
**Slash commands** are defined in a JSON file:
266+
```json
267+
[
268+
{"name": "paw", "description": "Talk to the bot", "params": [{"name": "message", "type": "string"}]},
269+
{"name": "new", "description": "Start a new session"}
270+
]
271+
```
272+
232273
## Security & Permissions
233274

234275
### Confirmation Prompts
@@ -251,11 +292,15 @@ Restrict which commands an agent can use:
251292
# List available profiles
252293
discli permission profiles
253294

254-
# Set a profile
295+
# Set a profile (persisted)
255296
discli permission set chat # Messages, reactions, threads only, no moderation
256297
discli permission set readonly # Can only read, no sending or deleting
257298
discli permission set moderation # Full access including kick/ban
258299
discli permission set full # Everything (default)
300+
301+
# Override per invocation (not persisted)
302+
discli --profile chat message send #general "hello"
303+
DISCLI_PROFILE=readonly discli message list #general
259304
```
260305

261306
| Profile | Can Send | Can Delete | Can Kick/Ban | Can Manage Channels |
@@ -334,6 +379,7 @@ Ready-to-run examples in the [`examples/`](examples/) directory:
334379

335380
| Example | Description |
336381
|---------|-------------|
382+
| [`serve_bot.py`](examples/serve_bot.py) | Full bot using `discli serve` with streaming responses and slash commands |
337383
| [`claude_agent.py`](examples/claude_agent.py) | AI support agent powered by Claude Agent SDK with persistent session |
338384
| [`support_agent.py`](examples/support_agent.py) | Keyword-based support bot that replies to @mentions |
339385
| [`thread_support_agent.py`](examples/thread_support_agent.py) | Creates a thread per support request and continues conversations inside |
@@ -379,7 +425,7 @@ discli/
379425
│ ├── config.py # Token storage (~/.discli/config.json)
380426
│ ├── security.py # Permissions, audit logging, rate limiting
381427
│ ├── utils.py # Output formatting, resolvers
382-
│ └── commands/ # Command groups (message, channel, role, etc.)
428+
│ └── commands/ # Command groups (message, channel, serve, etc.)
383429
├── agents/
384430
│ └── discord-agent.md # Full command reference for AI agents
385431
├── examples/ # Ready-to-run agent examples

agents/discord-agent.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ discli listen --events messages,reactions,members,edits,deletes
8686
discli listen --server "server name" --channel "#channel"
8787
```
8888

89+
### Persistent Bot (serve)
90+
`discli serve` stays connected and uses stdin/stdout JSONL for bidirectional communication.
91+
```bash
92+
discli serve --slash-commands commands.json --status online
93+
```
94+
**stdin commands:** `send`, `reply`, `edit`, `delete`, `typing_start`, `typing_stop`, `presence`, `reaction_add`, `reaction_remove`, `stream_start`, `stream_chunk`, `stream_end`, `interaction_followup`
95+
96+
**stdout events:** `ready`, `message`, `slash_command`, `message_edit`, `message_delete`, `reaction_add`, `reaction_remove`, `member_join`, `member_remove`, `response`, `error`
97+
8998
## Important Rules
9099

91100
### JSON Output

examples/serve_bot.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""
2+
Echo bot using discli serve.
3+
4+
Demonstrates the bidirectional JSONL protocol: reads events from stdout,
5+
sends commands via stdin, and uses streaming edits for responses.
6+
7+
Requirements:
8+
pip install discord-cli-agent
9+
10+
Usage:
11+
discli config set token YOUR_BOT_TOKEN
12+
python examples/serve_bot.py
13+
"""
14+
15+
import asyncio
16+
import json
17+
import sys
18+
from pathlib import Path
19+
20+
SLASH_COMMANDS = [
21+
{"name": "echo", "description": "Echo your message back",
22+
"params": [{"name": "message", "type": "string", "description": "Text to echo"}]},
23+
{"name": "ping", "description": "Check if the bot is alive"},
24+
]
25+
26+
27+
async def main():
28+
# Write slash commands to temp file
29+
import tempfile
30+
slash_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
31+
json.dump(SLASH_COMMANDS, slash_file)
32+
slash_file.close()
33+
34+
# Start discli serve
35+
proc = await asyncio.create_subprocess_exec(
36+
"discli", "--json", "serve",
37+
"--slash-commands", slash_file.name,
38+
"--status", "online",
39+
"--activity", "listening",
40+
"--activity-text", "your messages",
41+
stdin=asyncio.subprocess.PIPE,
42+
stdout=asyncio.subprocess.PIPE,
43+
stderr=asyncio.subprocess.PIPE,
44+
)
45+
46+
req_counter = 0
47+
48+
async def send_cmd(action: str, **kwargs) -> None:
49+
nonlocal req_counter
50+
req_counter += 1
51+
cmd = {"action": action, "req_id": str(req_counter), **kwargs}
52+
proc.stdin.write((json.dumps(cmd) + "\n").encode())
53+
await proc.stdin.drain()
54+
55+
async def stream_response(channel_id: str, reply_to: str, text: str) -> None:
56+
"""Send a response using streaming edits (simulates token-by-token)."""
57+
await send_cmd("stream_start", channel_id=channel_id, reply_to=reply_to)
58+
# Wait for stream_id in response
59+
await asyncio.sleep(0.5)
60+
# Stream word by word
61+
words = text.split()
62+
for i, word in enumerate(words):
63+
chunk = word if i == 0 else " " + word
64+
await send_cmd("stream_chunk", stream_id=stream_response.last_stream_id, content=chunk)
65+
await asyncio.sleep(0.1) # Simulate thinking
66+
await send_cmd("stream_end", stream_id=stream_response.last_stream_id)
67+
68+
stream_response.last_stream_id = None
69+
70+
print("Starting serve bot...")
71+
72+
try:
73+
while True:
74+
line = await proc.stdout.readline()
75+
if not line:
76+
break
77+
78+
data = json.loads(line.decode().strip())
79+
event = data.get("event")
80+
81+
if event == "ready":
82+
print(f"Bot connected as {data['bot_name']}")
83+
84+
elif event == "response":
85+
# Track stream IDs from responses
86+
if "stream_id" in data:
87+
stream_response.last_stream_id = data["stream_id"]
88+
89+
elif event == "message":
90+
# Skip bot's own messages
91+
if data.get("is_bot"):
92+
continue
93+
94+
content = data["content"]
95+
channel_id = data["channel_id"]
96+
message_id = data["message_id"]
97+
author = data["author"]
98+
99+
if data.get("mentions_bot"):
100+
print(f"[{author}] {content}")
101+
# Show typing, then reply
102+
await send_cmd("typing_start", channel_id=channel_id)
103+
await asyncio.sleep(1)
104+
await send_cmd("typing_stop", channel_id=channel_id)
105+
await send_cmd("reply",
106+
channel_id=channel_id,
107+
message_id=message_id,
108+
content=f"You said: {content}")
109+
110+
elif event == "slash_command":
111+
command = data["command"]
112+
interaction_token = data["interaction_token"]
113+
print(f"Slash command: /{command} from {data['user']}")
114+
115+
if command == "echo":
116+
msg = data["args"].get("message", "(empty)")
117+
await send_cmd("interaction_followup",
118+
interaction_token=interaction_token,
119+
content=f"Echo: {msg}")
120+
elif command == "ping":
121+
await send_cmd("interaction_followup",
122+
interaction_token=interaction_token,
123+
content="Pong!")
124+
125+
except KeyboardInterrupt:
126+
print("\nShutting down...")
127+
finally:
128+
proc.terminate()
129+
Path(slash_file.name).unlink(missing_ok=True)
130+
131+
132+
if __name__ == "__main__":
133+
asyncio.run(main())

0 commit comments

Comments
 (0)