| name | pulse |
| description | Search X (Twitter) for mentions of a project, product, or topic, classify each mention (problem raised, suggestion, praise, question, noise, spam), enrich with author follower/verification data, and generate a markdown digest. Use when the user runs "/pulse <project>" or asks to check X mentions, GitHub project buzz, social feedback for one of their repos, or general chatter about a topic they're tracking. |
/pulse
Given a project (or topic) name argument (e.g. /pulse myproject), search X for
mentions of it, classify and enrich them, and produce a digest. Works equally well for
a software project/product or a general topic you want to keep an eye on — nothing
about the config or pipeline assumes "software." This skill is read-only — it never
posts, replies, likes, or writes anything to X.
0. Prerequisites check
This skill requires two things to be set up (see this project's README, "One-time
setup"): an X API MCP server connected (added via claude mcp add, backed by the
xurl OAuth bridge talking to api.x.com/mcp) for user lookups, and a registered
xurl app with both an OAuth2 user-context login and an app-only Bearer Token
for search (see step 2 for why both are needed).
- If the MCP tools aren't available, or
xurl auth apps list shows no app with a
cached app-only token: stop and tell the user what's missing, pointing at the README
rather than guessing at setup steps yourself.
- Tool names on the MCP server can vary by version/config — look for the tools that do
user lookup (e.g. something like
get_users_by_usernames) rather than assuming an
exact name.
If you don't already know which xurl app name to use, run xurl auth apps list to
find it (there should be exactly one non-default app registered).
1. Resolve the project argument
Run:
python3 .claude/skills/pulse/scripts/config.py query <project>
(Run it from the pulse project root, or set PULSE_CONFIG=/path/to/pulse-config.json
if this skill has been copied elsewhere.)
- If it exits non-zero / returns
{"error": ...}: the project isn't in
pulse-config.json. Tell the user the project name is unknown and list the
available projects from the output. If pulse-config.json doesn't exist yet at
all, tell them to copy pulse-config.example.json to pulse-config.json and add
their project first. Stop here.
- On success you get back
{"project", "query", "context_terms", "require_context", "github"}. The query field is the fully-scoped X search query string to use —
don't rebuild it yourself, the co-occurrence logic for generic/collision-prone
project names already lives in that script.
2. Search X for mentions
Search does not go through the MCP tools. The hosted MCP's search_posts_all tool
only works over OAuth2 user-context auth, but X's search endpoints (both full-archive
and recent) require App-Only (Bearer Token) auth — a 403 with
"Supported authentication types are [OAuth 2.0 Application-Only]" is what you get if
you try it through MCP. Instead, call xurl directly via Bash, using --auth app:
Q='<the query string from step 1>'
ENC=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$Q")
xurl --auth app --app <app-name> "/2/tweets/search/all?query=${ENC}&max_results=25&tweet.fields=created_at,author_id,conversation_id,in_reply_to_user_id,public_metrics&expansions=author_id"
- Try
/2/tweets/search/all (full-archive) first. Bound the request: fetch at
most ~3 pages / ~100 results total via max_results + next_token pagination — do
not paginate through everything, this is meant to be a small, bounded number of API
calls per run.
- If that call fails on access grounds (403/access-tier error — not the
auth-type error above, which
--auth app already avoids), fall back to
/2/tweets/search/recent (last 7 days) with the same query and auth. Record
search_mode as "recent (full-archive unavailable: <short reason>)" in the final
digest so the user knows the coverage window was smaller than requested. Never
silently return an empty result without saying which mode actually ran.
- Do not filter out replies. Problems and suggestions are often buried in
reply threads, not top-level posts — the query already excludes retweets
(
-is:retweet) but nothing else. Determine is_reply from whether
in_reply_to_user_id is present or conversation_id != id. If the parent post's
text is cheaply available, use it as extra classification context, but don't spend
an extra call fetching parent posts for every single reply.
- De-duplicate by post ID across pages before moving on.
If the search step fails entirely (rate-limited, auth expired, endpoint genuinely
missing) — stop and report the specific error to the user. Do not produce a digest
that silently claims "0 mentions found" when the real story is "the search failed."
3. Enrich with author data
For the unique set of authors in your result set, use the connected server's user
lookup tool(s) to get follower count, verified status, and bio where available. Batch
this lookup (most user-lookup endpoints accept multiple IDs/handles per call) rather
than making one call per mention — stay within the "small, bounded number of API
calls" goal from step 2.
Enrichment is for sorting/flagging only — never drop or hide a mention because its
author has a small following. A problem raised by a 12-follower account is just as
valid as one from a 50k-follower account.
4. Classify each mention
For each mention, make a judgment call using the post text (plus reply/thread context
if you already have it cheaply) and assign exactly one category:
These categories are intentionally generic — they read one way for a software
project and another way for a general topic, but the classification logic doesn't
change:
- bug_report — a concrete problem being raised: something broken, wrong, or
unsatisfactory. For a software project that's an error/crash/regression; for a
general topic it could be a factual correction, a documented complaint, or a real
problem someone's pointing out. If a post both praises the thing and raises a
problem ("love this but X crashes on Windows" / "great point but that stat is
outdated"), classify by the actionable part: bug_report.
- feature_request — a suggestion or ask for something new/different. For a
software project that's a feature/integration ask; for a general topic it's an idea,
a request, or "someone should really look into/cover X."
- question — asking how something works, or for more information, without
raising a problem.
- praise — positive feedback with no actionable ask.
- noise — a search false-positive: the term matched but the post isn't actually
about the thing you're tracking (e.g. a homonym, unrelated context). Flag these
honestly — they measure search quality, not sentiment.
- spam — promotional/marketing content unrelated to genuine engagement with the
thing you're tracking, where the search term just happens to appear.
Also write a short third-person paraphrase of what the author said — summarize in
your own words, don't quote verbatim (avoids reproducing large chunks of someone's
post text and keeps the digest scannable).
5. Build the digest payload and render it
Assemble a JSON object shaped exactly like this (see
.claude/skills/pulse/scripts/digest.py docstring for the authoritative schema):
{
"project": "<project>",
"date": "<YYYY-MM-DD, UTC, today>",
"search_mode": "full_archive" or "recent (full-archive unavailable: ...)",
"query": "<the query string from step 1>",
"mentions": [
{
"author_handle": "...",
"author_followers": 0,
"author_verified": false,
"category": "bug_report|feature_request|question|praise|noise|spam",
"paraphrase": "...",
"url": "https://x.com/<handle>/status/<id>",
"is_reply": true
}
]
}
Write this payload to a temp file, then run:
python3 .claude/skills/pulse/scripts/digest.py < payload.json
This renders the markdown digest, saves it to digests/<project>-<date>.md, and
prints it to stdout — print/show that output to the user as the final result of this
skill. Report the saved file path too (digest.py prints it to stderr as
[saved to ...]).
Notes for future scheduled runs
Nothing in this skill is interactive-only — steps 1-5 are the same whether a human
typed /pulse myproject or a cron job ran claude -p "/pulse myproject" headlessly in
a CI pipeline. If you're ever asked to help set up a scheduled version, that's the
integration point: no rewrite needed, just a wrapper that invokes this same skill
non-interactively on a schedule.