Skip to content

feat(garmin): add Garmin Connect adapter#2006

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

feat(garmin): add Garmin Connect adapter#2006
yixin-1024 wants to merge 5 commits into
jackwener:mainfrom
yixin-1024:feat/garmin-adapter

Conversation

@yixin-1024

Copy link
Copy Markdown
Contributor

Garmin Connect adapter

Adds a Garmin Connect adapter (clis/garmin/). Garmin's modern web app reads its data from same-origin JSON services under /gc-api/.... Those authenticate off the logged-in session cookie plus a connect-csrf-token request header (published in a <meta> tag on every Connect page) and NK: NT. This adapter (Strategy.COOKIE) reproduces exactly that from the bound tab — so it's a clean JSON-API adapter, no DOM scraping.

Commands (10)

Command Service Output
whoami / login userprofile-service/socialProfile full name, user name, display id, location
activities [--limit] [--start] activitylist-service id, name, type, distance, duration, calories, avg HR, date
activity <id> activity-service/activity + max HR, elevation gain, avg speed, location
stats [--date] usersummary-service steps, step goal, calories, distance, floors, intensity min
sleep [--date] wellness-service/dailySleepData total / deep / light / rem / awake
heartrate [--date] wellness-service/dailyHeartRate resting / max / min / 7-day avg
prs personalrecord-service type, activity type, value, activity, date
gear gear-service name, make, model, type, status
devices device-service name, part number, application key

Dates default to today; activity ids accept bare/path/URL forms. Pure formatters (metersToKm / secondsToHms / isoDate / id-parse) live in utils.js with unit tests.

Testing

  • opencli validate garmin → PASS (10 commands, 0 errors / 0 warnings)
  • vitest run clis/garmin/utils.test.js → 4 passing
  • Every command exercised against a live logged-in account (activities, activity detail, daily stats, heart rate, PRs, gear, devices all returning correct data). sleep returns EmptyResultError cleanly for accounts with no sleep-tracking device.

🤖 Generated with Claude Code

yixin-1024 and others added 5 commits June 23, 2026 19:40
Garmin Connect's modern web app reads its data from same-origin JSON services
under /gc-api/...; those authenticate off the logged-in session cookie plus a
`connect-csrf-token` header (published in a <meta> tag on every Connect page)
and `NK: NT`. This adapter (Strategy.COOKIE) reproduces exactly that from the
bound tab — a clean JSON-API adapter, no DOM scraping.

Commands (10):
  - whoami / login   identity via userprofile-service/socialProfile (shared site-auth)
  - activities       recent activities (id, name, type, distance, duration, calories, hr)
  - activity <id>    one activity in detail (+ max hr, elevation, avg speed, location)
  - stats [--date]   daily wellness summary (steps, calories, distance, floors, intensity)
  - sleep [--date]   sleep breakdown (deep / light / rem / awake)
  - heartrate [--date]  resting / max / min heart rate
  - prs              personal records
  - gear             registered gear
  - devices          registered devices

Pure formatters (metersToKm / secondsToHms / isoDate / id-parse) live in utils.js
with unit tests. Dates default to today; ids accept bare/path/URL forms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expands the Garmin adapter from 10 to 20 commands, all over the same
/gc-api JSON services:

  - status        training status, fitness trend, VO2 max (run + bike)
  - load          acute (short-term) / chronic training load, ACWR, load focus
  - powercurve    cycling power curve (best watts per duration) — the FTP curve
  - bodybattery   Body Battery charged / drained / current
  - stress        daily average / max stress
  - weight        body-weight log (kg, BMI)
  - badges        earned badges
  - courses       saved routes (路书) — name, type, distance, elevation
  - course <id>   one route in detail (distance, gain/loss, geo route id)
  - connections   Garmin Connect friends

status/load/VO2 max all come from the trainingstatus/aggregated service;
power curve from fitnessstats-service. Athlete search was left out — Garmin's
web search endpoints are gated (403) and don't return query-matched results.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the social side (25 commands total):
  - search <keyword>   find athletes — POSTs the "Find Friends" usersearch-service
                       /search/v3 form endpoint, returns name/location/follow status
  - follow <athlete>   follow an athlete (POST follower-service), guarded by --execute
  - unfollow <athlete> stop following (DELETE), guarded by --execute
  - following          athletes you follow
  - followers          athletes who follow you
  - connections        fixed to the connection-service v2 pagination endpoint

garminApi() now supports POST/DELETE with form bodies and 204 responses;
adds requireExecute() and a displayName (GUID / profile-URL) normalizer.

Note: following a private athlete creates a pending follow *request* (it only
appears in `following` once approved); `unfollow` removes an active follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Four more /gc-api wellness reads (29 commands total):
  - hrv [--date]          heart-rate variability (last night / weekly avg, status)
  - hydration [--date]    water intake vs goal, sweat loss
  - respiration [--date]  breaths per minute (waking / sleep / high / low)
  - spo2 [--date]         blood-oxygen / pulse ox (avg / lowest / latest)

Each returns EmptyResultError cleanly when the day has no measurement
(e.g. HRV / SpO2 need a compatible watch worn overnight).

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/garmin.md: the doc-coverage --strict gate requires
  every clis/* adapter to have a doc page. Documents all 29 commands
  (Account / Activities / Wellness / Social grouping, examples, --execute
  notes, login prerequisite).

- silent-column-drop baseline: the gate's heuristic scans every object
  literal in a command's source file. Because related commands share one
  file (profile.js, health.js, social.js, ...), each command's row object is
  also audited against its siblings' columns and flagged. Every row this
  adapter actually returns matches its declared columns exactly. Adopt the 25
  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 👋 gentle nudge on this one.

Why it matters: Garmin has the largest installed base of dedicated sports / wearable devices in the world, and Garmin Connect is where all of that activity, health and device data lives. An adapter makes that data reachable for agents (activities, daily metrics, device data, etc.) using the user's own logged-in session. Clean and mergeable, no conflicts. Glad to iterate on anything you'd like changed. Thanks! 🙏

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