@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acedatacloud/skills",
3
- "version": "2026.720.0",
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 # who is logged in
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 # the logged-in account
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": "...", "followers_count": ...}
68
+ python3 $X whoami --expect GermeyAce
69
+ # → {"id": "...", "screen_name": "GermeyAce", "identity_verified": true, ...}
69
70
  ```
70
71
 
71
- On an auth error the cookie is expired — have the user reconnect at
72
- <https://auth.acedata.cloud/user/connections>. Do **not** loop-retry.
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
- - **Not E2E-verified** (see the warning above) — expect to validate the first run.
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.
@@ -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 current_user_id_from_cookies() -> str | None:
94
- twid = load_cookie_dict().get("twid", "")
95
- decoded = unquote(twid)
96
- match = re.search(r"(?:^|[&?])u=(\d+)", decoded) or re.search(r"u=(\d+)", decoded)
97
- return match.group(1) if match else None
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, _args):
234
- user_id = current_user_id_from_cookies()
235
- u = await client.get_user_by_id(user_id) if user_id else await client.user()
236
- out(fmt_user(u))
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
- tweets = await client.get_user_tweets(u.id, args.type, count=args.limit)
262
- items = list(tweets)[: args.limit]
263
- out({"user": fmt_user(u), "type": args.type,
264
- "count": len(items), "tweets": [fmt_tweet(t) for t in items]})
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
- try:
269
- tweets = await client.get_tweets_by_ids([args.id])
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
- t = await client.get_tweet_by_id(args.id)
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