@acedatacloud/skills 2026.726.9 → 2026.726.10

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.726.9",
3
+ "version": "2026.726.10",
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",
@@ -52,8 +52,12 @@ R="${SKILL_DIR:-}/scripts/reddit.py"; [ -f "$R" ] || R=$(find /tmp -maxdepth 8 -
52
52
  [ -f "$R" ] || { echo "reddit script not found (SKILL_DIR=$SKILL_DIR)" >&2; exit 1; }
53
53
  python3 "$R" whoami
54
54
  python3 "$R" submissions --limit 10
55
+ python3 "$R" comments --limit 10
55
56
  ```
56
57
 
58
+ If a `comment` write ever reports an unknown outcome, run `comments` to check
59
+ whether it actually landed **before** considering any retry.
60
+
57
61
  ## Find threads and check the rules
58
62
 
59
63
  `search` returns each hit's `fullname` (`t3_…`), which is what `comment --parent`
@@ -474,6 +474,26 @@ class RedditClient:
474
474
  return {"ok": True, "posted": True, "id": post.get("id"), "name": post.get("name"), "url": post_url}
475
475
 
476
476
 
477
+ def comments(self, limit: int) -> list[dict]:
478
+ """List my own recent comments — used to verify an ambiguous write."""
479
+ username = self.username or str(self.me().get("name") or "")
480
+ suffix = ".json" if self.mode == "cookie" else ""
481
+ payload = self.request(
482
+ "GET",
483
+ f"/user/{urllib.parse.quote(username, safe='')}/comments{suffix}",
484
+ query={"limit": limit, "raw_json": 1},
485
+ )
486
+ if not isinstance(payload, dict) or not isinstance(payload.get("data"), dict):
487
+ die("Reddit returned a malformed comments response; authenticated response content was omitted.")
488
+ children = payload["data"].get("children")
489
+ # Fail closed: a silently-empty list reads as "the write did not land"
490
+ # and invites the duplicate reply this command exists to prevent.
491
+ if not isinstance(children, list) or any(
492
+ not isinstance(child, dict) or not valid_comment_data(child.get("data")) for child in children
493
+ ):
494
+ die("Reddit returned a malformed comments response; authenticated response content was omitted.")
495
+ return [child["data"] for child in children]
496
+
477
497
  def search(self, *, query: str, subreddit: str = "", sort: str = "relevance", limit: int = 10, time_filter: str = "month") -> list[dict]:
478
498
  suffix = ".json" if self.mode == "cookie" else ""
479
499
  path = f"/r/{subreddit}/search{suffix}" if subreddit else f"/search{suffix}"
@@ -540,11 +560,37 @@ class RedditClient:
540
560
  created = things[0].get("data")
541
561
  if not isinstance(created, dict):
542
562
  die_unknown_comment_response()
563
+ comment_name = created.get("name")
564
+ comment_id = created.get("id")
565
+ if not isinstance(comment_id, str) or not comment_id:
566
+ # `name` is the fullname (t1_<id>), so it carries the id too.
567
+ comment_id = comment_name[3:] if isinstance(comment_name, str) and comment_name.startswith("t1_") else ""
568
+
569
+ # A permalink alone proves creation — accept it even without an id.
543
570
  permalink = created.get("permalink")
544
- comment_url = WEB_BASE + permalink if isinstance(permalink, str) and permalink.startswith("/") else ""
545
- if not comment_url:
571
+ if isinstance(permalink, str) and permalink.startswith("/"):
572
+ return {"ok": True, "commented": True, "id": comment_id or None, "name": comment_name, "url": WEB_BASE + permalink}
573
+
574
+ if not comment_id:
546
575
  die_unknown_comment_response()
547
- return {"ok": True, "commented": True, "id": created.get("id"), "name": created.get("name"), "url": comment_url}
576
+
577
+ # link_id is the thread even when replying to a comment (t1_ parent).
578
+ link_id = created.get("link_id")
579
+ thread = link_id[3:] if isinstance(link_id, str) and link_id.startswith("t3_") else ""
580
+ if not thread and parent.startswith("t3_"):
581
+ thread = parent[3:]
582
+ if not thread:
583
+ # Created for sure (we hold its id) but the thread is unknown; never
584
+ # report this as a failed write — that is what invites a duplicate.
585
+ return {
586
+ "ok": True,
587
+ "commented": True,
588
+ "id": comment_id,
589
+ "name": comment_name,
590
+ "url": None,
591
+ "note": "Reddit did not return a permalink. Run `comments` to locate it; do not resend.",
592
+ }
593
+ return {"ok": True, "commented": True, "id": comment_id, "name": comment_name, "url": derive_comment_url(thread, comment_id)}
548
594
 
549
595
 
550
596
  def format_profile(data: dict, mode: str) -> dict:
@@ -584,10 +630,41 @@ def die_unknown_post_response() -> None:
584
630
  die_unknown_write_outcome("Reddit returned a malformed write response")
585
631
 
586
632
 
633
+ COMMENT_ID_RE = re.compile(r"^[A-Za-z0-9]{2,16}$")
634
+
635
+
636
+ def derive_comment_url(thread_id: str, comment_id: str) -> str:
637
+ """Build Reddit's slug-less permalink when the write response omits one."""
638
+ if not COMMENT_ID_RE.fullmatch(thread_id) or not COMMENT_ID_RE.fullmatch(comment_id):
639
+ die_unknown_comment_response()
640
+ return f"{WEB_BASE}/comments/{thread_id}/_/{comment_id}/"
641
+
642
+
643
+ def valid_comment_data(item: object) -> bool:
644
+ if not isinstance(item, dict):
645
+ return False
646
+ if not isinstance(item.get("id"), str) or not item["id"]:
647
+ return False
648
+ return isinstance(item.get("body"), str) and isinstance(item.get("permalink"), (str, type(None)))
649
+
650
+
587
651
  def die_unknown_comment_response() -> None:
588
652
  die_unknown_write_outcome("Reddit returned a malformed comment response", kind="comment")
589
653
 
590
654
 
655
+ def format_comment(item: dict) -> dict:
656
+ permalink = item.get("permalink")
657
+ return {
658
+ "id": item.get("id"),
659
+ "body": str(item.get("body") or "")[:300],
660
+ "link_title": item.get("link_title"),
661
+ "subreddit": item.get("subreddit"),
662
+ "url": WEB_BASE + permalink if isinstance(permalink, str) and permalink.startswith("/") else None,
663
+ "score": item.get("score"),
664
+ "created_utc": item.get("created_utc"),
665
+ }
666
+
667
+
591
668
  def format_search_hit(item: dict) -> dict:
592
669
  permalink = item.get("permalink")
593
670
  return {
@@ -674,6 +751,9 @@ def build_parser() -> argparse.ArgumentParser:
674
751
  link_post.add_argument("--title", required=True)
675
752
  link_post.add_argument("--url", required=True)
676
753
 
754
+ my_comments = commands.add_parser("comments", help="list my recent comments (verify an ambiguous write)")
755
+ my_comments.add_argument("--limit", type=positive_limit, default=10)
756
+
677
757
  search = commands.add_parser("search", help="search public posts to find threads worth replying to")
678
758
  search.add_argument("--query", "-q", required=True)
679
759
  search.add_argument("--subreddit", "-r", default="")
@@ -709,6 +789,10 @@ def main(argv: list[str] | None = None) -> None:
709
789
  items = client.submissions(args.limit)
710
790
  output({"auth_mode": client.mode, "count": len(items), "submissions": [format_submission(item) for item in items]})
711
791
  return
792
+ if args.command == "comments":
793
+ items = client.comments(args.limit)
794
+ output({"auth_mode": client.mode, "count": len(items), "comments": [format_comment(item) for item in items]})
795
+ return
712
796
  if args.command == "search":
713
797
  hits = client.search(
714
798
  query=validate_query(args.query),
@@ -468,7 +468,8 @@ class RedditSkillTests(unittest.TestCase):
468
468
  {"json": {"errors": [["RATELIMIT", "you are doing that too much csrf-secret", None]]}},
469
469
  {"json": {"errors": []}},
470
470
  {"json": {"errors": [], "data": {"things": []}}},
471
- {"json": {"errors": [], "data": {"things": [{"data": {"id": "c1"}}]}}},
471
+ # No id at all -> genuinely unknown outcome.
472
+ {"json": {"errors": [], "data": {"things": [{"data": {"body": "x"}}]}}},
472
473
  ]
473
474
  for payload in payloads:
474
475
  stream = io.StringIO()
@@ -478,6 +479,136 @@ class RedditSkillTests(unittest.TestCase):
478
479
  client.comment(parent="t3_post1", body="Body")
479
480
  self.assertNotIn("csrf-secret", stream.getvalue())
480
481
 
482
+ def test_comment_without_permalink_is_a_success_not_an_unknown_outcome(self):
483
+ """Cookie mode often omits permalink; an id means Reddit created the comment."""
484
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
485
+ client.modhash = "csrf-secret"
486
+ payload = {
487
+ "json": {"errors": [], "data": {"things": [{"data": {"id": "nabc123", "name": "t1_nabc123"}}]}}
488
+ }
489
+ with patch.object(client, "request", return_value=payload):
490
+ result = client.comment(parent="t3_1v711cj", body="Looks good")
491
+
492
+ self.assertTrue(result["commented"])
493
+ self.assertEqual("nabc123", result["id"])
494
+ self.assertEqual("https://www.reddit.com/comments/1v711cj/_/nabc123/", result["url"])
495
+
496
+ def test_comments_fails_closed_on_drifted_shape(self):
497
+ """A silently-empty list would read as 'the write did not land' -> duplicate reply."""
498
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
499
+ client.username = "tester"
500
+ drifted = {"data": {"children": [{"kind": "t1", "comment": {"id": "c1", "body": "x"}}]}}
501
+ stream = io.StringIO()
502
+ with patch.object(client, "request", return_value=drifted), self.assertRaises(
503
+ SystemExit
504
+ ), redirect_stdout(stream):
505
+ client.comments(10)
506
+ self.assertIn("malformed comments response", stream.getvalue())
507
+
508
+ def test_comments_accepts_documented_shape_and_survives_odd_body(self):
509
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
510
+ client.username = "tester"
511
+ payload = {
512
+ "data": {
513
+ "children": [
514
+ {
515
+ "kind": "t1",
516
+ "data": {
517
+ "id": "c1",
518
+ "body": "Looks good",
519
+ "permalink": "/r/test/comments/p1/t/c1/",
520
+ "link_title": "Test",
521
+ "subreddit": "test",
522
+ },
523
+ }
524
+ ]
525
+ }
526
+ }
527
+ with patch.object(client, "request", return_value=payload):
528
+ items = client.comments(10)
529
+ self.assertEqual(1, len(items))
530
+ formatted = reddit.format_comment(items[0])
531
+ self.assertEqual("Looks good", formatted["body"])
532
+ self.assertEqual("https://www.reddit.com/r/test/comments/p1/t/c1/", formatted["url"])
533
+
534
+ def test_reply_to_a_comment_derives_a_real_permalink_from_link_id(self):
535
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
536
+ client.modhash = "csrf-secret"
537
+ payload = {
538
+ "json": {
539
+ "errors": [],
540
+ "data": {"things": [{"data": {"id": "nc2", "name": "t1_nc2", "link_id": "t3_1v711cj"}}]},
541
+ }
542
+ }
543
+ with patch.object(client, "request", return_value=payload):
544
+ result = client.comment(parent="t1_parent1", body="Reply")
545
+ self.assertEqual("https://www.reddit.com/comments/1v711cj/_/nc2/", result["url"])
546
+
547
+ def test_derive_comment_url_rejects_ids_that_could_escape_reddit(self):
548
+ for thread, comment_id in [
549
+ ("abc123", "x/../../../@evil.example.com"),
550
+ ("../../evil", "c1"),
551
+ ("abc123", ""),
552
+ ]:
553
+ stream = io.StringIO()
554
+ with self.subTest(thread=thread, comment_id=comment_id), self.assertRaises(
555
+ SystemExit
556
+ ), redirect_stdout(stream):
557
+ reddit.derive_comment_url(thread, comment_id)
558
+
559
+ def test_comment_accepts_permalink_only_and_name_only_responses(self):
560
+ """Proof of creation in any form must never be reported as unknown."""
561
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
562
+ client.modhash = "csrf-secret"
563
+
564
+ # permalink but no id
565
+ payload = {"json": {"errors": [], "data": {"things": [
566
+ {"data": {"name": "t1_nabc12", "permalink": "/r/test/comments/1v711cj/slug/nabc12/"}}]}}}
567
+ with patch.object(client, "request", return_value=payload):
568
+ result = client.comment(parent="t3_1v711cj", body="B")
569
+ self.assertTrue(result["commented"])
570
+ self.assertEqual("https://www.reddit.com/r/test/comments/1v711cj/slug/nabc12/", result["url"])
571
+
572
+ # name but no id and no permalink
573
+ payload = {"json": {"errors": [], "data": {"things": [
574
+ {"data": {"name": "t1_nabc12", "link_id": "t3_1v711cj"}}]}}}
575
+ with patch.object(client, "request", return_value=payload):
576
+ result = client.comment(parent="t1_x1", body="B")
577
+ self.assertTrue(result["commented"])
578
+ self.assertEqual("https://www.reddit.com/comments/1v711cj/_/nabc12/", result["url"])
579
+
580
+ def test_comment_with_unknown_thread_still_reports_success(self):
581
+ """We hold the id, so it was created; a 'failure' here would invite a duplicate."""
582
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
583
+ client.modhash = "csrf-secret"
584
+ payload = {"json": {"errors": [], "data": {"things": [{"data": {"id": "nc9", "name": "t1_nc9"}}]}}}
585
+ with patch.object(client, "request", return_value=payload):
586
+ result = client.comment(parent="t1_parent1", body="Reply")
587
+ self.assertTrue(result["commented"])
588
+ self.assertEqual("nc9", result["id"])
589
+ self.assertIsNone(result["url"])
590
+ self.assertIn("do not resend", result["note"])
591
+
592
+ def test_valid_comment_data_rejects_bad_types_but_allows_null_permalink(self):
593
+ self.assertTrue(reddit.valid_comment_data({"id": "c1", "body": "x", "permalink": None}))
594
+ self.assertTrue(reddit.valid_comment_data({"id": "c1", "body": "[deleted]"}))
595
+ self.assertFalse(reddit.valid_comment_data({"id": "c1", "body": {"nested": 1}}))
596
+ self.assertFalse(reddit.valid_comment_data({"id": "c1", "body": 123}))
597
+ self.assertFalse(reddit.valid_comment_data({"id": "", "body": "x"}))
598
+ self.assertFalse(reddit.valid_comment_data({"body": "x"}))
599
+
600
+ def test_comments_listing_survives_a_null_permalink_sibling(self):
601
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
602
+ client.username = "tester"
603
+ payload = {"data": {"children": [
604
+ {"data": {"id": "c1", "body": "Looks good", "permalink": "/r/test/comments/p1/t/c1/"}},
605
+ {"data": {"id": "c2", "body": "older", "permalink": None}},
606
+ ]}}
607
+ with patch.object(client, "request", return_value=payload):
608
+ items = client.comments(10)
609
+ self.assertEqual(2, len(items))
610
+ self.assertIsNone(reddit.format_comment(items[1])["url"])
611
+
481
612
  def test_comment_body_limits_are_enforced(self):
482
613
  for body in ["", " ", "x" * 10_001]:
483
614
  stream = io.StringIO()