Skip to content

feat(strava): add Strava athlete, activity, social & club adapter#2005

Open
yixin-1024 wants to merge 5 commits into
jackwener:mainfrom
yixin-1024:feat/strava-adapter
Open

feat(strava): add Strava athlete, activity, social & club adapter#2005
yixin-1024 wants to merge 5 commits into
jackwener:mainfrom
yixin-1024:feat/strava-adapter

Conversation

@yixin-1024

Copy link
Copy Markdown
Contributor

Strava adapter

Adds a Strava adapter (clis/strava/) covering athlete, activity, social and club data. Strava serves authenticated pages as server-rendered HTML / React micro-frontends (opencli browser analyze → Pattern C, no JSON API), so this works as a Strategy.COOKIE adapter that reads structured data off the logged-in session — React data-react-props blobs, __NEXT_DATA__, SSR tables — and drives the ADP UI for writes. Login is detected via Strava's /login redirect.

Commands (18)

Read

Command Source
profile <athlete> SSR profile (name, location, follower counts)
activities <athlete> recent-activities list (type, name, id)
activity <id> inline-stats (distance, moving time, elevation, date)
whoami / login window.currentAthlete + profile h1 (shared site-auth)
kudos <id> <ADPKudosAndComments> react-props (counts, can_kudo, owner)
comments <id> ADP comment modal (author, text, time, comment_id)
feed [--limit] dashboard following feed
segments [--limit] starred segments table
clubs <athlete> clubs on a profile
club <id> club name, sport, location, members
club-activities <id> recent club member activities
prs <athlete> personal records / best efforts
map <id> route export URLs (GPX / original)

Write (guarded by --execute, since they notify other athletes)

Command Action
kudo <id> give kudos (verified via live DOM — react-props are SSR-only)
comment <id> <text> post a comment via the ADP modal
comment-delete <id> <comment_id> delete your own comment (open → delete → confirm)
join <club> join a club via its Join CTA

All commands accept bare ids, paths, or full URLs. Pure parsers live in utils.js with unit tests (utils.test.js).

Notes

  • Athlete free-text search was intentionally omitted: Strava web /athletes/search only returns find-friends suggestions (no query-matched results, no search XHR), so a search command would surface silently-wrong data.
  • prs/club parsing is best-effort over the rendered page.

Testing

  • opencli validate strava → PASS (18 commands, 0 errors / 0 warnings)
  • vitest run clis/strava/utils.test.js → 10 passing
  • Every command exercised against a live logged-in account; all four write commands verified end-to-end (post→read→delete comment loop, real kudos, real club join).

🤖 Generated with Claude Code

yixin-1024 and others added 5 commits June 23, 2026 18:29
Strava serves authenticated athlete/activity pages as server-rendered HTML
(`opencli browser analyze` -> Pattern C: no JSON XHR, no SSR state), so this
adapter scrapes the rendered DOM through the logged-in Chrome session
(Strategy.COOKIE). Login is verified by detecting Strava's /login redirect for
signed-out requests.

Commands:
  - strava profile <athlete>    name, location, following/followers counts
  - strava activities <athlete> recent activities (type, name, id, url), --limit
  - strava activity <id>        distance, moving time, elevation, date

Pure parsing helpers live in utils.js with unit tests in utils.test.js.
Accepts bare ids, /athletes|/activities paths, or full URLs as input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expands the Strava adapter from a React-props + SSR-HTML scrape of the
logged-in session (Strategy.COOKIE):

  - whoami / login   identity via window.currentAthlete + nav (shared site-auth)
  - kudos <activity> kudos/comment counts + can_kudo + owner, read from the
                     <ADPKudosAndComments> react-props blob on the activity page
  - feed [--limit]   dashboard following feed (athlete_id, activity_id, title)
  - segments         your starred segments (type, name, distance, elevation)
  - clubs <athlete>  clubs an athlete belongs to (id, name from logo alt)

Split into auth.js / social.js / feed.js / segments.js / clubs.js; id
normalizers (club/segment) added to utils.js with unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation

Adds the interactive command set, all verified against the live logged-in session:

  Reads:
  - comments <activity>        comment list (modal scrape: author, text, time, id)
  - club <id>                  club name, sport, location, member count
  - club-activities <id>       recent club member activities
  - prs <athlete>              personal records / best efforts (label, value)
  - map <activity>             route export URLs (GPX / original)

  Writes (guarded by --execute, since they notify other athletes):
  - kudo <activity>            give kudos via the ADP button; verified via live DOM
                               (react-props are SSR-only and don't reflect the click)
  - comment <activity> <text>  post a comment through the ADP modal
  - comment-delete <act> <id>  delete your own comment (open modal → delete → confirm)
  - join <club>                join a club via its Join CTA

whoami now reads the full name off the profile h1 (window.currentAthlete only
carries the id). requireExecute() guard added to utils.js.

Athlete free-text search was intentionally dropped: Strava's web /athletes/search
only returns find-friends suggestions (no query-matched results, no search XHR),
so a `search` command would surface silently-wrong data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ries

The `activity` command only surfaced distance / moving_time / elevation /
date — but the activity page carries the full ride/run breakdown. Scrape it
all from the logged-in session:

  - both inline-stats blocks: the primary (distance/time/elevation) and the
    ride-only secondary-stats (weighted_avg_power / total_work)
  - the "More Stats" table (identified by its Avg/Max header so we never grab
    an unrelated unstyled table): avg/max speed, pace, heart rate, cadence,
    power, plus single-valued calories / temperature / elapsed_time
  - device (head unit) and gear (bike/shoe); Strava's lone em-dash "no gear"
    placeholder is normalized to empty

New pure helper parseMoreStats() flattens the table into avg_<k>/max_<k> and
single keys; parseInlineStats() learns weighted_avg_power / total_work. Both
covered by vitest. Unknown labels are dropped rather than guessed.

Verified live against a ride (full power/HR/cadence) and a run (pace only,
no stats table) — empty fields stay empty, no silently-wrong data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two PR CI gates flagged the new adapter; both are satisfied without
touching adapter behaviour:

- docs/adapters/browser/strava.md: the doc-coverage --strict gate requires
  every clis/* adapter to have a doc page. Documents all 18 commands
  (read/write grouping, examples, --execute notes, login prerequisite).

- silent-column-drop baseline: the gate's heuristic scans every object
  literal in a command's source file. It flags the page.evaluate DOM-scrape
  intermediates (e.g. {iconClass, typeText, name, href}) and rows from other
  commands that share the same file, but every row this adapter actually
  returns matches its declared columns exactly. Adopt the 17 false-positive
  signatures into the baseline, matching the existing entries for the
  twitter / xiaohongshu / bilibili / weibo / zhihu scrapers. Purely additive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@louie-via

Copy link
Copy Markdown

Hi @jackwener 👋 friendly ping on this one whenever you have a moment.

Why it's worth having: Strava is the largest online platform for endurance sports (running / cycling / triathlon) with tens of millions of active athletes. An OpenCLI adapter exposes a huge amount of personal training data to agent workflows — athlete profile, activity history, kudos/comments (social), and clubs. The PR is mergeable with no conflicts and adds the adapter behind the logged-in browser session, consistent with the project's model. Happy to address any feedback. Thanks for maintaining OpenCLI! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants