Repository navigation
Releases
Kai v2.5.0
Indoor rides whose FIT file can't be decoded can now be analysed, through their TCX export. Nothing to change when upgrading from v2.4.0, except for people who copy the Garmin skill into their own agent (see below).
Added
garmin_activity_files.pyreads TCX files (#36).parse,queryandanalyzeaccept.tcx(any case) and return the same fields as for FIT (timestamp, heart rate, distance, altitude, speed, power, cadence, position), plus a summary per lap. It uses only the standard library, so there is nothing new to install.- Why it matters: the FIT file of some indoor rides uploaded by third-party apps (seen with TrainingPeaks Virtual) can't be decoded by the FIT parser, so those activities couldn't be analysed at all. Their TCX export carries the full per-second data. To use it:
garmin_activity_files.py download --activity-id <id> --format tcx, thenanalyze,parseorquerythe saved file. analyzegives the same statistics from a TCX as from the FIT of the same activity.
Changed
- When a FIT file can't be decoded, the error now says to download the same activity with
--format tcxand parse that. - A TCX file that declares a DTD or XML entities is refused (a real one never does), which closes the usual XML entity-expansion trick. Files over 100 MB are refused before being read.
If you copied the Garmin skill into your own agent rather than tracking main: copy the updated skills/garmin-health-analysis/ folder, which includes the new TCX support and the assets/ folder from v2.4.0.
How it was checked
22 new offline tests (162 in total), including the same activity built as a FIT and as a TCX giving identical analyze output; fifteen deliberate breakages of the parser each fail a test. Against a real Garmin account: all 6 indoor rides that couldn't be analysed before now analyse and match Garmin's own summaries (distance within 2%, average heart rate within 3%, max heart rate exactly), and for 12 device-recorded activities across five types the FIT and TCX analyses of the same activity agree within 1%. A synthetic 100,000-point file (45 MB) parses in about 6.5 seconds. Not checked: TCX files from sources other than Garmin Connect.
Known issues
parse_gpx_filefails on GPX files that carry heart rate (14 of 16 activities in a sample), andanalyzecan't use GPX output (#42). Use TCX for analysis meanwhile.query --timewith a timestamp that has no timezone is read in the machine's local time; pass aZor an offset.- The Hermes Agent and Codex setups are still untested against live installs.
Full changelog: v2.4.0...v2.5.0
Kai v2.4.0
Garmin dashboards now work with no internet connection. Nothing to change when upgrading from v2.3.2, except for people who copy the Garmin skill into their own agent (see below).
Changed
- Garmin dashboards no longer need the internet (#7).
garmin_chart.pyloaded Chart.js fromcdn.jsdelivr.net, so a dashboard was blank offline and depended on a third-party server. Chart.js 4.4.0 is now bundled inskills/garmin-health-analysis/assets/and embedded in each page, so a dashboard is one self-contained file that draws with no network access and still works after you move, email or archive it. Pages are about 200 KB larger. - What's bundled: the official Chart.js build from npm, unmodified (checked against the registry's integrity hash), plus its
@kurkle/colordependency, both MIT-licensed. Their licences, a checksum file and a README with the update steps are in the same folder, and the root README's credits mention them. - If the bundled file is ever missing, the script warns and falls back to the CDN instead of failing.
If you copied the Garmin skill into your own agent rather than tracking main: copy the whole skills/garmin-health-analysis/ folder, including the new assets/. If only the scripts are copied, dashboards still work but fall back to the CDN with a warning.
How it was checked
11 new offline tests (140 in total), and seven deliberate breakages of the change, including a one-byte edit of the bundled library, each fail a test. Rendered in headless Chromium with every hostname blocked: the previous version's page drew nothing, the new one drew every stat card and chart. Only Chromium was tried, and with a synthetic dashboard rather than real Garmin data (the chart settings and the Chart.js version are unchanged).
Known issues
- FIT files from indoor rides uploaded by third-party apps (seen with TrainingPeaks Virtual) can't be decoded by the FIT parser, and
parsecan't read the TCX export that does carry their data (#36). - The Hermes Agent and Codex setups are still untested against live installs.
Full changelog: v2.3.2...v2.4.0
Kai v2.3.2
Bug fix release: downloading a Garmin activity as a FIT file now gives you a real FIT file, and SECURITY.md becomes a real vulnerability-reporting policy. Nothing to change when upgrading from v2.3.1 unless you copied this workspace into your own agent (see the CREDENTIALS.md note below).
Fixed
garmin_activity_files.py download --format fitsaved a ZIP archive under a.fitname (#15). Garmin sends the original file zipped, soparse,queryandanalyzefailed withInvalid .FIT File Header. The file is now unpacked on download, andparse/query/analyzealso accept a ZIP-wrapped FIT, so files an earlier version left on disk work without downloading them again.downloadfailed when--output-dirdidn't exist. It is now created (mode700)..FITin capitals (what devices and Garmin Express write) was rejected as an unsupported file type.- A FIT file the parser can't decode now says so plainly instead of
Invalid struct format: <0f.
Changed
- Downloaded activity files are now saved owner-only (
600) and are never written through a symlink at the destination. They contain GPS tracks, and the default folder,/tmp, is shared. If you relied on other users being able to read them, adjust the permissions yourself. SECURITY.mdrenamed toCREDENTIALS.md, and a realSECURITY.mdadded (#24). Kai's own rules on handling your credentials lived inSECURITY.md, but GitHub treats that filename as a repository's vulnerability-reporting policy (shown on the Security tab, offered to anyone reporting a bug). The agent rules are nowCREDENTIALS.md, andSECURITY.mdsays which versions are supported, what's in scope and how to report a problem privately (with a reminder not to attach real tokens or personal data).AGENTS.md's session routine readsCREDENTIALS.md, and a new test checks that every file the routine names exists.
If you copied this workspace into your own agent rather than trackingmain: copyCREDENTIALS.mdin and changeSECURITY.mdtoCREDENTIALS.mdin the "Every Session" section of yourAGENTS.md. Your old file still has the same rules, so nothing breaks in the meantime. Details in docs/upgrading.md.
How it was checked
27 new offline tests (26 for the FIT fix, 1 for the session routine), including a FIT file built inside the test so the whole path (unzip, parse, analyze) runs end to end; deliberately breaking the fix in nine ways each fails a test. Against a real Garmin account, every activity recorded on a watch in a 14-activity sample downloaded and analyzed correctly.
Known issues
- FIT files from indoor rides uploaded by third-party apps (seen with TrainingPeaks Virtual) can't be decoded by the FIT parser, and
parsecan't read the TCX export that does carry their data (#36). In the sample, all 5 such rides failed. - The Hermes Agent and Codex setups are still untested against live installs.
Full changelog: v2.3.1...v2.3.2
Kai v2.3.1
Bug fix release: tyre mileage tracking now uses the athlete's own local date, not UTC. Additive: no commands, paths or credentials change from v2.3.0.
Fixed
-
tyre-mileage.shcompared a tyre'sfitted_dateagainst activities' UTC timestamps instead of the athlete's own local date (#22):- East of UTC (e.g. Sydney): a ride on the local morning of the fit day could have a UTC timestamp before UTC midnight of that date, so it was never fetched from Strava at all and silently missed.
- West of UTC (e.g. the Americas): a ride the evening before the fit day could fall after UTC midnight and be wrongly counted.
The fix compares each activity's own
start_date_local(which Strava already provides, per-activity, from its GPS-detected timezone) againstfitted_datedirectly — no machine or ledger timezone configuration needed. The filtering logic moved into a small, independently tested module (tyre_activity_filters.py); the Stravaafter=fetch keeps a generous margin for efficiency only, with the local-date compare doing the real filtering afterwards.Verified against live data: a real early-morning ride in Sydney that the old code fetched zero times is now correctly counted, and a same-fetch-window ride from the day before is still correctly excluded.
Known issues
- FIT downloads from the Garmin skill are saved as a ZIP, so
analyze/parseon them fails (#15). SECURITY.mdholds Kai's own agent rules on handling credentials, but GitHub treats that filename as the repo's vulnerability-reporting policy (#24; fix in review).- The Hermes Agent and Codex setups are still untested against live installs.
Full changelog: v2.3.0...v2.3.1
Kai v2.3.0
Tyre ledger helpers, and a documentation restructure. Additive: no commands, paths or credentials change from v2.2.1.
Added: tyre ledger helpers (#16)
skills/gear-maintenance/scripts/tyres.py edits the tyre ledger, so data/tyres.json no longer needs hand-editing:
python3 skills/gear-maintenance/scripts/tyres.py list
python3 skills/gear-maintenance/scripts/tyres.py add-wheelset road_wheels --name "Road wheels" --gear-id b1234567
python3 skills/gear-maintenance/scripts/tyres.py add-set road_wheels --model "GP5000 28mm" --replace-at 4000
python3 skills/gear-maintenance/scripts/tyres.py add-set road_wheels --model "GP5000 28mm" --replace # retires the old set, fits the new one, in one write
python3 skills/gear-maintenance/scripts/tyres.py retire road_wheels-gp5000-28mm-2026-01-01- A wheelset wears one active tyre set at a time;
add-set --replaceretires the old one (recording when) and fits the new one atomically. fitted_bike_odometer_kmis informational only: from--odometer, or from Strava only when the wheelset has exactly one bike and the set is fitted today. Strava being unreachable never blocks a change.- Notes, custom fields and key order in your existing ledger are preserved. A ledger left in the old
skills/strava/data/location is edited in place, with a notice; a new file is never created alongside it. - The mileage report is unchanged (
tyre-mileage.sh).
Documentation restructure
The README was 381 lines covering three audiences at once. It's now 123 lines: the pitch, the capability table, how it works, a five-step quick start, and a documentation index. Everything else moved out, and each command now lives in exactly one place instead of being repeated across files:
- docs/installation.md — requirements, Python packages (venv and
uv), connecting each data source, gear maintenance setup. - docs/openclaw.md, docs/claude-code-codex.md, docs/hermes.md — one page per agent.
- docs/features.md — every capability in detail: what you get, how it works, what it needs, known limits, and example prompts.
- docs/architecture.md — how it works, repository layout, customizing, privacy detail.
- docs/upgrading.md — v1.x → v2, gear maintenance moved.
- CONTRIBUTING.md — set-up, tests, branch and PR naming, the personal-data rule, how to add a skill, releases.
- CHANGELOG.md — one entry per release from here on.
.github/— a pull request template and bug/feature issue templates, each with a reminder to keep personal data out of anything public.
Old links keep working: the README still has a short "Upgrading from v1.x" section at the anchor the v2.0.0 release notes point to.
Tests (83, was 79)
New: every relative link and heading anchor across the tracked docs resolves; the anchor published releases link to still exists; the README stays under 160 lines; every docs/ page is linked from it; 23 new tests for tyres.py (refusal without --replace, atomic replace, id collisions, ledger-location fallback, a round trip through the real mileage report).
Known issues
- FIT downloads from the Garmin skill are saved as a ZIP, so
analyze/parseon them fails (#15). tyre-mileage.shtreats a tyre's fitted date as midnight UTC, so a fit-day ride outside UTC can be missed or miscounted (#22).SECURITY.mdholds Kai's own agent rules, but GitHub treats that filename as the repo's vulnerability-reporting policy (#24).- The Hermes Agent and Codex setups are still untested against live installs.
Full changelog: v2.2.1...v2.3.0
Kai v2.2.1
Docs and error-message fixes. No behaviour or path changes: nothing to do when upgrading from v2.2.0 except git pull.
Fixed
- Stale login instructions. Five Garmin scripts (
garmin_data.py,garmin_data_extended.py,garmin_query.py,garmin_activity_files.py,garmin_chart.py) told an unauthenticated user to rungarmin_auth.py login --email ... --password ..., or just said "Not authenticated". The--passwordflag was removed in v2.0. They now say to ask the account owner to rungarmin_auth.py loginin their own terminal. The Garmin README had the same stale line and is fixed too. {baseDir}removed (#3). The Garmin, Strava and TrainingPeaksSKILL.mdfiles used an OpenClaw-specific placeholder that Claude Code, Codex and Hermes don't resolve. They, and the skill READMEs, now use plain paths from the repo root (skills/<name>/scripts/...), so commands can be pasted as they are. Garmin commands use.venv/bin/python3, asAGENTS.mdrequires.
Added
- README: a "Branches and pull requests" section with the branch naming (
feat/,fix/,docs/,test/,refactor/,chore/), PR and release conventions. - Tests (56, was 52): docs checks (no
{baseDir}, every documented repo path exists, no doc recommends a password argument) and a check that each Garmin script points to the login command when unauthenticated.
Known issues
- FIT downloads from the Garmin skill are saved as a ZIP, so
analyze/parseon them fails (#15). - Tyre ledger helper commands are still hand-edited JSON (#16).
- The Hermes Agent and Codex setups are still untested against live installs.
Full changelog: v2.2.0...v2.2.1
Kai v2.2.0
Chain wax and tyre tracking move into their own skill, gear-maintenance. Nothing breaks if you don't use them; if you do, see Upgrading below.
What's new
- New skill:
skills/gear-maintenance/. It holdschain-wax.pyandtyre-mileage.sh(previously in the Strava skill) with their example ledgers, aSKILL.mdand a README. They aren't Strava-specific: a bike without Strava tracking works too, and Strava is just one way to read an odometer. The Strava skill is purely Strava again. - Claude Code and Codex find the new skill through the
.claude/skills/and.agents/skills/links. - README: a new optional setup step (chain wax and tyres, with a quick start), an updated requirements note, the skills diagram and layout, and an example prompt.
tyre-mileage.shnow explains what to do when no tyre ledger exists, instead of failing with a traceback.- Tests: 52 offline tests (was 44). New ones cover the ledger-location logic for both scripts, and the repo-hygiene test now also checks the new skill and its links are tracked and that personal ledgers stay ignored.
Upgrading from v2.0 / v2.1
If you use chain wax or tyre tracking, commands moved from skills/strava/scripts/ to skills/gear-maintenance/scripts/, and ledgers from skills/strava/data/ to skills/gear-maintenance/data/:
git pull
mkdir -p skills/gear-maintenance/data
mv skills/strava/data/chain-wax.json skills/strava/data/tyres.json skills/gear-maintenance/data/ 2>/dev/nullA ledger left in the old place still works, with a notice, so nothing stops working in the meantime. If you don't use these features there is nothing to do.
Known issues
- FIT downloads from the Garmin skill are saved as a ZIP, so
analyze/parseon them fails (#15). - Tyre ledger helper commands (add and retire sets) are still hand-edited JSON (#16).
- The Hermes Agent and Codex setups are still untested against live installs.
Full changelog: v2.1.0...v2.2.0
Kai v2.1.0
Chain wax tracking is now a script instead of arithmetic done by the agent. Additive release: nothing to change if you don't use it.
What's new
skills/strava/scripts/chain-wax.py(Python standard library only) keeps a ledger of chain waxes per bike and reads live bike odometers from Strava.reportshows km since the last wax and the next-due odometer for each bike (--jsonfor machine output).logrecords a wax: today with the live odometer by default, or a past date with--odometer. Options:--product,--kind hot|drip,--interval MIN-MAX,--degreased.add-bikeandset-odometercreate the ledger and handle bikes without Strava tracking (a partner's bike, say): you give the odometer by hand and the report flags readings older than 30 days.
- Statuses:
OK,DUE SOON(the last 10% before the minimum),DUE(between minimum and maximum),OVERDUE. - Due ranges are fixed when a wax is logged. Hot wax defaults to an early first re-wax at 150-250 km (the first coating is thin), then 450-500 km. Drip lube defaults to 200-300 km. Any interval can be set.
- Indoor kilometres count, since a chain wears on the trainer too.
- Safe writes: the ledger is written atomically, and a corrupt ledger is reported and never overwritten.
AGENTS.md: Kai now runs the script whenever bike mileage or maintenance comes up, so the answer is the same every session.- Ledger file:
skills/strava/data/chain-wax.jsonis git-ignored personal data;chain-wax.example.jsonshows the format. - Tests: 24 offline tests for the script and 3 repo-hygiene tests (the right files tracked, personal files ignored). 44 in total.
Getting started
python3 skills/strava/scripts/chain-wax.py add-bike road --name "Road bike" --gear-id <your Strava bike id>
python3 skills/strava/scripts/chain-wax.py log road --odometer <km at your last wax> --date <YYYY-MM-DD> --product "Hot wax"
python3 skills/strava/scripts/chain-wax.py reportFind your Strava bike ids with skills/strava/scripts/gear-mileage.sh. Full details are in the Strava skill README.
Known issues
- FIT downloads from the Garmin skill are saved as a ZIP, so
analyze/parseon them fails (#15). - Tyre ledger helper commands are still hand-edited JSON (#16).
- The Hermes Agent and Codex setups are still untested against live installs.
Full changelog: v2.0.0...v2.1.0
Kai v2.0.0
The Garmin skill moves to garminconnect 0.3.x and gets safer login. This is a breaking release for Garmin users: read Upgrading from v1.x. TrainingPeaks, Strava and Kai's instructions are unchanged.
Breaking changes
- Python 3.12 or newer for the Garmin skill (was 3.10). On older systems use
uv venv --python 3.12; the README shows how. garminconnect0.3.x (was pinned to 0.2.38). A plainpip install garminconnectno longer breaks the skill, and the dependency on the retiredgarthlogin is gone.- You must log in to Garmin once more. Tokens saved by v1.x are in a different format and can't be reused.
garmin_auth.py statusrecognises them and tells you. garmin_auth.py login --passwordis removed, and apasswordinconfig.jsonis ignored with a warning. Use the prompt,--password-stdinorGARMIN_PASSWORD.- Anything of yours that calls the Garmin scripts must use the project's
.venv/bin/python3, not the system Python.
What's new
- Password prompt (#6):
loginasks for your password and any MFA code on the terminal (hidden input). The password is never stored; only session tokens are kept (garmin_tokens.json,600in a700folder). - No terminal, no hang: in an agent's shell,
loginstops with "run this yourself" instead of waiting or asking for the password in chat.AGENTS.mdnow tells Kai never to ask for it. - Port to 0.3.x (#5): verified against a live account. All 34 script cases give the same output structure as on 0.2.38 (32 byte-identical; the other two differ by 1 ms in timestamps and by data that changed during the day).
GARMIN_TOKEN_DIRkeeps tokens somewhere else.- Offline tests for the login code (
.venv/bin/python3 -m unittest discover -s tests), and a rewritteninstall.sh. - README: upgrade guide, requirements table,
uvinstructions and a privacy note on passwords.
Known issues
- FIT downloads are saved as a ZIP, so
analyze/parseon them fails (#15). This also happens on v1.x. GPX downloads work. - The Hermes Agent and Codex setups are still untested against live installs.
- Skill symlinks need Linux or macOS (or WSL).
- TrainingPeaks (cookie auth) and Garmin (community library) use unofficial interfaces and can break or be rate-limited.
Full changelog: v1.1.0...v2.0.0
Kai v1.1.0
Adds Claude Code and Codex support, and makes the docs runtime-neutral.
What's new
- Claude Code: a one-line
CLAUDE.md(@AGENTS.md) loads Kai's operating manual, and.claude/skills/links to the four skills. Tested: Claude Code picked up the instructions, took on the Kai role and listed all four skills. - Codex:
AGENTS.mdis read natively and.agents/skills/links to the four skills. Based on Codex's documentation, not yet run against this repo. - README: new "Using Kai with Claude Code or Codex" section. The intro, requirements table, diagram and privacy notes no longer assume OpenClaw.
Upgrading from v1.0.0
git pull. There are no changes to the skills or to requirements.txt. Your USER.md, MEMORY.md and memory/ are untouched.
Known limitations
- The Hermes Agent and Codex setups are untested against live installs.
- The skill symlinks need Linux or macOS (or WSL). Without symlinks the skills still work through their
skills/paths. - Garmin needs Python 3.10+ and the pinned packages from
requirements.txt. A plainpip install garminconnectinstalls 0.3.x, which the Garmin scripts don't support. - TrainingPeaks (cookie auth) and Garmin (community library) use unofficial interfaces and can break or be rate-limited.
Full changelog: v1.0.0...v1.1.0