@acedatacloud/skills 2026.726.11 → 2026.727.0
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.
|
|
3
|
+
"version": "2026.727.0",
|
|
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/reddit/SKILL.md
CHANGED
|
@@ -55,8 +55,9 @@ python3 "$R" submissions --limit 10
|
|
|
55
55
|
python3 "$R" comments --limit 10
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
`comment` verifies itself: when Reddit answers in a shape it cannot parse, it
|
|
59
|
+
looks the comment up via `comments` before reporting anything. If it still says
|
|
60
|
+
the write likely failed, re-check with `comments` yourself before resending.
|
|
60
61
|
|
|
61
62
|
## Find threads and check the rules
|
|
62
63
|
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import argparse
|
|
15
15
|
import contextlib
|
|
16
16
|
import gzip
|
|
17
|
+
import io
|
|
17
18
|
import json
|
|
18
19
|
import os
|
|
19
20
|
import re
|
|
@@ -31,6 +32,9 @@ SUBREDDIT_RE = re.compile(r"^[A-Za-z0-9_]{2,21}$")
|
|
|
31
32
|
COOKIE_NAME_RE = re.compile(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$")
|
|
32
33
|
# Only a comment (t1_) or a post (t3_) can be replied to.
|
|
33
34
|
THING_ID_RE = re.compile(r"^t[13]_[A-Za-z0-9]{2,16}$")
|
|
35
|
+
VERIFY_ATTEMPTS = 2
|
|
36
|
+
VERIFY_RETRY_SECONDS = 3
|
|
37
|
+
VERIFY_CLOCK_SKEW_SECONDS = 15
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
def output(value) -> None:
|
|
@@ -474,6 +478,78 @@ class RedditClient:
|
|
|
474
478
|
return {"ok": True, "posted": True, "id": post.get("id"), "name": post.get("name"), "url": post_url}
|
|
475
479
|
|
|
476
480
|
|
|
481
|
+
def verify_recent_comment(
|
|
482
|
+
self, parent: str, body: str, written_at: float, seen_ids: set | None = None
|
|
483
|
+
) -> dict:
|
|
484
|
+
"""Resolve an unparsable write by finding the comment we just posted.
|
|
485
|
+
|
|
486
|
+
Matching on body alone would let a stale duplicate vouch for a rejected
|
|
487
|
+
write, so the hit must also belong to this thread and be newly created.
|
|
488
|
+
"""
|
|
489
|
+
wanted = body.strip()
|
|
490
|
+
recent = []
|
|
491
|
+
for attempt in range(VERIFY_ATTEMPTS):
|
|
492
|
+
if attempt:
|
|
493
|
+
# The user-comments listing is a read replica and lags a write.
|
|
494
|
+
time.sleep(VERIFY_RETRY_SECONDS)
|
|
495
|
+
try:
|
|
496
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
497
|
+
recent = self.comments(25)
|
|
498
|
+
except SystemExit:
|
|
499
|
+
recent = []
|
|
500
|
+
match = self.match_written_comment(recent, parent, wanted, written_at, seen_ids)
|
|
501
|
+
if match:
|
|
502
|
+
formatted = format_comment(match)
|
|
503
|
+
return {
|
|
504
|
+
"ok": True,
|
|
505
|
+
"commented": True,
|
|
506
|
+
"id": formatted["id"],
|
|
507
|
+
"name": None,
|
|
508
|
+
"url": formatted["url"],
|
|
509
|
+
"note": "Reddit returned an unrecognized response; the comment was confirmed via `comments`.",
|
|
510
|
+
}
|
|
511
|
+
die(
|
|
512
|
+
"Reddit returned an unrecognized comment response and the comment could not be "
|
|
513
|
+
"confirmed in this account's recent comments. It may have been rejected, or the "
|
|
514
|
+
"listing may still be catching up. Re-check with `comments` before resending; "
|
|
515
|
+
"do not replay automatically."
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
@staticmethod
|
|
519
|
+
def match_written_comment(
|
|
520
|
+
recent: list, parent: str, wanted: str, written_at: float, seen_ids: set | None = None
|
|
521
|
+
) -> dict | None:
|
|
522
|
+
"""Find the comment this write created — never merely a lookalike.
|
|
523
|
+
|
|
524
|
+
Body alone is not proof: the same text can already exist on the same
|
|
525
|
+
thread, so an id present before the write can never vouch for it.
|
|
526
|
+
"""
|
|
527
|
+
thread = parent[3:] if parent.startswith("t3_") else ""
|
|
528
|
+
for item in recent:
|
|
529
|
+
if str(item.get("body") or "").strip() != wanted:
|
|
530
|
+
continue
|
|
531
|
+
if seen_ids is not None and str(item.get("id") or "") in seen_ids:
|
|
532
|
+
continue
|
|
533
|
+
created = item.get("created_utc")
|
|
534
|
+
if not isinstance(created, (int, float)) or created < written_at - VERIFY_CLOCK_SKEW_SECONDS:
|
|
535
|
+
continue
|
|
536
|
+
# Reply target must match: same parent, not merely the same thread.
|
|
537
|
+
parent_id = str(item.get("parent_id") or "")
|
|
538
|
+
if parent_id and parent_id != parent:
|
|
539
|
+
continue
|
|
540
|
+
if thread:
|
|
541
|
+
link_id = item.get("link_id")
|
|
542
|
+
if isinstance(link_id, str) and link_id.startswith("t3_"):
|
|
543
|
+
if link_id[3:] != thread:
|
|
544
|
+
continue
|
|
545
|
+
elif f"/comments/{thread}/" not in str(item.get("permalink") or ""):
|
|
546
|
+
continue
|
|
547
|
+
elif not parent_id:
|
|
548
|
+
# A t1_ reply we cannot attribute to its parent is not proof.
|
|
549
|
+
continue
|
|
550
|
+
return item
|
|
551
|
+
return None
|
|
552
|
+
|
|
477
553
|
def comments(self, limit: int) -> list[dict]:
|
|
478
554
|
"""List my own recent comments — used to verify an ambiguous write."""
|
|
479
555
|
username = self.username or str(self.me().get("name") or "")
|
|
@@ -541,13 +617,15 @@ class RedditClient:
|
|
|
541
617
|
die("Reddit did not return a modhash; the Cookie session cannot perform writes.")
|
|
542
618
|
form["uh"] = self.modhash
|
|
543
619
|
|
|
620
|
+
written_at = time.time()
|
|
544
621
|
payload = self.request("POST", "/api/comment", form=form, write_kind="comment")
|
|
622
|
+
# Cookie mode can answer in jQuery form with no `json` object at all.
|
|
545
623
|
if not isinstance(payload, dict) or not isinstance(payload.get("json"), dict):
|
|
546
|
-
|
|
624
|
+
return self.verify_recent_comment(parent, body, written_at)
|
|
547
625
|
json_payload = payload["json"]
|
|
548
626
|
errors = json_payload.get("errors")
|
|
549
627
|
if not isinstance(errors, list):
|
|
550
|
-
|
|
628
|
+
return self.verify_recent_comment(parent, body, written_at)
|
|
551
629
|
if errors:
|
|
552
630
|
die(
|
|
553
631
|
"Reddit rejected the comment. Check subreddit rules, account age/karma "
|
|
@@ -555,11 +633,11 @@ class RedditClient:
|
|
|
555
633
|
"authenticated error details were omitted."
|
|
556
634
|
)
|
|
557
635
|
things = (json_payload.get("data") or {}).get("things")
|
|
558
|
-
if
|
|
559
|
-
die_unknown_comment_response()
|
|
560
|
-
created = things[0].get("data")
|
|
636
|
+
created = things[0].get("data") if isinstance(things, list) and things and isinstance(things[0], dict) else None
|
|
561
637
|
if not isinstance(created, dict):
|
|
562
|
-
|
|
638
|
+
# Cookie mode returns shapes this parser does not model. Never guess:
|
|
639
|
+
# look for the comment we just wrote before reporting any outcome.
|
|
640
|
+
return self.verify_recent_comment(parent, body, written_at)
|
|
563
641
|
comment_name = created.get("name")
|
|
564
642
|
comment_id = created.get("id")
|
|
565
643
|
if not isinstance(comment_id, str) or not comment_id:
|
|
@@ -572,7 +650,7 @@ class RedditClient:
|
|
|
572
650
|
return {"ok": True, "commented": True, "id": comment_id or None, "name": comment_name, "url": WEB_BASE + permalink}
|
|
573
651
|
|
|
574
652
|
if not comment_id:
|
|
575
|
-
|
|
653
|
+
return self.verify_recent_comment(parent, body, written_at)
|
|
576
654
|
|
|
577
655
|
# link_id is the thread even when replying to a comment (t1_ parent).
|
|
578
656
|
link_id = created.get("link_id")
|
|
@@ -609,6 +609,145 @@ class RedditSkillTests(unittest.TestCase):
|
|
|
609
609
|
self.assertEqual(2, len(items))
|
|
610
610
|
self.assertIsNone(reddit.format_comment(items[1])["url"])
|
|
611
611
|
|
|
612
|
+
def test_unparsable_write_is_resolved_by_verifying_recent_comments(self):
|
|
613
|
+
"""Two live writes hit shapes this parser did not model; verify, never guess."""
|
|
614
|
+
client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
|
|
615
|
+
client.modhash = "csrf-secret"
|
|
616
|
+
client.username = "tester"
|
|
617
|
+
unparsable = {"json": {"errors": [], "data": {}}}
|
|
618
|
+
listing = {"data": {"children": [
|
|
619
|
+
{"data": {"id": "ozw0cnk", "body": "Honest answer: it depends.",
|
|
620
|
+
"link_id": "t3_1uqn02z", "created_utc": time.time(),
|
|
621
|
+
"permalink": "/r/aivideomaking/comments/1uqn02z/help/ozw0cnk/"}},
|
|
622
|
+
]}}
|
|
623
|
+
with patch.object(client, "request", side_effect=[unparsable, listing]):
|
|
624
|
+
result = client.comment(parent="t3_1uqn02z", body="Honest answer: it depends.")
|
|
625
|
+
|
|
626
|
+
self.assertTrue(result["commented"])
|
|
627
|
+
self.assertEqual("ozw0cnk", result["id"])
|
|
628
|
+
self.assertEqual(
|
|
629
|
+
"https://www.reddit.com/r/aivideomaking/comments/1uqn02z/help/ozw0cnk/", result["url"]
|
|
630
|
+
)
|
|
631
|
+
self.assertIn("confirmed via", result["note"])
|
|
632
|
+
|
|
633
|
+
def test_unparsable_write_with_no_matching_comment_reports_likely_failure(self):
|
|
634
|
+
client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
|
|
635
|
+
client.modhash = "csrf-secret"
|
|
636
|
+
client.username = "tester"
|
|
637
|
+
unparsable = {"json": {"errors": [], "data": {}}}
|
|
638
|
+
listing = {"data": {"children": [
|
|
639
|
+
{"data": {"id": "other", "body": "an unrelated older comment", "link_id": "t3_zzz",
|
|
640
|
+
"created_utc": time.time(), "permalink": "/r/x/comments/a/b/other/"}},
|
|
641
|
+
]}}
|
|
642
|
+
stream = io.StringIO()
|
|
643
|
+
with patch.object(client, "request", side_effect=[unparsable, listing, listing]), patch.object(
|
|
644
|
+
reddit.time, "sleep"
|
|
645
|
+
), self.assertRaises(SystemExit), redirect_stdout(stream):
|
|
646
|
+
client.comment(parent="t3_1uqn02z", body="Body that was never posted")
|
|
647
|
+
message = stream.getvalue()
|
|
648
|
+
self.assertIn("could not be confirmed", message)
|
|
649
|
+
self.assertIn("do not replay", message)
|
|
650
|
+
self.assertNotIn("csrf-secret", message)
|
|
651
|
+
|
|
652
|
+
def test_jquery_rejection_is_never_laundered_into_success_by_a_stale_duplicate(self):
|
|
653
|
+
"""A jQuery-shaped rejection has no errors array; a same-text older comment must not vouch for it."""
|
|
654
|
+
client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
|
|
655
|
+
client.modhash = "csrf-secret"
|
|
656
|
+
client.username = "tester"
|
|
657
|
+
jquery_rejection = {"jquery": [[0, 1, "call", ["you are doing that too much. try again in 6 minutes."]]]}
|
|
658
|
+
stale = {"data": {"children": [
|
|
659
|
+
{"data": {"id": "OLD111", "body": "Honest answer: it depends.", "link_id": "t3_OTHER",
|
|
660
|
+
"created_utc": time.time() - 86400,
|
|
661
|
+
"permalink": "/r/other/comments/OTHER/old_thread/OLD111/"}},
|
|
662
|
+
]}}
|
|
663
|
+
stream = io.StringIO()
|
|
664
|
+
with patch.object(client, "request", side_effect=[jquery_rejection, stale, stale]), patch.object(
|
|
665
|
+
reddit.time, "sleep"
|
|
666
|
+
), self.assertRaises(SystemExit), redirect_stdout(stream):
|
|
667
|
+
client.comment(parent="t3_NEWTHRD", body="Honest answer: it depends.")
|
|
668
|
+
self.assertIn("could not be confirmed", stream.getvalue())
|
|
669
|
+
self.assertNotIn("OLD111", stream.getvalue())
|
|
670
|
+
|
|
671
|
+
def test_jquery_shape_recovers_when_the_comment_really_landed(self):
|
|
672
|
+
client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
|
|
673
|
+
client.modhash = "csrf-secret"
|
|
674
|
+
client.username = "tester"
|
|
675
|
+
jquery = {"jquery": [[0, 1, "call", ["ok"]]]}
|
|
676
|
+
listing = {"data": {"children": [
|
|
677
|
+
{"data": {"id": "ozw0cnk", "body": "Looks good", "link_id": "t3_1v711cj",
|
|
678
|
+
"created_utc": time.time(), "permalink": "/r/test/comments/1v711cj/test/ozw0cnk/"}},
|
|
679
|
+
]}}
|
|
680
|
+
with patch.object(client, "request", side_effect=[jquery, listing]):
|
|
681
|
+
result = client.comment(parent="t3_1v711cj", body="Looks good")
|
|
682
|
+
self.assertTrue(result["commented"])
|
|
683
|
+
self.assertEqual("https://www.reddit.com/r/test/comments/1v711cj/test/ozw0cnk/", result["url"])
|
|
684
|
+
|
|
685
|
+
def test_verify_retries_once_for_listing_replication_lag(self):
|
|
686
|
+
"""A just-written comment can lag the replica; one empty read must not mean failure."""
|
|
687
|
+
client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
|
|
688
|
+
client.modhash = "csrf-secret"
|
|
689
|
+
client.username = "tester"
|
|
690
|
+
unparsable = {"json": {"errors": [], "data": {}}}
|
|
691
|
+
empty = {"data": {"children": []}}
|
|
692
|
+
landed = {"data": {"children": [
|
|
693
|
+
{"data": {"id": "lag1", "body": "Looks good", "link_id": "t3_1v711cj",
|
|
694
|
+
"created_utc": time.time(), "permalink": "/r/test/comments/1v711cj/t/lag1/"}},
|
|
695
|
+
]}}
|
|
696
|
+
with patch.object(client, "request", side_effect=[unparsable, empty, landed]), patch.object(
|
|
697
|
+
reddit.time, "sleep"
|
|
698
|
+
) as slept:
|
|
699
|
+
result = client.comment(parent="t3_1v711cj", body="Looks good")
|
|
700
|
+
self.assertTrue(result["commented"])
|
|
701
|
+
self.assertEqual("lag1", result["id"])
|
|
702
|
+
slept.assert_called()
|
|
703
|
+
|
|
704
|
+
def test_verify_ignores_a_comment_created_before_the_write(self):
|
|
705
|
+
client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
|
|
706
|
+
recent = [{"id": "old", "body": "same text", "link_id": "t3_thread1",
|
|
707
|
+
"created_utc": time.time() - 7200, "permalink": "/r/x/comments/thread1/s/old/"}]
|
|
708
|
+
self.assertIsNone(
|
|
709
|
+
reddit.RedditClient.match_written_comment(recent, "t3_thread1", "same text", time.time())
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
def test_match_requires_the_same_reply_target_not_just_recency(self):
|
|
713
|
+
"""These fixtures are all RECENT, so only the thread/parent check can reject them."""
|
|
714
|
+
now = time.time()
|
|
715
|
+
cases = [
|
|
716
|
+
# same body, recent, but a different thread
|
|
717
|
+
({"id": "x1", "body": "same text", "link_id": "t3_OTHER", "created_utc": now,
|
|
718
|
+
"permalink": "/r/o/comments/OTHER/s/x1/"}, "t3_THREADX"),
|
|
719
|
+
# link_id absent -> permalink must still place it in our thread
|
|
720
|
+
({"id": "x2", "body": "same text", "created_utc": now,
|
|
721
|
+
"permalink": "/r/o/comments/OTHER/s/x2/"}, "t3_THREADX"),
|
|
722
|
+
# right thread, but it is a reply to a different comment
|
|
723
|
+
({"id": "x3", "body": "same text", "link_id": "t3_THREADX", "parent_id": "t1_someother",
|
|
724
|
+
"created_utc": now, "permalink": "/r/o/comments/THREADX/s/x3/"}, "t3_THREADX"),
|
|
725
|
+
# t1_ parent, mismatched parent_id
|
|
726
|
+
({"id": "x4", "body": "same text", "parent_id": "t1_wrong", "created_utc": now,
|
|
727
|
+
"permalink": "/r/o/comments/THREADX/s/x4/"}, "t1_target"),
|
|
728
|
+
# t1_ parent with no parent_id at all -> unattributable, not proof
|
|
729
|
+
({"id": "x5", "body": "same text", "created_utc": now,
|
|
730
|
+
"permalink": "/r/o/comments/THREADX/s/x5/"}, "t1_target"),
|
|
731
|
+
]
|
|
732
|
+
for item, parent in cases:
|
|
733
|
+
with self.subTest(item=item["id"]):
|
|
734
|
+
self.assertIsNone(
|
|
735
|
+
reddit.RedditClient.match_written_comment([item], parent, "same text", now)
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
def test_match_accepts_the_comment_this_write_created(self):
|
|
739
|
+
now = time.time()
|
|
740
|
+
top_level = {"id": "ok1", "body": "same text", "link_id": "t3_THREADX", "parent_id": "t3_THREADX",
|
|
741
|
+
"created_utc": now, "permalink": "/r/o/comments/THREADX/s/ok1/"}
|
|
742
|
+
self.assertEqual(
|
|
743
|
+
"ok1", reddit.RedditClient.match_written_comment([top_level], "t3_THREADX", "same text", now)["id"]
|
|
744
|
+
)
|
|
745
|
+
nested = {"id": "ok2", "body": "same text", "link_id": "t3_THREADX", "parent_id": "t1_target",
|
|
746
|
+
"created_utc": now, "permalink": "/r/o/comments/THREADX/s/ok2/"}
|
|
747
|
+
self.assertEqual(
|
|
748
|
+
"ok2", reddit.RedditClient.match_written_comment([nested], "t1_target", "same text", now)["id"]
|
|
749
|
+
)
|
|
750
|
+
|
|
612
751
|
def test_comment_body_limits_are_enforced(self):
|
|
613
752
|
for body in ["", " ", "x" * 10_001]:
|
|
614
753
|
stream = io.StringIO()
|
|
@@ -674,12 +813,12 @@ class RedditSkillTests(unittest.TestCase):
|
|
|
674
813
|
|
|
675
814
|
# Malformed shape.
|
|
676
815
|
stream = io.StringIO()
|
|
677
|
-
with patch.object(client, "request", return_value={"json": {"errors": []}}),
|
|
678
|
-
|
|
679
|
-
), redirect_stdout(stream):
|
|
816
|
+
with patch.object(client, "request", return_value={"json": {"errors": []}}), patch.object(
|
|
817
|
+
reddit.time, "sleep"
|
|
818
|
+
), self.assertRaises(SystemExit), redirect_stdout(stream):
|
|
680
819
|
client.comment(parent="t3_post1", body="Body")
|
|
681
|
-
self.assertIn("
|
|
682
|
-
self.assertIn("
|
|
820
|
+
self.assertIn("could not be confirmed", stream.getvalue())
|
|
821
|
+
self.assertIn("do not replay", stream.getvalue())
|
|
683
822
|
self.assertNotIn("recent submissions", stream.getvalue())
|
|
684
823
|
|
|
685
824
|
# Transport failure inside request().
|