@acedatacloud/skills 2026.722.2 → 2026.722.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.722.
|
|
3
|
+
"version": "2026.722.3",
|
|
4
4
|
"description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
package/skills/discord/SKILL.md
CHANGED
|
@@ -1,74 +1,121 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: discord
|
|
3
|
-
description:
|
|
3
|
+
description: Work with the user's Discord account. Two modes, auto-selected by how they connected — OAuth (read-only identity + server list) or User Token / BYOC (full personal-account actions: list channels, read messages, send & reply). Use when the user mentions Discord, asks which servers they are in, or wants to read/post in a channel as themselves.
|
|
4
4
|
when_to_use: |
|
|
5
|
-
Trigger
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Trigger for anything on the user's connected Discord account. What you can do
|
|
6
|
+
depends on how they connected:
|
|
7
|
+
- OAuth connection → read-only: their identity (username, avatar, email) and
|
|
8
|
+
the list of servers (guilds) they belong to. No channel messages.
|
|
9
|
+
- User Token (BYOC) connection → full personal-account actions: list channels,
|
|
10
|
+
read recent messages, and send / reply in a channel as the user. Writes are
|
|
11
|
+
gated behind an explicit confirmation.
|
|
9
12
|
connections: [discord]
|
|
10
13
|
allowed_tools: [Bash]
|
|
11
14
|
license: Apache-2.0
|
|
12
15
|
metadata:
|
|
13
16
|
author: acedatacloud
|
|
14
|
-
version: "
|
|
17
|
+
version: "2.0"
|
|
15
18
|
---
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
with `curl + jq`. The user's OAuth bearer token is in `$DISCORD_TOKEN`;
|
|
19
|
-
every call needs it as `Authorization: Bearer $DISCORD_TOKEN`. Use the
|
|
20
|
-
versioned base URL `https://discord.com/api/v10`.
|
|
20
|
+
# discord — work with the user's Discord account
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
token expired or the connection was revoked — tell the user to re-connect
|
|
25
|
-
Discord at `auth.acedata.cloud/user/connections`. A `429` carries a
|
|
26
|
-
`retry_after` (seconds) field — sleep that long, then retry; never
|
|
27
|
-
parallelize.
|
|
22
|
+
This skill has **two modes**, picked automatically by which credential the
|
|
23
|
+
connector injected. Check the environment first and follow the matching section:
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`/guilds/{id}/channels`, `/channels/...`, or `/guilds/{id}/members` — they
|
|
35
|
-
return 401/403 with a user OAuth token. If the user asks for those, say it
|
|
36
|
-
needs a Discord bot integration, which isn't set up.
|
|
37
|
-
|
|
38
|
-
## Recipes
|
|
39
|
-
|
|
40
|
-
### Verify auth + identity (always run first)
|
|
25
|
+
- **`$DISCORD_USER_TOKEN` is set** → **User Token (BYOC) mode**: full
|
|
26
|
+
personal-account actions via the `scripts/discord_user.py` CLI (list channels, read
|
|
27
|
+
messages, send / reply). This acts as the user's **real account**.
|
|
28
|
+
- **only `$DISCORD_TOKEN` is set** → **OAuth mode**: read-only identity + server
|
|
29
|
+
list via `curl`. Channel messages are **not** available in this mode.
|
|
41
30
|
|
|
42
31
|
```sh
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
32
|
+
if [ -n "$DISCORD_USER_TOKEN" ]; then echo "mode: user-token (full)"; \
|
|
33
|
+
elif [ -n "$DISCORD_TOKEN" ]; then echo "mode: oauth (read-only)"; \
|
|
34
|
+
else echo "no Discord connection — connect at https://auth.acedata.cloud/user/connections"; fi
|
|
46
35
|
```
|
|
47
36
|
|
|
48
|
-
|
|
37
|
+
Both tokens are **secret — full account access. Never echo or print them.** On
|
|
38
|
+
`401`, the token expired or was revoked — tell the user to reconnect at
|
|
39
|
+
`auth.acedata.cloud/user/connections`. On `429`, sleep the `retry_after`
|
|
40
|
+
seconds, then retry; never parallelize.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## User Token (BYOC) mode — full actions
|
|
45
|
+
|
|
46
|
+
When `$DISCORD_USER_TOKEN` is set, drive the user's real account through
|
|
47
|
+
[`discord.py-self`](https://github.com/dolfies/discord.py-self) via the CLI at
|
|
48
|
+
`scripts/discord_user.py` (run with `python3`). It logs in with the token only (no
|
|
49
|
+
gateway) and prints JSON.
|
|
50
|
+
|
|
51
|
+
`discord.py-self` is preinstalled in the hosted sandbox (import name `discord`).
|
|
52
|
+
Do **not** `pip install` it at runtime; if import fails, report that the sandbox
|
|
53
|
+
image is missing the dependency and stop.
|
|
54
|
+
|
|
55
|
+
**Writes (`send` / `reply`) are gated by a trailing `--confirm`.** Without it
|
|
56
|
+
they dry-run and print what they *would* send. **Confirm the exact channel and
|
|
57
|
+
content with the user before sending** — it's irreversible and public. Only
|
|
58
|
+
append `--confirm` (as the very last argument) once the user approves.
|
|
49
59
|
|
|
50
60
|
```sh
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
# identity (always run first)
|
|
62
|
+
python3 scripts/discord_user.py whoami
|
|
63
|
+
# list the servers (guilds) the account is in
|
|
64
|
+
python3 scripts/discord_user.py guilds
|
|
65
|
+
# list text channels in a server
|
|
66
|
+
python3 scripts/discord_user.py channels --guild 1133012399448928276
|
|
67
|
+
# read recent messages in a channel
|
|
68
|
+
python3 scripts/discord_user.py messages --channel 1133012400174534792 --limit 20
|
|
69
|
+
# send — dry-run first, then re-run with a trailing --confirm
|
|
70
|
+
python3 scripts/discord_user.py send --channel 1133012400174534792 --text "hello"
|
|
71
|
+
python3 scripts/discord_user.py send --channel 1133012400174534792 --text "hello" --confirm
|
|
72
|
+
# reply to a specific message
|
|
73
|
+
python3 scripts/discord_user.py reply \
|
|
74
|
+
--channel 1133012400174534792 --message 987654321098765432 --text "on it" --confirm
|
|
54
75
|
```
|
|
55
76
|
|
|
56
|
-
|
|
57
|
-
`
|
|
77
|
+
`--confirm` is honored ONLY as the final argument (one strip), so a message body
|
|
78
|
+
that merely contains the text `--confirm` can never silently trigger a send. IDs
|
|
79
|
+
are numeric Discord snowflakes. A proxy can be forced via `DISCORD_PROXY` (falls
|
|
80
|
+
back to `HTTPS_PROXY` / `ALL_PROXY`) if the sandbox needs one to reach
|
|
81
|
+
`discord.com`.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## OAuth mode — read-only
|
|
86
|
+
|
|
87
|
+
When only `$DISCORD_TOKEN` is set, drive the
|
|
88
|
+
[Discord API](https://discord.com/developers/docs/reference) with `curl + jq`.
|
|
89
|
+
Auth header is `Authorization: Bearer $DISCORD_TOKEN`; base URL
|
|
90
|
+
`https://discord.com/api/v10`.
|
|
91
|
+
|
|
92
|
+
**Scope is read-only `identify` + `email` + `guilds`.** This mode can ONLY read
|
|
93
|
+
the account's identity and its guild list. It CANNOT read/send channel messages,
|
|
94
|
+
list a guild's channels or members, or manage anything — `/guilds/{id}/channels`,
|
|
95
|
+
`/channels/...`, `/guilds/{id}/members` return 401/403 with an OAuth token. If the
|
|
96
|
+
user needs those, they must connect Discord with a **User Token** (the BYOC mode
|
|
97
|
+
above), or use a **Discord Bot** (`discordbot` connection) to act as a bot.
|
|
58
98
|
|
|
59
99
|
```sh
|
|
100
|
+
# identity (always run first)
|
|
101
|
+
curl -sS -H "Authorization: Bearer $DISCORD_TOKEN" \
|
|
102
|
+
"https://discord.com/api/v10/users/@me" \
|
|
103
|
+
| jq '{id, username, global_name, email, avatar}'
|
|
104
|
+
|
|
105
|
+
# the servers (guilds) the user is in (add ?with_counts=true for member counts)
|
|
60
106
|
curl -sS -H "Authorization: Bearer $DISCORD_TOKEN" \
|
|
61
107
|
"https://discord.com/api/v10/users/@me/guilds?with_counts=true" \
|
|
62
108
|
| jq 'map({id, name, owner, members: .approximate_member_count})'
|
|
63
109
|
```
|
|
64
110
|
|
|
111
|
+
---
|
|
112
|
+
|
|
65
113
|
## Notes
|
|
66
114
|
|
|
67
|
-
- A "server" in the UI is a "guild" in the API
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`?after=<last_guild_id>`.
|
|
115
|
+
- A "server" in the UI is a "guild" in the API; messages live in channels inside
|
|
116
|
+
guilds. In User Token mode: `guilds` → `channels --guild <id>` → act on a
|
|
117
|
+
channel id. Don't invent ids.
|
|
118
|
+
- In OAuth mode, `owner: true` means the user owns that guild. Guild icon URL
|
|
119
|
+
(when `icon` is non-null): `https://cdn.discordapp.com/icons/<guild_id>/<icon>.png`
|
|
120
|
+
(use `.gif` if the hash starts with `a_`). The guild list paginates at 200;
|
|
121
|
+
paginate with `?after=<last_guild_id>` if you ever hit it.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
discord_user.py — act on Discord with the user's own account (BYOC user-token path).
|
|
4
|
+
|
|
5
|
+
This is the **BYOC** half of the `discord` skill. It runs only when the user
|
|
6
|
+
connected Discord with a personal user token (env ``DISCORD_USER_TOKEN``); the
|
|
7
|
+
OAuth half of the skill uses plain ``curl`` and is documented in SKILL.md.
|
|
8
|
+
|
|
9
|
+
Drives Discord's user API through `discord.py-self`
|
|
10
|
+
(https://github.com/dolfies/discord.py-self). This acts as the user's REAL
|
|
11
|
+
account, so every state-changing command (send / reply) is GATED by a trailing
|
|
12
|
+
``--confirm`` — without it, the command dry-runs.
|
|
13
|
+
|
|
14
|
+
The connector injects the token as env var ``DISCORD_USER_TOKEN``. It is full
|
|
15
|
+
account access — NEVER echo or print it.
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
python3 discord_user.py whoami
|
|
19
|
+
python3 discord_user.py guilds
|
|
20
|
+
python3 discord_user.py channels --guild 1133012399448928276
|
|
21
|
+
python3 discord_user.py messages --channel 1133012400174534792 --limit 20
|
|
22
|
+
python3 discord_user.py send --channel 1133012400174534792 --text "hello" --confirm
|
|
23
|
+
python3 discord_user.py reply --channel 1133012400174534792 --message 987654321 --text "on it" --confirm
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import asyncio
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
_RAW = sys.argv[1:]
|
|
35
|
+
# --confirm is honored ONLY as the last token, and only one is stripped, so a
|
|
36
|
+
# message body that merely contains "--confirm" can never silently confirm a write.
|
|
37
|
+
CONFIRM = bool(_RAW) and _RAW[-1] == "--confirm"
|
|
38
|
+
ARGV = _RAW[:-1] if CONFIRM else list(_RAW)
|
|
39
|
+
|
|
40
|
+
# State-changing commands — dry-run unless the invocation ends with --confirm.
|
|
41
|
+
GATED = {"send", "reply"}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def out(obj) -> None:
|
|
45
|
+
print(json.dumps(obj, ensure_ascii=False, indent=2, default=str))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def die(msg: str, code: int = 1) -> None:
|
|
49
|
+
out({"error": msg})
|
|
50
|
+
sys.exit(code)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_token() -> str:
|
|
54
|
+
tok = os.environ.get("DISCORD_USER_TOKEN")
|
|
55
|
+
if not tok:
|
|
56
|
+
die("DISCORD_USER_TOKEN is not set — connect Discord (User Token) at "
|
|
57
|
+
"https://auth.acedata.cloud/user/connections, then retry.")
|
|
58
|
+
return tok.strip()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def proxy() -> str | None:
|
|
62
|
+
return (
|
|
63
|
+
os.environ.get("DISCORD_PROXY")
|
|
64
|
+
or os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
|
|
65
|
+
or os.environ.get("ALL_PROXY") or os.environ.get("all_proxy")
|
|
66
|
+
or None
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def channel_id(args) -> int:
|
|
71
|
+
try:
|
|
72
|
+
return int(args.channel)
|
|
73
|
+
except (TypeError, ValueError):
|
|
74
|
+
die("--channel must be a numeric Discord channel id")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def guild_id(args) -> int:
|
|
78
|
+
try:
|
|
79
|
+
return int(args.guild)
|
|
80
|
+
except (TypeError, ValueError):
|
|
81
|
+
die("--guild must be a numeric Discord guild (server) id")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def messageable_channel(client, args):
|
|
85
|
+
"""Fetch a channel and confirm it can hold messages (not a category/forum)."""
|
|
86
|
+
import discord.abc
|
|
87
|
+
ch = await client.fetch_channel(channel_id(args))
|
|
88
|
+
if not isinstance(ch, discord.abc.Messageable):
|
|
89
|
+
die(f"channel {ch.id} is a {ch.type} channel, which can't hold messages — "
|
|
90
|
+
"pass a text/voice channel id (see `channels --guild <id>`)")
|
|
91
|
+
return ch
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ── commands ──────────────────────────────────────────────────────────────
|
|
95
|
+
async def cmd_whoami(client, args):
|
|
96
|
+
u = client.user
|
|
97
|
+
out({"id": str(u.id), "username": u.name, "global_name": getattr(u, "global_name", None),
|
|
98
|
+
"bot": u.bot})
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def cmd_guilds(client, args):
|
|
102
|
+
guilds = await client.fetch_guilds(with_counts=True)
|
|
103
|
+
out([{"id": str(g.id), "name": g.name,
|
|
104
|
+
"members": getattr(g, "approximate_member_count", None)} for g in guilds])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def cmd_channels(client, args):
|
|
108
|
+
import discord
|
|
109
|
+
guild = await client.fetch_guild(guild_id(args))
|
|
110
|
+
channels = await guild.fetch_channels()
|
|
111
|
+
text = [c for c in channels if isinstance(c, (discord.TextChannel,))]
|
|
112
|
+
out([{"id": str(c.id), "name": c.name, "type": str(c.type)} for c in text])
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def cmd_messages(client, args):
|
|
116
|
+
ch = await messageable_channel(client, args)
|
|
117
|
+
msgs = [m async for m in ch.history(limit=args.limit)]
|
|
118
|
+
out([{"id": str(m.id), "author": m.author.name, "author_id": str(m.author.id),
|
|
119
|
+
"ts": m.created_at, "content": m.content} for m in msgs])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def cmd_send(client, args):
|
|
123
|
+
ch = await messageable_channel(client, args)
|
|
124
|
+
if not CONFIRM:
|
|
125
|
+
out({"dry_run": True, "would_send": {"channel": str(ch.id),
|
|
126
|
+
"channel_name": getattr(ch, "name", None), "text": args.text},
|
|
127
|
+
"hint": "re-run with a trailing --confirm to actually send"})
|
|
128
|
+
return
|
|
129
|
+
msg = await ch.send(args.text)
|
|
130
|
+
out({"sent": True, "channel": str(ch.id), "message_id": str(msg.id)})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def cmd_reply(client, args):
|
|
134
|
+
import discord
|
|
135
|
+
ch = await messageable_channel(client, args)
|
|
136
|
+
try:
|
|
137
|
+
target = int(args.message)
|
|
138
|
+
except (TypeError, ValueError):
|
|
139
|
+
die("--message must be a numeric Discord message id")
|
|
140
|
+
if not CONFIRM:
|
|
141
|
+
out({"dry_run": True, "would_reply": {"channel": str(ch.id),
|
|
142
|
+
"reply_to": str(target), "text": args.text},
|
|
143
|
+
"hint": "re-run with a trailing --confirm to actually send"})
|
|
144
|
+
return
|
|
145
|
+
ref = discord.MessageReference(message_id=target, channel_id=ch.id,
|
|
146
|
+
guild_id=getattr(ch, "guild", None) and ch.guild.id)
|
|
147
|
+
msg = await ch.send(args.text, reference=ref)
|
|
148
|
+
out({"sent": True, "channel": str(ch.id), "message_id": str(msg.id),
|
|
149
|
+
"reply_to": str(target)})
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
HANDLERS = {
|
|
153
|
+
"whoami": cmd_whoami, "guilds": cmd_guilds, "channels": cmd_channels,
|
|
154
|
+
"messages": cmd_messages, "send": cmd_send, "reply": cmd_reply,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
159
|
+
p = argparse.ArgumentParser(prog="discord_user.py", description="Discord user-token CLI")
|
|
160
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
161
|
+
|
|
162
|
+
sub.add_parser("whoami", help="verify the token and show the logged-in account")
|
|
163
|
+
sub.add_parser("guilds", help="list the servers (guilds) the account is in")
|
|
164
|
+
|
|
165
|
+
sp = sub.add_parser("channels", help="list text channels in a server")
|
|
166
|
+
sp.add_argument("--guild", required=True)
|
|
167
|
+
|
|
168
|
+
sp = sub.add_parser("messages", help="read recent messages in a channel")
|
|
169
|
+
sp.add_argument("--channel", required=True)
|
|
170
|
+
sp.add_argument("--limit", type=int, default=20)
|
|
171
|
+
|
|
172
|
+
sp = sub.add_parser("send", help="send a message to a channel (GATED by trailing --confirm)")
|
|
173
|
+
sp.add_argument("--channel", required=True)
|
|
174
|
+
sp.add_argument("--text", required=True)
|
|
175
|
+
|
|
176
|
+
sp = sub.add_parser("reply", help="reply to a message (GATED by trailing --confirm)")
|
|
177
|
+
sp.add_argument("--channel", required=True)
|
|
178
|
+
sp.add_argument("--message", required=True)
|
|
179
|
+
sp.add_argument("--text", required=True)
|
|
180
|
+
return p
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def run(args) -> None:
|
|
184
|
+
import discord
|
|
185
|
+
client = discord.Client(proxy=proxy())
|
|
186
|
+
try:
|
|
187
|
+
await client.login(load_token())
|
|
188
|
+
await HANDLERS[args.cmd](client, args)
|
|
189
|
+
finally:
|
|
190
|
+
# close() is safe even if login() failed (no session opened).
|
|
191
|
+
await client.close()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def main() -> None:
|
|
195
|
+
args = build_parser().parse_args(ARGV)
|
|
196
|
+
if args.cmd in GATED and not CONFIRM:
|
|
197
|
+
# Still resolve the channel for the dry-run preview, but never write.
|
|
198
|
+
pass
|
|
199
|
+
try:
|
|
200
|
+
import discord
|
|
201
|
+
from discord.errors import (
|
|
202
|
+
LoginFailure, Forbidden, NotFound, HTTPException,
|
|
203
|
+
)
|
|
204
|
+
except Exception as e: # discord.py-self not importable
|
|
205
|
+
die(f"discord.py-self is not available in the sandbox image: {e}. "
|
|
206
|
+
"Deploy the sandbox skill dependencies image; do not pip-install it at runtime.")
|
|
207
|
+
try:
|
|
208
|
+
asyncio.run(run(args))
|
|
209
|
+
except LoginFailure as e:
|
|
210
|
+
die("auth failed — the Discord user token is wrong or expired. Reconnect "
|
|
211
|
+
f"at https://auth.acedata.cloud/user/connections. ({e})")
|
|
212
|
+
except Forbidden as e:
|
|
213
|
+
die(f"forbidden by Discord (no access to that channel/server, or blocked): {e}")
|
|
214
|
+
except NotFound as e:
|
|
215
|
+
die(f"not found — check the channel / guild / message id: {e}")
|
|
216
|
+
except HTTPException as e:
|
|
217
|
+
# 429 rate limits and other API errors land here.
|
|
218
|
+
status = getattr(e, "status", None)
|
|
219
|
+
if status == 429:
|
|
220
|
+
retry = getattr(e, "retry_after", None)
|
|
221
|
+
die(f"rate limited by Discord — wait {retry or 'a bit'}s and retry; never parallelize. ({e})")
|
|
222
|
+
die(f"Discord API error (HTTP {status}): {e}")
|
|
223
|
+
except Exception as e:
|
|
224
|
+
die(f"Discord request failed ({type(e).__name__}: {e}). Likely an expired "
|
|
225
|
+
"token — reconnect at https://auth.acedata.cloud/user/connections.")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__":
|
|
229
|
+
main()
|