/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 listshows 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 inpulse-config.json. Tell the user the project name is unknown and list theavailableprojects from the output. Ifpulse-config.jsondoesn't exist yet at all, tell them to copypulse-config.example.jsontopulse-config.jsonand add their project first. Stop here. - On success you get back
{"project", "query", "context_terms", "require_context", "github"}. Thequeryfield 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 viamax_results+next_tokenpagination — 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 appalready avoids), fall back to/2/tweets/search/recent(last 7 days) with the same query and auth. Recordsearch_modeas"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. Determineis_replyfrom whetherin_reply_to_user_idis present orconversation_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.