@acedatacloud/skills 2026.720.0 → 2026.720.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/x/SKILL.md +20 -7
- package/skills/x/scripts/x.py +92 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.720.
|
|
3
|
+
"version": "2026.720.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",
|
package/skills/x/SKILL.md
CHANGED
|
@@ -44,13 +44,14 @@ python3 -c "import twikit" || { echo "sandbox missing twikit; deploy the sandbox
|
|
|
44
44
|
# script (re-run this setup at the top of every fresh-shell Bash block below).
|
|
45
45
|
X="$SKILL_DIR/scripts/x.py"; [ -f "$X" ] || X=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/x.py' 2>/dev/null | head -1)
|
|
46
46
|
[ -f "$X" ] || { echo "x script not found (SKILL_DIR=$SKILL_DIR)" >&2; exit 1; }
|
|
47
|
-
python3 "$X" whoami #
|
|
47
|
+
python3 "$X" whoami # confirm the shipped CLI can read the authenticated account
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## Read commands (run directly)
|
|
51
51
|
|
|
52
52
|
```sh
|
|
53
|
-
python3 $X whoami
|
|
53
|
+
python3 $X whoami --expect GermeyAce # verify this exact account
|
|
54
|
+
python3 $X whoami # discover the authenticated account
|
|
54
55
|
python3 $X search --query "ai agents" --product Latest --limit 20 # Top | Latest | Media
|
|
55
56
|
python3 $X search-users --query "openai" --limit 10
|
|
56
57
|
python3 $X timeline --limit 20 # my home timeline
|
|
@@ -64,12 +65,20 @@ python3 $X trends --category trending --limit 20 # trending|for-you|
|
|
|
64
65
|
## Verify the connection first
|
|
65
66
|
|
|
66
67
|
```sh
|
|
67
|
-
python3 $X whoami
|
|
68
|
-
# → {"id": "...", "screen_name": "
|
|
68
|
+
python3 $X whoami --expect GermeyAce
|
|
69
|
+
# → {"id": "...", "screen_name": "GermeyAce", "identity_verified": true, ...}
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
`whoami` reads the authenticated account's screen name from X account settings,
|
|
73
|
+
then resolves its public profile. `--expect` compares that server-authenticated
|
|
74
|
+
screen name with the required account before any write. This avoids X's
|
|
75
|
+
Cloudflare-blocked `UserByRestId` endpoint without trusting a local identity
|
|
76
|
+
cookie. Always use `--expect` when the user names the required account.
|
|
77
|
+
|
|
78
|
+
On an actual auth error the cookie is expired — have the user reconnect at
|
|
79
|
+
<https://auth.acedata.cloud/user/connections>. A Cloudflare block is different:
|
|
80
|
+
reconnecting cookies does not fix it. Use `--expect` for identity checks; other
|
|
81
|
+
blocked endpoints need `X_PROXY` or the official X API. Do **not** loop-retry.
|
|
73
82
|
|
|
74
83
|
## Write commands — GATED (dry-run unless trailing `--confirm`)
|
|
75
84
|
|
|
@@ -102,13 +111,17 @@ python3 $X delete --id 123456 --confirm # delete one of M
|
|
|
102
111
|
|
|
103
112
|
- **This is the user's real X account.** Confirm before any write — posts are
|
|
104
113
|
immediate and public.
|
|
105
|
-
- **
|
|
114
|
+
- **Live writes are not E2E-verified.** Read commands were verified on 2026-07-06;
|
|
115
|
+
validate the first confirmed write carefully.
|
|
106
116
|
- **twikit is a scraper of X's non-public API.** It can break when X changes its
|
|
107
117
|
internal endpoints. The bundled script carries an AceDataCloud compatibility
|
|
108
118
|
patch for the current `ondemand.s` webpack chunk map used to generate
|
|
109
119
|
transaction IDs. If `Couldn't get KEY_BYTE indices` appears again, report it
|
|
110
120
|
as X/twikit upstream drift — do NOT ask the user to reconnect cookies for that
|
|
111
121
|
specific error.
|
|
122
|
+
- X currently returns a dependency error for its dedicated `UserMedia` query.
|
|
123
|
+
The CLI falls back to the normal Tweets timeline and filters media posts
|
|
124
|
+
locally; the response includes a `fallback` field when this happens.
|
|
112
125
|
- **ToS / rate-limit / ban risk.** This acts through the web API, not the
|
|
113
126
|
official API — high-frequency automation can get the account rate-limited or
|
|
114
127
|
suspended. Keep volume human-like.
|
package/skills/x/scripts/x.py
CHANGED
|
@@ -19,6 +19,7 @@ breakage.
|
|
|
19
19
|
|
|
20
20
|
Examples:
|
|
21
21
|
python3 x.py whoami
|
|
22
|
+
python3 x.py whoami --expect GermeyAce
|
|
22
23
|
python3 x.py search --query "python" --product Latest --limit 20
|
|
23
24
|
python3 x.py timeline --limit 20
|
|
24
25
|
python3 x.py user-tweets --user elonmusk --type Tweets --limit 20
|
|
@@ -38,7 +39,6 @@ import json
|
|
|
38
39
|
import os
|
|
39
40
|
import re
|
|
40
41
|
import sys
|
|
41
|
-
from urllib.parse import unquote
|
|
42
42
|
|
|
43
43
|
UA = (
|
|
44
44
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
|
@@ -90,11 +90,26 @@ def load_cookie_dict() -> dict:
|
|
|
90
90
|
return cookies
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
93
|
+
def normalize_screen_name(value: str) -> str:
|
|
94
|
+
screen_name = value.strip().lstrip("@")
|
|
95
|
+
if not re.fullmatch(r"[A-Za-z0-9_]{1,15}", screen_name):
|
|
96
|
+
die("--expect must be a valid X screen name (1-15 letters, digits, or underscores)")
|
|
97
|
+
return screen_name
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def cloudflare_block_message(error: Exception) -> str | None:
|
|
101
|
+
text = str(error)
|
|
102
|
+
if "Cloudflare" not in text or not any(
|
|
103
|
+
marker in text for marker in ("Sorry, you have been blocked", "Attention Required!")
|
|
104
|
+
):
|
|
105
|
+
return None
|
|
106
|
+
ray_match = re.search(r"Cloudflare Ray ID:.*?([a-f0-9]{16})", text, re.IGNORECASE | re.DOTALL)
|
|
107
|
+
ray = f" Ray ID: {ray_match.group(1)}." if ray_match else ""
|
|
108
|
+
return (
|
|
109
|
+
"X blocked this automated web-API request at Cloudflare; the login cookies may still be valid."
|
|
110
|
+
f"{ray} For identity checks, use `whoami --expect <screen_name>`; for other blocked endpoints, "
|
|
111
|
+
"configure X_PROXY or use the official X API."
|
|
112
|
+
)
|
|
98
113
|
|
|
99
114
|
|
|
100
115
|
def make_client():
|
|
@@ -230,10 +245,34 @@ async def resolve_user(client, target: str):
|
|
|
230
245
|
|
|
231
246
|
# ── read commands ───────────────────────────────────────────────────
|
|
232
247
|
|
|
233
|
-
async def cmd_whoami(client,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
248
|
+
async def cmd_whoami(client, args):
|
|
249
|
+
settings, _ = await client.v11.settings()
|
|
250
|
+
authenticated_screen_name = settings.get("screen_name") if isinstance(settings, dict) else None
|
|
251
|
+
if not isinstance(authenticated_screen_name, str) or not re.fullmatch(
|
|
252
|
+
r"[A-Za-z0-9_]{1,15}", authenticated_screen_name
|
|
253
|
+
):
|
|
254
|
+
die("X account settings did not return a valid screen name; stopped without performing any write")
|
|
255
|
+
expected = getattr(args, "expect", None)
|
|
256
|
+
if expected:
|
|
257
|
+
expected_screen_name = normalize_screen_name(expected)
|
|
258
|
+
if authenticated_screen_name.casefold() != expected_screen_name.casefold():
|
|
259
|
+
die(
|
|
260
|
+
f"connected X account is @{authenticated_screen_name}, not @{expected_screen_name}; "
|
|
261
|
+
"stopped without performing any write"
|
|
262
|
+
)
|
|
263
|
+
u = await client.get_user_by_screen_name(authenticated_screen_name)
|
|
264
|
+
result = fmt_user(u)
|
|
265
|
+
result.update(
|
|
266
|
+
{
|
|
267
|
+
"identity_verified": True,
|
|
268
|
+
"verification": (
|
|
269
|
+
"authenticated_settings_matches_expected_screen_name"
|
|
270
|
+
if expected
|
|
271
|
+
else "authenticated_settings_screen_name"
|
|
272
|
+
),
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
out(result)
|
|
237
276
|
|
|
238
277
|
|
|
239
278
|
async def cmd_search(client, args):
|
|
@@ -258,20 +297,42 @@ async def cmd_timeline(client, args):
|
|
|
258
297
|
|
|
259
298
|
async def cmd_user_tweets(client, args):
|
|
260
299
|
u = await resolve_user(client, args.user)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
300
|
+
fallback = None
|
|
301
|
+
try:
|
|
302
|
+
tweets = await client.get_user_tweets(u.id, args.type, count=args.limit)
|
|
303
|
+
items = list(tweets)[: args.limit]
|
|
304
|
+
except Exception as error:
|
|
305
|
+
media_dependency_error = (
|
|
306
|
+
args.type == "Media"
|
|
307
|
+
and (
|
|
308
|
+
(isinstance(error, KeyError) and error.args == ("code",))
|
|
309
|
+
or "Dependency: Unspecified" in str(error)
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
if not media_dependency_error:
|
|
313
|
+
raise
|
|
314
|
+
fallback_count = min(max(args.limit * 3, 40), 100)
|
|
315
|
+
tweets = await client.get_user_tweets(u.id, "Tweets", count=fallback_count)
|
|
316
|
+
items = [tweet for tweet in tweets if bool(getattr(tweet, "media", None))][: args.limit]
|
|
317
|
+
fallback = "Tweets (locally filtered for media)"
|
|
318
|
+
result = {
|
|
319
|
+
"user": fmt_user(u),
|
|
320
|
+
"type": args.type,
|
|
321
|
+
"count": len(items),
|
|
322
|
+
"tweets": [fmt_tweet(t) for t in items],
|
|
323
|
+
}
|
|
324
|
+
if fallback:
|
|
325
|
+
result["fallback"] = fallback
|
|
326
|
+
out(result)
|
|
265
327
|
|
|
266
328
|
|
|
267
329
|
async def cmd_tweet(client, args):
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
t = tweets[0] if tweets else None
|
|
271
|
-
except Exception:
|
|
272
|
-
t = None
|
|
330
|
+
tweets = await client.get_tweets_by_ids([args.id])
|
|
331
|
+
t = tweets[0] if tweets else None
|
|
273
332
|
if t is None:
|
|
274
|
-
|
|
333
|
+
die("tweet not found / unavailable")
|
|
334
|
+
if str(getattr(t, "id", "")) != args.id:
|
|
335
|
+
die("tweet id mismatch in X response")
|
|
275
336
|
out(fmt_tweet(t))
|
|
276
337
|
|
|
277
338
|
|
|
@@ -409,7 +470,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
409
470
|
p = argparse.ArgumentParser(prog="x.py", description="X (Twitter) cookie CLI")
|
|
410
471
|
sub = p.add_subparsers(dest="command", required=True)
|
|
411
472
|
|
|
412
|
-
sub.add_parser("whoami", help="show the logged-in account")
|
|
473
|
+
sp = sub.add_parser("whoami", help="show or verify the logged-in account")
|
|
474
|
+
sp.add_argument("--expect", help="verify the cookie account matches this @screen_name")
|
|
413
475
|
|
|
414
476
|
sp = sub.add_parser("search", help="search tweets by keyword")
|
|
415
477
|
sp.add_argument("--query", required=True)
|
|
@@ -489,12 +551,21 @@ def main() -> None:
|
|
|
489
551
|
except (AccountLocked, AccountSuspended) as e:
|
|
490
552
|
die(f"account locked/suspended by X: {e}")
|
|
491
553
|
except TooManyRequests as e:
|
|
554
|
+
cloudflare_message = cloudflare_block_message(e)
|
|
555
|
+
if cloudflare_message:
|
|
556
|
+
die(cloudflare_message)
|
|
492
557
|
die(f"rate limited by X — wait and retry, or slow down. ({e})")
|
|
493
558
|
except (NotFound, TweetNotAvailable, UserNotFound, UserUnavailable) as e:
|
|
494
559
|
die(f"not found / unavailable: {e}")
|
|
495
560
|
except Forbidden as e:
|
|
561
|
+
cloudflare_message = cloudflare_block_message(e)
|
|
562
|
+
if cloudflare_message:
|
|
563
|
+
die(cloudflare_message)
|
|
496
564
|
die(f"forbidden by X (content rule, protected account, or ToS): {e}")
|
|
497
565
|
except TwitterException as e:
|
|
566
|
+
cloudflare_message = cloudflare_block_message(e)
|
|
567
|
+
if cloudflare_message:
|
|
568
|
+
die(cloudflare_message)
|
|
498
569
|
die(f"X API error: {e}")
|
|
499
570
|
except Exception as e:
|
|
500
571
|
# twikit scrapes X's non-public API; a bare error here usually means an
|