@acedatacloud/skills 2026.725.0 → 2026.726.1
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 +1 -1
- package/skills/oschina/SKILL.md +85 -0
- package/skills/oschina/scripts/oschina.py +287 -0
- package/skills/reddit/SKILL.md +68 -17
- package/skills/reddit/scripts/reddit.py +211 -13
- package/skills/reddit/tests/test_reddit.py +183 -0
- package/skills/segmentfault/SKILL.md +85 -0
- package/skills/segmentfault/scripts/segmentfault.py +302 -0
- package/skills/yuque/SKILL.md +104 -0
- package/skills/yuque/scripts/yuque.py +311 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.726.1",
|
|
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",
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oschina
|
|
3
|
+
description: Read the connected 开源中国 / OSChina (my.oschina.net) account and create Markdown blog drafts with the user's own login cookies (BYOC). Use when the user mentions 开源中国 / OSChina, wants to save a 开源中国 draft, or asks who their connected 开源中国 account is.
|
|
4
|
+
when_to_use: |
|
|
5
|
+
Trigger for the user's 开源中国 (my.oschina.net) account driven by their own
|
|
6
|
+
login cookie: show the connected account, or turn Markdown into a 开源中国
|
|
7
|
+
blog draft. 开源中国 exposes no publish API, so this skill stops at a draft
|
|
8
|
+
and hands the user the editor URL. Writes are gated behind explicit
|
|
9
|
+
confirmation.
|
|
10
|
+
connections: [oschina]
|
|
11
|
+
allowed_tools: [Bash]
|
|
12
|
+
license: Apache-2.0
|
|
13
|
+
metadata:
|
|
14
|
+
author: acedatacloud
|
|
15
|
+
version: "1.0"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# oschina — read & draft on 开源中国 via your own cookies
|
|
19
|
+
|
|
20
|
+
Drives the user's **real** 开源中国 account through the same
|
|
21
|
+
`apiv1.oschina.net` endpoints the site uses, authenticated by the login cookie
|
|
22
|
+
they captured with the ACE extension. No browser, no third-party deps — just
|
|
23
|
+
`urllib`.
|
|
24
|
+
|
|
25
|
+
The connector injects the cookie jar as a JSON env var `$OSCHINA_COOKIES`.
|
|
26
|
+
Never print it.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python3 "$SKILL_DIR/scripts/oschina.py" whoami
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If `$SKILL_DIR` points at a different skill loaded in the same turn, resolve
|
|
33
|
+
this skill's directory explicitly before running the commands below.
|
|
34
|
+
|
|
35
|
+
## Important: draft only
|
|
36
|
+
|
|
37
|
+
**开源中国 has no public publish API.** Every open-source client for this
|
|
38
|
+
platform (including the upstream adapter this skill is modelled on) stops at
|
|
39
|
+
draft creation. This skill does the same: it saves the Markdown as a draft and
|
|
40
|
+
returns the editor URL. Tell the user plainly that they must open that URL and
|
|
41
|
+
click publish themselves — do not claim the article was published.
|
|
42
|
+
|
|
43
|
+
## Verify the connection first
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 "$SKILL_DIR/scripts/oschina.py" whoami
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If this fails with an auth error, the cookie has expired. Ask the user to
|
|
50
|
+
reconnect at `https://auth.acedata.cloud/user/connections` rather than retrying.
|
|
51
|
+
|
|
52
|
+
## Create a draft — GATED
|
|
53
|
+
|
|
54
|
+
Prepare the complete Markdown in a file. The first call is always a dry run and
|
|
55
|
+
does not write anything.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Dry run — shows exactly what would be written.
|
|
59
|
+
python3 "$SKILL_DIR/scripts/oschina.py" draft \
|
|
60
|
+
--title "标题" --content-file /tmp/article.md
|
|
61
|
+
|
|
62
|
+
# Actually create the draft after the user confirms.
|
|
63
|
+
python3 "$SKILL_DIR/scripts/oschina.py" draft \
|
|
64
|
+
--title "标题" --content-file /tmp/article.md --confirm
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`--confirm` is valid only as the final argument. Show the title and full
|
|
68
|
+
content to the user before writing.
|
|
69
|
+
|
|
70
|
+
## Gotchas
|
|
71
|
+
|
|
72
|
+
- Content is sent as Markdown (`contentType: 1`). Do not pre-render it to HTML.
|
|
73
|
+
- Images referenced by external URL are not re-hosted. If the source host
|
|
74
|
+
blocks hotlinking they will not render; mention this when the article has
|
|
75
|
+
images.
|
|
76
|
+
- `--catalog` takes a 开源中国 catalog id; the default `0` uses the account's
|
|
77
|
+
default catalog. If the API rejects the catalog, ask the user for the right
|
|
78
|
+
id rather than guessing.
|
|
79
|
+
- Do not retry a timed-out write automatically — the outcome may be unknown and
|
|
80
|
+
a retry can create a duplicate draft.
|
|
81
|
+
|
|
82
|
+
## Record the output
|
|
83
|
+
|
|
84
|
+
This skill only produces drafts, so do **not** call `publish_artifact`. Report
|
|
85
|
+
the returned `draft_id` and `edit_url` to the user instead.
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
oschina — read & publish on 开源中国 (my.oschina.net) with the user's own login
|
|
4
|
+
cookies (BYOC). Standard-library only (urllib), no third-party deps, so it runs
|
|
5
|
+
in the bare sandbox without an image change.
|
|
6
|
+
|
|
7
|
+
The connector injects the user's cookie jar as a JSON env var ``OSCHINA_COOKIES``
|
|
8
|
+
— a list of ``{name, value, domain, ...}`` dicts captured by the ACE extension.
|
|
9
|
+
|
|
10
|
+
Read commands run directly. ``publish`` is GATED: without a trailing
|
|
11
|
+
``--confirm`` it only dry-runs. ``--confirm`` is honored ONLY as the last
|
|
12
|
+
argument, so a title/content that merely contains "--confirm" can never silently
|
|
13
|
+
go live.
|
|
14
|
+
|
|
15
|
+
NOTE: 开源中国 exposes no public "publish" API — only draft creation. This CLI
|
|
16
|
+
creates a draft and returns its editor URL; the user finishes publishing in the
|
|
17
|
+
开源中国 editor.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
python3 oschina.py whoami
|
|
21
|
+
python3 oschina.py draft --title T --content-file a.md --confirm
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import gzip
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
import urllib.error
|
|
32
|
+
import urllib.parse
|
|
33
|
+
import urllib.request
|
|
34
|
+
|
|
35
|
+
UA = (
|
|
36
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
|
37
|
+
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
|
38
|
+
)
|
|
39
|
+
PLATFORM = "oschina"
|
|
40
|
+
API = "https://apiv1.oschina.net/oschinapi"
|
|
41
|
+
ORIGIN = "https://my.oschina.net"
|
|
42
|
+
MAX_CONTENT_BYTES = 10 * 1024 * 1024
|
|
43
|
+
|
|
44
|
+
_RAW = sys.argv[1:]
|
|
45
|
+
CONFIRM = bool(_RAW) and _RAW[-1] == "--confirm"
|
|
46
|
+
ARGV = _RAW[:-1] if CONFIRM else list(_RAW)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
50
|
+
"""Refuse redirects outright.
|
|
51
|
+
|
|
52
|
+
add_unredirected_header only protects the Cookie; urllib copies every other
|
|
53
|
+
header onto the redirected request, so a 30x to a foreign host could hand
|
|
54
|
+
over any auth header we add later.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_OPENER = urllib.request.build_opener(_NoRedirect())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def out(obj) -> None:
|
|
65
|
+
print(json.dumps(obj, ensure_ascii=False, indent=2, default=str))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def die(msg: str, code: int = 1) -> None:
|
|
69
|
+
out({"error": msg})
|
|
70
|
+
sys.exit(code)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Cookie jar (shared pattern across the cookie-BYOC skills) ────────
|
|
74
|
+
|
|
75
|
+
def load_cookies() -> list:
|
|
76
|
+
env = f"{PLATFORM.upper()}_COOKIES"
|
|
77
|
+
raw = os.environ.get(env)
|
|
78
|
+
if not raw:
|
|
79
|
+
die(f"{env} is not set — connect 开源中国 at "
|
|
80
|
+
f"https://auth.acedata.cloud/user/connections, then retry.")
|
|
81
|
+
try:
|
|
82
|
+
jar = json.loads(raw)
|
|
83
|
+
except json.JSONDecodeError as e:
|
|
84
|
+
die(f"{env} is not valid JSON: {e}")
|
|
85
|
+
if not isinstance(jar, list):
|
|
86
|
+
die(f"{env} must be a JSON list of cookies, got {type(jar).__name__}")
|
|
87
|
+
return jar
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _domain_matches(host: str, domain: str) -> bool:
|
|
91
|
+
d = domain.lstrip(".").lower()
|
|
92
|
+
h = host.lower()
|
|
93
|
+
return not d or h == d or h.endswith("." + d)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cookie_header(jar: list, url: str) -> str:
|
|
97
|
+
host = urllib.parse.urlsplit(url).hostname or ""
|
|
98
|
+
host_in_scope = any(
|
|
99
|
+
c.get("domain") and _domain_matches(host, str(c["domain"])) for c in jar
|
|
100
|
+
)
|
|
101
|
+
parts = []
|
|
102
|
+
for c in jar:
|
|
103
|
+
name, value = c.get("name"), c.get("value")
|
|
104
|
+
if not name or value is None:
|
|
105
|
+
continue
|
|
106
|
+
domain = c.get("domain")
|
|
107
|
+
if domain:
|
|
108
|
+
if not _domain_matches(host, str(domain)):
|
|
109
|
+
continue
|
|
110
|
+
elif not host_in_scope:
|
|
111
|
+
continue
|
|
112
|
+
parts.append(f"{name}={value}")
|
|
113
|
+
return "; ".join(parts)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def request(method: str, url: str, jar: list, *, headers=None, body=None, write: bool = False):
|
|
117
|
+
hdrs = {
|
|
118
|
+
"User-Agent": UA,
|
|
119
|
+
"Accept": "*/*",
|
|
120
|
+
"Accept-Language": "zh-CN,zh;q=0.9",
|
|
121
|
+
"Origin": ORIGIN,
|
|
122
|
+
"Referer": f"{ORIGIN}/",
|
|
123
|
+
}
|
|
124
|
+
if headers:
|
|
125
|
+
hdrs.update(headers)
|
|
126
|
+
data = None
|
|
127
|
+
if body is not None:
|
|
128
|
+
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
129
|
+
hdrs.setdefault("Content-Type", "application/json")
|
|
130
|
+
req = urllib.request.Request(url, data=data, headers=hdrs, method=method)
|
|
131
|
+
# Unredirected → the cookie is not re-sent if the API 30x-redirects to a
|
|
132
|
+
# different host (e.g. a login page), so the jar never leaks off-site.
|
|
133
|
+
req.add_unredirected_header("Cookie", cookie_header(jar, url))
|
|
134
|
+
try:
|
|
135
|
+
with _OPENER.open(req, timeout=30) as resp:
|
|
136
|
+
raw = resp.read()
|
|
137
|
+
if resp.headers.get("Content-Encoding") == "gzip":
|
|
138
|
+
raw = gzip.decompress(raw)
|
|
139
|
+
return resp.status, raw.decode("utf-8", "replace")
|
|
140
|
+
except urllib.error.HTTPError as e:
|
|
141
|
+
if e.code in (301, 302, 303, 307, 308):
|
|
142
|
+
die(f"开源中国 redirected {method} {url} — not followed, so no "
|
|
143
|
+
f"credential left oschina.net. You are most likely logged out; "
|
|
144
|
+
f"reconnect at https://auth.acedata.cloud/user/connections."
|
|
145
|
+
+ (" This was a WRITE: its outcome is UNKNOWN — check your "
|
|
146
|
+
"drafts before retrying." if write else ""))
|
|
147
|
+
raw = e.read()
|
|
148
|
+
try:
|
|
149
|
+
if e.headers.get("Content-Encoding") == "gzip":
|
|
150
|
+
raw = gzip.decompress(raw)
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
return e.code, raw.decode("utf-8", "replace")
|
|
154
|
+
except urllib.error.URLError as e:
|
|
155
|
+
if write:
|
|
156
|
+
die(f"开源中国 write {method} {url} did not return a result "
|
|
157
|
+
f"({e.reason}); the outcome is UNKNOWN. Check your 开源中国 "
|
|
158
|
+
f"drafts before retrying so you do not create a duplicate.")
|
|
159
|
+
die(f"network error reaching {url}: {e.reason}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def api_call(method: str, path: str, jar: list, *, body=None, write: bool = False):
|
|
163
|
+
"""Unwrap the {success, message, code, result} envelope, dying on failure.
|
|
164
|
+
开源中国 returns HTTP 200 even for logical errors."""
|
|
165
|
+
url = f"{API}{path}"
|
|
166
|
+
status, text = request(method, url, jar, body=body, write=write)
|
|
167
|
+
try:
|
|
168
|
+
env = json.loads(text)
|
|
169
|
+
except json.JSONDecodeError:
|
|
170
|
+
if write:
|
|
171
|
+
die(f"开源中国 write to {path} returned a non-JSON response ({status}); "
|
|
172
|
+
f"the outcome is UNKNOWN. Check your drafts before retrying. "
|
|
173
|
+
f"Body: {text[:200]}")
|
|
174
|
+
die(f"non-JSON response ({status}) from {path}: {text[:300]}")
|
|
175
|
+
if not isinstance(env, dict):
|
|
176
|
+
die(f"unexpected response from {path}: {text[:300]}")
|
|
177
|
+
if not env.get("success"):
|
|
178
|
+
msg = str(env.get("message") or "")
|
|
179
|
+
code = env.get("code")
|
|
180
|
+
if code in (40001, 401, 403) or "未登录" in msg or "登录" in msg:
|
|
181
|
+
die(f"auth failed (code={code}: {msg}) — cookie likely expired. "
|
|
182
|
+
f"Reconnect at https://auth.acedata.cloud/user/connections.")
|
|
183
|
+
die(f"开源中国 API error on {path} (code={code}): {msg}")
|
|
184
|
+
return env.get("result")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ── commands ────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
def os_me(jar) -> dict:
|
|
190
|
+
me = api_call("GET", "/user/myDetails", jar)
|
|
191
|
+
if not isinstance(me, dict) or not me.get("userId"):
|
|
192
|
+
die("could not read 开源中国 profile (cookie expired?)")
|
|
193
|
+
return me
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def cmd_whoami(jar, _args):
|
|
197
|
+
me = os_me(jar)
|
|
198
|
+
vo = me.get("userVo") or {}
|
|
199
|
+
uid = me.get("userId")
|
|
200
|
+
out({
|
|
201
|
+
"user_id": str(uid),
|
|
202
|
+
"name": vo.get("name"),
|
|
203
|
+
"url": f"{ORIGIN}/u/{uid}",
|
|
204
|
+
"avatar": vo.get("portraitUrl"),
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def read_content(args) -> str:
|
|
209
|
+
content = args.content
|
|
210
|
+
if args.content_file:
|
|
211
|
+
try:
|
|
212
|
+
with open(args.content_file, encoding="utf-8") as f:
|
|
213
|
+
content = f.read()
|
|
214
|
+
except OSError as e:
|
|
215
|
+
die(f"cannot read --content-file: {e}")
|
|
216
|
+
if content is None:
|
|
217
|
+
die("provide --content-file <path.md> or --content <markdown>")
|
|
218
|
+
if len(content.encode("utf-8")) > MAX_CONTENT_BYTES:
|
|
219
|
+
die("content exceeds the 10 MiB safety limit")
|
|
220
|
+
return content
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def cmd_draft(jar, args):
|
|
224
|
+
if not args.title:
|
|
225
|
+
die("--title is required")
|
|
226
|
+
content = read_content(args)
|
|
227
|
+
|
|
228
|
+
if not CONFIRM:
|
|
229
|
+
out({
|
|
230
|
+
"dry_run": True, "command": "draft", "platform": PLATFORM,
|
|
231
|
+
"title": args.title, "catalog": args.catalog,
|
|
232
|
+
"private": not args.public,
|
|
233
|
+
"content_characters": len(content),
|
|
234
|
+
"note": "开源中国 content is Markdown. Re-run with --confirm as the LAST "
|
|
235
|
+
"argument to actually create the draft. 开源中国 exposes no "
|
|
236
|
+
"publish API — finish publishing in the 开源中国 editor.",
|
|
237
|
+
})
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
me = os_me(jar)
|
|
241
|
+
uid = me.get("userId")
|
|
242
|
+
result = api_call("POST", "/api/draft/save_draft", jar, write=True, body={
|
|
243
|
+
"title": args.title,
|
|
244
|
+
"user": int(uid),
|
|
245
|
+
"content": content,
|
|
246
|
+
"contentType": 1, # 1 = Markdown, 2 = HTML
|
|
247
|
+
"catalog": args.catalog,
|
|
248
|
+
"originUrl": "",
|
|
249
|
+
"privacy": not args.public,
|
|
250
|
+
"disableComment": False,
|
|
251
|
+
})
|
|
252
|
+
draft_id = result.get("id") if isinstance(result, dict) else None
|
|
253
|
+
if not draft_id:
|
|
254
|
+
die(f"draft creation failed: {str(result)[:300]}")
|
|
255
|
+
out({
|
|
256
|
+
"ok": True,
|
|
257
|
+
"draft_only": True,
|
|
258
|
+
"draft_id": str(draft_id),
|
|
259
|
+
"edit_url": f"{ORIGIN}/u/{uid}/blog/write/draft/{draft_id}",
|
|
260
|
+
"note": "Draft saved. 开源中国 has no publish API — open edit_url to publish.",
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
COMMANDS = {
|
|
265
|
+
"whoami": cmd_whoami,
|
|
266
|
+
"draft": cmd_draft,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def main() -> None:
|
|
271
|
+
p = argparse.ArgumentParser(prog="oschina.py", description="开源中国 cookie CLI")
|
|
272
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
273
|
+
sub.add_parser("whoami", help="show the logged-in account")
|
|
274
|
+
sp = sub.add_parser("draft", help="create a draft article (GATED by trailing --confirm)")
|
|
275
|
+
sp.add_argument("--title")
|
|
276
|
+
sp.add_argument("--content", help="Markdown content inline")
|
|
277
|
+
sp.add_argument("--content-file", help="path to a Markdown file")
|
|
278
|
+
sp.add_argument("--catalog", type=int, default=0, help="开源中国 catalog id (0 = default)")
|
|
279
|
+
sp.add_argument("--public", action="store_true",
|
|
280
|
+
help="mark the draft non-private; it still needs publishing in the editor")
|
|
281
|
+
args = p.parse_args(ARGV)
|
|
282
|
+
jar = load_cookies()
|
|
283
|
+
COMMANDS[args.command](jar, args)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
main()
|
package/skills/reddit/SKILL.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reddit
|
|
3
|
-
description:
|
|
3
|
+
description: Search subreddits, read rules, reply to threads, and submit posts (link or text) to Reddit using either official OAuth or your own Reddit login cookies. Use when the user mentions Reddit, posting to a subreddit, replying or commenting on a thread, submitting a link or self-post, or checking their Reddit profile / submissions.
|
|
4
4
|
when_to_use: |
|
|
5
|
-
Trigger when the user wants to
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
before
|
|
5
|
+
Trigger when the user wants to find relevant Reddit threads, reply to a
|
|
6
|
+
post or comment, submit a post to a subreddit (link or self/text post),
|
|
7
|
+
or read their own Reddit identity and submissions. Commenting and posting
|
|
8
|
+
are public and subject to that subreddit's rules / karma requirements —
|
|
9
|
+
confirm the target and the exact text before writing.
|
|
10
10
|
connections: [reddit]
|
|
11
11
|
allowed_tools: [Bash, publish_artifact]
|
|
12
12
|
license: Apache-2.0
|
|
13
13
|
metadata:
|
|
14
14
|
author: acedatacloud
|
|
15
|
-
version: "2.
|
|
15
|
+
version: "2.1"
|
|
16
16
|
connection_method_preferences:
|
|
17
17
|
reddit/reddit: [oauth, cookie]
|
|
18
18
|
---
|
|
@@ -54,6 +54,35 @@ python3 "$R" whoami
|
|
|
54
54
|
python3 "$R" submissions --limit 10
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## Find threads and check the rules
|
|
58
|
+
|
|
59
|
+
`search` returns each hit's `fullname` (`t3_…`), which is what `comment --parent`
|
|
60
|
+
takes. `subreddit-info` returns the subreddit's rules — **read them before
|
|
61
|
+
writing anything there.**
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
R="${SKILL_DIR:-}/scripts/reddit.py"; [ -f "$R" ] || R=$(find /tmp -maxdepth 8 -path '*/skills/*/reddit/scripts/reddit.py' -print -quit 2>/dev/null)
|
|
65
|
+
[ -f "$R" ] || { echo "reddit script not found (SKILL_DIR=$SKILL_DIR)" >&2; exit 1; }
|
|
66
|
+
|
|
67
|
+
python3 "$R" search --query "suno api" --time week --limit 10
|
|
68
|
+
python3 "$R" search --query "image generation api" --subreddit SideProject --sort new
|
|
69
|
+
python3 "$R" subreddit-info --subreddit SideProject
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Reply to a thread — GATED
|
|
73
|
+
|
|
74
|
+
`--parent` accepts a fullname (`t3_…` post, `t1_…` comment) or a reddit.com
|
|
75
|
+
permalink. **Show the target thread and the exact reply text, then obtain
|
|
76
|
+
explicit confirmation.** Without a trailing `--confirm` this is a dry run.
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
R="${SKILL_DIR:-}/scripts/reddit.py"; [ -f "$R" ] || R=$(find /tmp -maxdepth 8 -path '*/skills/*/reddit/scripts/reddit.py' -print -quit 2>/dev/null)
|
|
80
|
+
[ -f "$R" ] || { echo "reddit script not found (SKILL_DIR=$SKILL_DIR)" >&2; exit 1; }
|
|
81
|
+
|
|
82
|
+
python3 "$R" comment --parent t3_1r9yrtn --body-file reply.md
|
|
83
|
+
python3 "$R" comment --parent t3_1r9yrtn --body-file reply.md --confirm
|
|
84
|
+
```
|
|
85
|
+
|
|
57
86
|
## Submit a post — GATED
|
|
58
87
|
|
|
59
88
|
Posting is public. **Always show the subreddit, final title and final body/URL,
|
|
@@ -79,25 +108,47 @@ contains the text `--confirm` can never trigger a write.
|
|
|
79
108
|
## Safety and failure handling
|
|
80
109
|
|
|
81
110
|
- Never print `REDDIT_COOKIES`, `REDDIT_TOKEN`, `reddit_session` or the modhash.
|
|
82
|
-
- Do not vote, send private messages, evade bans,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
111
|
+
- Do not vote, send private messages, evade bans, or cross-post identical
|
|
112
|
+
content. This skill intentionally exposes none of those operations.
|
|
113
|
+
- **Never post the same reply text to more than one thread.** Reddit treats
|
|
114
|
+
repeated identical comments as spam and actions the account, not the comment.
|
|
115
|
+
Write each reply against the specific thread it answers.
|
|
116
|
+
- Follow each subreddit's rules — run `subreddit-info` first. Account age, karma
|
|
117
|
+
and flair requirements can reject a post or comment; report the rejection
|
|
118
|
+
without exposing Reddit's raw authenticated response, which may contain
|
|
119
|
+
reflected credential material.
|
|
88
120
|
- Do not retry a write automatically. A timeout may occur after Reddit accepted
|
|
89
121
|
it, and replaying could create a duplicate.
|
|
90
|
-
- Respect rate limits and never bulk-submit. Use `r/test` only
|
|
91
|
-
end-to-end validation.
|
|
122
|
+
- Respect rate limits and never bulk-submit or bulk-comment. Use `r/test` only
|
|
123
|
+
for a deliberate end-to-end validation.
|
|
92
124
|
- Cookie mode drives Reddit's first-party web JSON endpoints and may drift when
|
|
93
125
|
Reddit changes its site. Report unexpected HTML or route errors as upstream
|
|
94
126
|
drift instead of guessing another private endpoint.
|
|
95
127
|
|
|
128
|
+
## Self-promotion — read this before marketing anything
|
|
129
|
+
|
|
130
|
+
Reddit bans accounts for promotion patterns, not for individual links. When the
|
|
131
|
+
user's goal is promoting their own product:
|
|
132
|
+
|
|
133
|
+
- **Answer the question first.** A reply must stand on its own as useful even if
|
|
134
|
+
the product link were removed. If it does not, do not send it.
|
|
135
|
+
- **Disclose the affiliation** ("I build X") — undisclosed promotion is the
|
|
136
|
+
fastest route to a ban.
|
|
137
|
+
- **One link at most, at the end**, and never in a post title.
|
|
138
|
+
- **Do not blanket-reply.** A handful of genuinely relevant threads beats volume,
|
|
139
|
+
and volume is exactly what spam detection keys on.
|
|
140
|
+
- If a subreddit has a dedicated self-promo / "Showoff" thread, use it — that is
|
|
141
|
+
the sanctioned channel.
|
|
142
|
+
- A low-karma or new account will have posts auto-removed by AutoModerator in
|
|
143
|
+
most active subreddits. Check `whoami` karma and warn the user rather than
|
|
144
|
+
burning the account on a post that will be silently filtered.
|
|
145
|
+
|
|
96
146
|
|
|
97
147
|
## Record the output
|
|
98
148
|
|
|
99
|
-
After you successfully publish and obtain the live result
|
|
100
|
-
`publish_artifact` tool ONCE so the user can track this
|
|
149
|
+
After you successfully publish a post **or a comment** and obtain the live result
|
|
150
|
+
URL, call the built-in `publish_artifact` tool ONCE so the user can track this
|
|
151
|
+
deliverable in **My Outputs**:
|
|
101
152
|
|
|
102
153
|
```
|
|
103
154
|
publish_artifact(kind="message", channel="reddit", title="<title>", url="<the REAL returned URL>", status="delivered")
|