catence 0.1.0 → 0.2.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +239 -269
  2. package/config.example.json +32 -20
  3. package/dist/contracts/index.d.ts +1 -0
  4. package/dist/contracts/index.js +1 -0
  5. package/dist/contracts/progress.d.ts +23 -0
  6. package/dist/contracts/progress.js +48 -0
  7. package/dist/contracts/release.d.ts +1 -1
  8. package/dist/contracts/release.js +1 -1
  9. package/dist/core/logging.d.ts +16 -0
  10. package/dist/core/logging.js +68 -0
  11. package/dist/core/query/repository.d.ts +2 -0
  12. package/dist/core/query/repository.js +42 -0
  13. package/dist/core/runtime/catalog.d.ts +41 -0
  14. package/dist/core/runtime/catalog.js +121 -0
  15. package/dist/core/runtime/configuration.d.ts +1 -1
  16. package/dist/core/runtime/configuration.js +3 -3
  17. package/dist/core/runtime/secrets.d.ts +24 -0
  18. package/dist/core/runtime/secrets.js +68 -0
  19. package/dist/core/runtime/self-update.d.ts +44 -0
  20. package/dist/core/runtime/self-update.js +138 -0
  21. package/dist/elt/application/management.d.ts +2 -0
  22. package/dist/elt/application/management.js +197 -18
  23. package/dist/elt/ingestion/importer.d.ts +3 -2
  24. package/dist/elt/ingestion/importer.js +4 -3
  25. package/dist/elt/ingestion/providers/intervals.d.ts +3 -1
  26. package/dist/elt/ingestion/providers/intervals.js +4 -1
  27. package/dist/elt/ingestion/providers/strava/service.js +9 -2
  28. package/dist/elt/storage/database.d.ts +7 -2
  29. package/dist/elt/storage/database.js +81 -4
  30. package/dist/elt/storage/migrations.js +25 -1
  31. package/dist/interfaces/cli/commands.js +201 -46
  32. package/dist/interfaces/http/cli-options.d.ts +2 -2
  33. package/dist/interfaces/http/cli-options.js +10 -10
  34. package/dist/interfaces/http/server.d.ts +4 -1
  35. package/dist/interfaces/http/server.js +31 -4
  36. package/dist/interfaces/mcp/cli-options.d.ts +2 -2
  37. package/dist/interfaces/mcp/cli-options.js +14 -14
  38. package/dist/interfaces/mcp/main.js +12 -5
  39. package/dist/interfaces/mcp/server.d.ts +2 -2
  40. package/dist/interfaces/mcp/server.js +156 -98
  41. package/dist/runtime/index.d.ts +8 -1
  42. package/dist/runtime/index.js +4 -1
  43. package/docs/deployment.md +338 -0
  44. package/docs/docker-deployment.md +232 -0
  45. package/docs/llm-providers.md +467 -0
  46. package/package.json +6 -2
  47. package/python/catence/providers/garmin/cli.py +30 -9
  48. package/python/catence/providers/garmin/progress.py +85 -0
  49. package/python/catence/providers/garmin/worker.py +132 -5
  50. package/scripts/discover-opencode-go.mjs +210 -0
  51. package/scripts/discover-opencode-zen.mjs +218 -0
package/README.md CHANGED
@@ -1,389 +1,359 @@
1
1
  # Catence
2
2
 
3
- Local MCP server for primary Garmin data, with added Intervals.icu metrics and Strava enrichment.
3
+ Catence is a local-first fitness MCP server for Garmin, Intervals.icu, and targeted Strava enrichment. It gives an agent evidence-backed access to normalized training, wellness, activity, stream, segment, and gear data without uploading the underlying dataset to Catence.
4
4
 
5
- ![](docs/assets/image.png)
5
+ ![Catence](docs/assets/image.png)
6
6
 
7
- ## What?
7
+ ## What is included
8
8
 
9
- As a context, I'm both an endurance athlete and a data junkie. I already got a coach for all the serious stuff, but I wanted to hook up some—any—LLM to the insane amount of data collected from Garmin, derived on Intervals.icu (I don't pay for TrainingPeaks) and comparable efforts on Strava segments. Naturally, the first answer was a local MCP server for my current tooling.
9
+ - Garmin, Intervals.icu, and Strava ingestion with source-aware normalization.
10
+ - Read-only MCP tools, plus explicitly named, lock-guarded Strava hydration tools.
11
+ - One shared Catence agent can serve several isolated athlete stores. Every personal-data MCP call names an `athleteId`; Catence never silently combines athletes.
12
+ - A password-protected Chainlit Console with an authenticated dashboard.
13
+ - A generated demo catalog for safe evaluation in Glama, desktop MCP clients, or local development.
10
14
 
11
- ## Scope
15
+ Catence helps explore recovery, training load, trends, activity detail, swimming/cycling/running progress, segments, gear, and data quality. It reports the evidence and coverage it can see; it does not diagnose or prescribe training.
12
16
 
13
- Currently a bit limited but I'd like to expand it:
17
+ ## Requirements
14
18
 
15
- - [x] Garmin, Intervals.icu, Strava data fetching
16
- - [x] Data normalization and storage
17
- - [x] Read-only local MCP server
18
- - [x] Local Streamable HTTP server
19
- - [x] Release-ready npm, [APM](https://github.com/microsoft/apm), and MCPB distribution artifacts
20
- - [x] Generated demo store for safe MCP evaluation
21
- - [x] Bundled local Chainlit Console with persisted chat history
22
- - [ ] Data writing (training sessions, plans)
23
- - [ ] TrainingPeaks support?
24
- - [ ] Limited multi-user support (see Caveats)
19
+ - Node.js 22+
20
+ - Python 3.12+ and [uv](https://docs.astral.sh/uv/) for Garmin and Strava provider workers
21
+ - Provider credentials for the athletes you choose to sync
25
22
 
26
- ## What Catence can help you do
23
+ ## Quick start
27
24
 
28
- Catence is most useful when a question benefits from checking several local signals instead of reacting to a single score. The Console and MCP tools can help you explore questions such as:
25
+ Install Catence and create the first athlete. The default catalog home is `~/.catence`; set `CATENCE_HOME` or pass `--home` to use a different location. For the full local or Docker deployment walkthrough, see [deployment notes](docs/deployment.md).
29
26
 
30
- - **Recovery and readiness:** “What changed in my sleep, HRV, resting heart rate, stress, and recent load before today’s session?”
31
- - **Training load:** “Is this week harder than my recent baseline, and which sessions contributed most?”
32
- - **Performance trends:** “How has my threshold pace, cycling power, or swim efficiency changed across the season?”
33
- - **Session review:** “Compare this long run or interval set with similar recent efforts, including pace/power, heart rate, terrain, and recovery.”
34
- - **Segments and gear:** “Show my history on this climb,” or “which shoes and bikes have carried the most recent training volume?”
35
- - **Data-quality review:** “Which recent activities lack streams, power, health context, or a matching provider record?”
36
-
37
- ## Sources
38
-
39
- The current available sources respond to my own used platforms. I used some wrappers:
40
-
41
- - [cyberjunky/python-garminconnect](https://github.com/cyberjunky/python-garminconnect)
42
- - [paladini/node-intervals-icu](https://github.com/paladini/node-intervals-icu)
43
- - [stravalib/stravalib](https://github.com/stravalib/stravalib)
44
-
45
- Strava is only used for segments and gear.
46
-
47
- ### Caveats
48
-
49
- There are two caveats to the data fetching, both coming from the fact that this was created to be used by a single person. Accessing the full Garmin API requires applying via a company—which I don't have—and getting Strava data for multiple users is another headache I don't want to get into.
50
-
51
- - I don't use the official Garmin Connect API. The library uses email/pwd.
52
- - Connecting Strava requires the user to be a Strava Premium subscriber and create an [API application](https://stravalib.readthedocs.io/en/latest/get-started/authenticate-with-strava.html). The rotating token is stored locally in
53
- `<data-dir>/secrets/strava.json`.
54
-
55
- What I mean by multi-user is being able to manage and serve various users' data on the same server.
56
-
57
- ## Data
58
-
59
- Priority is assigned per data type. Missing values mean the provider did not supply that metric.
60
-
61
- | Data group | Priority and provenance | Data available per activity or date | Aggregated data |
62
- | --- | --- | --- | --- |
63
- | Generic activity data | <ol><li>Garmin (canonical activity data)</li><li>Intervals.icu (supplemental analysis)</li></ol> | Date/time, timezone, sport, name, distance, elapsed/moving time, elevation, calories, average/max HR, source identity, and quality flags | Activity count, distance, duration, elevation, and calories by date range |
64
- | Running — indoor and outdoor | <ol><li>Garmin (activity facts and streams)</li><li>Intervals.icu (analysis values)</li></ol> | Pace/speed, cadence, HR, power when available, GPS, elevation, grade, splits, intervals, VO₂max, lactate-threshold metrics, and race predictions | Running volume, pace/load trends, and training-metric history |
65
- | Cycling — indoor and outdoor | <ol><li>Garmin (activity facts, FIT streams, and settings)</li><li>Intervals.icu (load, RPE, feel, and weighted power)</li></ol> | Average/weighted power, cadence, speed, HR, GPS, elevation, grade, temperature, cycling dynamics, splits, and FTP observations | Power-duration bests, power coverage, cycling volume/load, and FTP trends |
66
- | Swimming — pool and open water | <ol><li>Garmin (explicit lengths and summaries)</li><li>Intervals.icu (auto-detected sets)</li></ol> | Lengths/laps, pool length, distance, duration, stroke count/rate, SWOLF, average/max HR, sets, and GPS when supplied | Swim volume, session comparisons, and data-completeness summaries |
67
- | Structured intervals and sets | <ol><li>Intervals.icu (Intervals-based intervals)</li><li>Garmin (Garmin splits and sets)</li></ol> | Interval labels, repetitions, duration, moving time, distance, pace, power, HR, intensity, and swim-set details | Interval volume, intensity, and provider-specific comparisons |
68
- | Daily health and wellness | <ol><li>Garmin (primary health data)</li></ol> | HR, resting HR, HRV, sleep, sleep score, stress, Body Battery, readiness, SpO₂, weight, steps, hydration, and nutrition | Daily, weekly, and monthly health and nutrition trends |
69
- | Strava segments and gear | <ol><li>Strava (source of record)</li></ol> | Segment names, grades, climb category, elevation, effort time/distance, power, HR, cadence, PR/KOM ranks, bikes, shoes, and historic efforts | Segment, climb, effort-history, and gear comparisons |
70
- | Derived and cross-source analytics | <ol><li>Garmin (canonical inputs)</li><li>Intervals.icu (interval inputs)</li><li>Strava (segment and gear inputs)</li><li>Catence (derived results)</li></ol> | Reconciled activity values and source-quality flags | Rolling statistics, baselines, correlations, seasonal comparisons, trends, descriptive model fits, and progress reports |
71
-
72
- ## Set up
73
-
74
- Catence exposes the same local MCP server over stdio and optional Streamable HTTP. A tagged release publishes the `catence` npm package, platform-specific MCPB demo bundles, and an APM package. Until that first public release is available, the source-checkout commands below remain fully supported.
75
-
76
- ### Safe one-command demo
27
+ ```sh
28
+ npm install --global catence@0.2.0
29
+ catence-data setup --athlete alex --label "Alex"
30
+ ```
77
31
 
78
- After publication, start a no-account MCP server with generated data:
32
+ Store each provider value through stdin so it does not enter shell history. Values are written to the selected athlete’s owner-only local secret file.
79
33
 
80
34
  ```sh
81
- npx --yes catence@0.1.0 demo
35
+ printf %s 'alex@example.com' | catence-data --athlete alex secret set --provider garmin --field email --value-stdin
36
+ printf %s 'your-garmin-password' | catence-data --athlete alex secret set --provider garmin --field password --value-stdin
37
+ printf %s 'intervals-api-key' | catence-data --athlete alex secret set --provider intervals --field apiKey --value-stdin
38
+ printf %s '12345' | catence-data --athlete alex secret set --provider intervals --field athleteId --value-stdin
82
39
  ```
83
40
 
84
- It creates `./catence-demo` by default and refuses to replace a directory that is not already marked as a Catence demo. The synthetic store deliberately
85
- contains lagged training-load/recovery effects, a multi-signal anomaly period, and some missing sleep-score dates. Every MCP result includes a generated-data disclaimer, so it cannot be mistaken for a personal health record.
86
-
87
- From a source checkout, the equivalent command is:
41
+ Sync and create retrieval context:
88
42
 
89
43
  ```sh
90
- npm run catence-data -- demo
91
- npm run mcp -- demo --data-dir ./catence-demo
44
+ catence-data --athlete alex sync --provider all
45
+ catence-data --athlete alex build-retrieval-index
92
46
  ```
93
47
 
94
- ### Install for live data
95
-
96
- After publication, install the package once, then create and synchronize a store that you own:
48
+ Start the stdio MCP server:
97
49
 
98
50
  ```sh
99
- npm install --global catence@0.1.0
100
- catence-data --data-dir /absolute/path/to/catence-data init
101
- catence-data --data-dir /absolute/path/to/catence-data sync --provider all
102
- catence-data --data-dir /absolute/path/to/catence-data build-retrieval-index
103
- catence --data-dir /absolute/path/to/catence-data
51
+ catence
104
52
  ```
105
53
 
106
- ### Prerequisites
54
+ An agent first calls `list_athletes`, then includes `athleteId` with every data tool. This is intentional: a shared agent may access the stores you configured, but no tool implicitly aggregates or crosses between athletes.
107
55
 
108
- - Node.js 22 or later
109
- - Python 3.12 or later and [uv](https://docs.astral.sh/uv/) for Garmin and Strava provider syncs
56
+ ### Add another athlete
110
57
 
111
- Clone the repository, install the Node and Python dependencies, and create your local `.env` with the following credentials:
58
+ ```sh
59
+ catence-data athlete add --id sam --label "Sam"
60
+ printf %s 'sam@example.com' | catence-data --athlete sam secret set --provider garmin --field email --value-stdin
61
+ printf %s 'sam-password' | catence-data --athlete sam secret set --provider garmin --field password --value-stdin
62
+ catence-data --athlete sam sync --provider garmin
63
+ catence-data athlete list
64
+ ```
112
65
 
113
- - Garmin email/password
114
- - Intervals.icu [API Key and Athlete ID](https://forum.intervals.icu/t/api-access-to-intervals-icu/609)
115
- - Strava [Client ID and Secret](https://stravalib.readthedocs.io/en/latest/get-started/authenticate-with-strava.html)
66
+ The catalog layout is deliberately simple:
67
+
68
+ ```text
69
+ ~/.catence/
70
+ catalog.json
71
+ config.json # shared Console configuration; no secrets
72
+ console/chat-history.sqlite3
73
+ athletes/
74
+ alex/ # isolated DuckDB, raw data, and tokens
75
+ secrets/providers.json # mode 0600
76
+ sam/
77
+ ...
78
+ ```
116
79
 
117
- ### Create and sync a local data store
80
+ Garmin, Intervals, and Strava client credentials are isolated per athlete. Strava OAuth tokens remain in that athlete’s own store. The old 0.1 single-store directory cannot be migrated safely: create a fresh home and re-sync each athlete.
118
81
 
119
- By default, runtime data is placed in `.catence/` inside the checkout. You can also choose an absolute path outside the repository and supply it consistently with `--data-dir` (or set `CATENCE_DATA_DIR`). The examples below use an absolute path.
82
+ ### Common operations
120
83
 
121
84
  ```sh
122
- npm run catence-data -- --data-dir /absolute/path/to/catence-data init
123
- npm run catence-data -- --data-dir /absolute/path/to/catence-data sync --provider all
124
- npm run catence-data -- --data-dir /absolute/path/to/catence-data build-retrieval-index
85
+ # Incremental or explicit data work for one athlete
86
+ catence-data --athlete alex status
87
+ catence-data --athlete alex sync --provider intervals
88
+ catence-data --athlete alex sync --provider garmin --from 2025-07-29
89
+ catence-data --athlete alex backfill --provider garmin --from 2020-01-01 --refresh
90
+ catence-data --athlete alex retry --run <run-id>
91
+ catence-data --athlete alex progress --watch # follow an active sync run live
92
+
93
+ # Strava OAuth uses clientId and clientSecret previously stored for this athlete
94
+ catence-data --athlete alex auth strava --callback
95
+ catence-data --athlete alex disconnect strava
96
+
97
+ # Update the runtime and Console together on the tracked release channel
98
+ catence-data update --check
99
+ catence-data update
100
+
101
+ # Choose a non-default catalog location
102
+ catence-data --home /srv/catence setup --athlete alex --label "Alex"
103
+ catence --home /srv/catence
125
104
  ```
126
105
 
127
- The retrieval index is derived context for agent search; rebuild it after a sync. Syncs are incremental after the first import. Use `backfill` only for an explicit historical range, and add `--refresh` only when you intentionally want to re-fetch already covered Garmin activity details, files, and streams.
106
+ `catence-data update` follows the channel of the installed runtime: a beta
107
+ install tracks the npm `beta` tag and the matching PyPI prereleases, a stable
108
+ install tracks `latest`. Pass `--channel stable` to move off a beta. The command
109
+ replaces the global `catence` package and installs or upgrades
110
+ `catence-console` with `uv tool` when `uv` is available; `--check` only reports.
128
111
 
129
- ### Common commands
112
+ Register `http://127.0.0.1:8765/strava/callback` with the Strava application for the callback flow. The manual `auth strava --code <authorization-code>` flow remains available for headless environments.
130
113
 
131
- Run these from the repository root. Omit `--data-dir` to use the checkout's `.catence/` directory.
114
+ ## Background sync and live progress
132
115
 
133
- ```sh
134
- # Inspect local coverage and sync individual providers
135
- npm run catence-data -- status
136
- npm run catence-data -- sync --provider intervals
137
- npm run catence-data -- sync --provider garmin --from 2025-07-29
138
- npm run catence-data -- sync --provider strava
139
- npm run catence-data -- sync --provider all
140
-
141
- # Recover or import a specific historical range
142
- npm run catence-data -- retry --run <run-id>
143
- npm run catence-data -- backfill --from 2020-01-01
144
- npm run catence-data -- backfill --provider garmin --from 2026-07-01 --refresh
145
-
146
- # Refresh derived search context and launch the local MCP server
147
- npm run catence-data -- build-retrieval-index
148
- npm run mcp
149
- npm run mcp -- --data-dir /absolute/path/to/catence-data
150
-
151
- # Optional local Streamable HTTP MCP and dashboard API
152
- npm run mcp -- serve --data-dir /absolute/path/to/catence-data --allow-origin http://127.0.0.1:8000
153
- npm run mcp -- serve --allow-origin http://127.0.0.1:8000
154
- ```
116
+ Long Garmin backfills fetch hundreds of days of data and can take 20-30+ minutes. Sync runs stream worker output to the console in real time, tolerate Ctrl+C and SSH/terminal deaths cleanly, and expose a live progress heartbeat you can follow from the CLI, the MCP server, or a Docker helper.
155
117
 
156
- The first manual sync uses the previous 12 months only when there is no local normalized coverage. `sync --provider strava` refreshes gear data. Strava activity segments and gear are enriched on demand by the MCP tools.
118
+ ### What happens during a run
157
119
 
158
- ### Connect Strava
120
+ When a sync starts, Catence creates a row in `sync_runs` with status `running` and begins persisting heartbeats to the `sync_run_progress` table (schema migration 14). The Garmin staging worker emits one compact JSON progress record every few seconds on stdout; the runtime parses it, normalizes it, and upserts it. Even if the worker cannot emit — for example while stuck in login — the runtime persists a 30-second fallback heartbeat so a run is always observable and never falsely looks stale.
159
121
 
160
- With `STRAVA_CLIENT_ID` and `STRAVA_CLIENT_SECRET` in `.env`, the local callback flow avoids copying a code out of the browser URL. Register `http://127.0.0.1:8765/strava/callback` as the authorization callback in the Strava application, then run:
161
-
162
- ```sh
163
- npm run catence-data -- auth strava --callback
164
- ```
122
+ Each progress record follows the same contract:
165
123
 
166
- The command prints the authorization URL to the terminal, waits up to five minutes for the browser callback, validates OAuth state, and stores the resulting token only in `<data-dir>/secrets/strava.json`.
124
+ - `runId`, `provider` — which run the record belongs to
125
+ - `stage` — `starting`, `login`, `singletons`, `daily`, `range`, `ftp_history`, `max_metrics`, `hrv_history`, `scores`, `activities`, `collections` while extracting; `completed` on success; `failed`, `interrupted`, or `timed_out` as terminal states written by the runtime
126
+ - `currentStep` — the endpoint, date, or activity being fetched right now
127
+ - `completedUnits` / `totalUnits` — for example days fetched out of the total days in the window
128
+ - `percentComplete` — 0 to 100
129
+ - `elapsedSeconds` / `estimatedRemainingSeconds` — the ETA is computed once the run has a known total and has completed at least one unit
130
+ - `heartbeatAt` — UTC ISO timestamp of the record
167
131
 
168
- The existing manual flow remains available for headless use:
132
+ ### Interruption and stale-run recovery
169
133
 
170
- ```sh
171
- npm run catence-data -- auth strava
172
- npm run catence-data -- auth strava --code <authorization-code>
173
- ```
134
+ - SIGINT, SIGTERM, and SIGHUP (Ctrl+C, terminal close, `kill`) are trapped on both the Node runtime and the Python worker. The child stops between days and the run is marked `interrupted` in `sync_runs` — never a zombie `running` row — and the process exits with 130/143/129.
135
+ - Before every sync or retry, runs still stuck in `running` for more than 15 minutes are marked `timed_out` (with `completed_at` set), so a dead SSH session cannot block later syncs.
136
+ - Resume a failed or interrupted run without re-reading history with `retry --run <run-id>`.
174
137
 
175
- Catence requests `read`, `activity:read_all`, and `read_all`. To remove the stored Strava connection later, run:
138
+ ### Watching progress
176
139
 
177
140
  ```sh
178
- npm run catence-data -- disconnect strava
141
+ catence-data --athlete alex progress # one-shot JSON snapshot
142
+ catence-data --athlete alex progress --watch # live view; exits when no run is active
179
143
  ```
180
144
 
181
- ## Run the local Console
145
+ `--watch` renders a compact progress table on a TTY and exits when the last active run finishes; piped output stays JSON.
182
146
 
183
- The Console is Catence’s local chat experience. It sends model calls through LiteLLM in-process and calls the same Streamable HTTP MCP tools that a coding agent uses. Provider keys are never written to Catence configuration or chat storage.
147
+ The read-only MCP tool `catence_sync_progress` returns the same snapshot — active runs with their latest heartbeat plus the most recent runs — and takes `athleteId` like every other personal-data tool.
184
148
 
185
- ### Install and launch
149
+ ### Docker deployments
186
150
 
187
- After publication, the Console is a separate Python distribution. It includes the Catence-maintained Chainlit frontend and starts the matching npm runtime on loopback automatically:
151
+ `scripts/deploy-console.sh` generates a `sync.sh` helper that gained a `progress` command and a `--detach` flag:
188
152
 
189
153
  ```sh
190
- uvx catence-console@0.1.0 serve --data-dir /absolute/path/to/catence-data
154
+ cd catence-deploy
155
+ ./sync.sh --athlete alex sync --detach # start the run in the background and return
156
+ ./sync.sh --athlete alex progress --watch # follow it live
157
+ ./sync.sh --athlete alex progress # one-shot snapshot
191
158
  ```
192
159
 
193
- The command needs Node.js 22 and `npx`, but does not require a Catence or `catence-ui` checkout. Pass `--mcp-url http://127.0.0.1:8787/mcp` when you intentionally want to use an already-running compatible Catence runtime.
160
+ `--detach` works with `backfill` too. A detached run keeps streaming into the container logs (`docker compose -f catence-deploy/docker-compose.yml logs -f console`) and keeps writing heartbeats, so it survives the shell that started it and is recoverable by the next sync.
194
161
 
195
- For source development, install the locally built `catence-chainlit` wheel and run `uv run --project console catence-console serve` from this repository.
162
+ ## Safe demo and Glama
196
163
 
197
- ### First-run setup
198
-
199
- Launch the Console once with your selected provider’s environment variables in the terminal. If `<data-dir>/config.json` has no Console section, the browser walks through provider and model selection (Azure, OpenAI, or Anthropic) and writes only non-secret configuration. For example:
164
+ Run a clearly marked generated dataset without provider accounts:
200
165
 
201
166
  ```sh
202
- # Azure OpenAI / Foundry: use the Azure resource root and the Responses API route.
203
- export AZURE_API_KEY='…'
204
- export AZURE_API_BASE='https://your-resource.openai.azure.com'
205
- export AZURE_API_VERSION='preview'
206
-
207
- uvx catence-console@0.1.0 serve --data-dir /absolute/path/to/catence-data
167
+ npx --yes catence@0.2.0 demo
208
168
  ```
209
169
 
210
- For an existing configuration or an automated setup, add a `console` section to `<data-dir>/config.json`. It defines named providers and deployments and environment-variable *names*, never credential values. The complete shape is in [`config.example.json`](config.example.json). For the two common direct providers, the minimal profiles are:
170
+ It creates or reuses `~/.catence-demo` with one `demo` athlete. The generated data has explicit caveats in every tool response and never contains personal measurements. The Glama registry entry uses this command for **Try in Browser**, so the hosted sandbox has no access to local credentials or personal data.
211
171
 
212
- ```json
213
- {
214
- "openai": {
215
- "label": "OpenAI",
216
- "model": "openai/gpt-5-mini",
217
- "apiKeyEnv": "OPENAI_API_KEY"
218
- },
219
- "anthropic": {
220
- "label": "Anthropic",
221
- "model": "anthropic/claude-sonnet-4-5",
222
- "apiKeyEnv": "ANTHROPIC_API_KEY"
223
- }
224
- }
225
- ```
172
+ From a checkout:
226
173
 
227
- A provider with several deployments looks like this:
228
-
229
- ```json
230
- {
231
- "console": {
232
- "defaultProfile": "provider",
233
- "limits": {
234
- "toolRounds": 8,
235
- "toolResultCharacters": 24000
236
- },
237
- "profiles": {
238
- "azure-foundry": {
239
- "label": "Provider",
240
- "defaultModel": "terra",
241
- "defaultReasoningEffort": "medium",
242
- "models": {
243
- "terra": { "label": "GPT-5.6 Terra", "model": "provider/gpt-5.6-terra" },
244
- "luna": { "label": "GPT-5.6 Luna", "model": "provider/gpt-5.6-luna" },
245
- "sol": { "label": "GPT-5.6 Sol", "model": "provider/gpt-5.6-sol" }
246
- },
247
- "apiKeyEnv": "API_KEY",
248
- "apiBaseEnv": "API_BASE",
249
- "apiVersionEnv": "API_VERSION"
250
- }
251
- }
252
- }
253
- }
174
+ ```sh
175
+ npm run mcp -- demo
254
176
  ```
255
177
 
256
- An OpenAI-compatible provider such as OpenCode uses a model such as `openai/your-model` plus `apiBaseEnv` and, if needed, `apiKeyEnv`. LiteLLM handles provider normalization.
257
-
258
- The Console shows all configured deployments in its Model selector and sends the selected Thinking effort to LiteLLM as `reasoning_effort`. Choose only efforts supported by the selected deployment. Existing profiles with a single `model` remain supported.
259
-
260
- ### Launching
178
+ ## MCP clients and HTTP
261
179
 
262
- Set the referenced variables in the terminal that will run the Console, then launch everything on loopback with one command:
180
+ For a packaged installation, point a client at `catence`:
263
181
 
264
182
  ```sh
265
- uvx catence-console@0.1.0 serve --data-dir /absolute/path/to/catence-data
183
+ codex mcp add catence -- catence
184
+ claude mcp add --transport stdio catence -- catence
266
185
  ```
267
186
 
268
- On its first normal launch this starts the matching Catence runtime at `http://127.0.0.1:8787`, permits only the local Console origins, and serves the prebuilt chat and dashboard at `http://127.0.0.1:8000`.
269
-
270
- To preflight a profile and an already-running Catence HTTP server, use:
187
+ For a source checkout:
271
188
 
272
189
  ```sh
273
- uvx catence-console@0.1.0 doctor \
274
- --data-dir /absolute/path/to/catence-data \
275
- --mcp-url http://127.0.0.1:8787/mcp
190
+ codex mcp add catence -- npm --prefix /absolute/path/to/catence run mcp
276
191
  ```
277
192
 
278
- `doctor` reports profile IDs, models, missing environment-variable names, and Catence health.
193
+ Optional local Streamable HTTP MCP and dashboard APIs:
279
194
 
280
- ### Local chat history
195
+ ```sh
196
+ catence serve --host 127.0.0.1 --port 8787
197
+ ```
281
198
 
282
- Console chats, messages, tool steps, and thread metadata are persisted at `<data-dir>/console/chat-history.sqlite3`. A later turn receives a compact list of prior calls (not their results); it can lazily load one saved result when it is material, or repeat the authoritative call for fresh data.
199
+ `GET /api/v1/athletes` returns IDs and labels only. `GET /api/v1/dashboard` requires `athleteId`, for example `http://127.0.0.1:8787/api/v1/dashboard?athleteId=alex&days=28`. Browser origins must be listed with `--allow-origin`; the packaged Console instead proxies the dashboard through its authenticated same-origin route.
283
200
 
284
- ## Add Catence to an MCP client
201
+ ## Catence Console
285
202
 
286
- First complete the setup and at least one sync. For a packaged install, point a client to the installed `catence` binary (or use the safe demo command):
203
+ The Console is a local web chat that starts a matching Catence HTTP server on loopback. It uses Chainlit password login and fails closed unless all three authentication variables are supplied.
287
204
 
288
205
  ```sh
289
- catence --data-dir /absolute/path/to/catence-data
290
- npx --yes catence@0.1.0 demo
206
+ export CATENCE_HOME="$HOME/.catence"
207
+ export CATENCE_CONSOLE_USERNAME='coach'
208
+ export CATENCE_CONSOLE_PASSWORD_HASH="$(catence-console auth hash-password)"
209
+ export CHAINLIT_AUTH_SECRET="$(openssl rand -hex 32)"
210
+ export OPENAI_API_KEY='…'
211
+
212
+ uvx catence-console@0.2.0 serve
291
213
  ```
292
214
 
293
- For a source checkout, each client should start the same local command, pointing at the same absolute data directory:
215
+ On the first authenticated visit, the Console wizard asks for an OpenAI-compatible, OpenAI, or Anthropic model and writes only the non-secret model configuration to `~/.catence/config.json`. The `openai-compatible` option covers Azure and Opencode; see [`docs/llm-providers.md`](docs/llm-providers.md). Model-provider keys remain process environment variables. The Console settings include an athlete selector; the server overwrites model-supplied athlete scope with that selected value on every personal-data call.
216
+
217
+ To preflight the Console and a running runtime:
294
218
 
295
219
  ```sh
296
- npm --prefix /absolute/path/to/catence run mcp -- --data-dir /absolute/path/to/catence-data
220
+ uvx catence-console@0.2.0 doctor --home "$HOME/.catence" --mcp-url http://127.0.0.1:8787/mcp
297
221
  ```
298
222
 
299
- The MCP server's ordinary reads are local and read-only. Its explicit Strava hydration tools are the exception; they use the already stored local connection and a shared write lock. `catence serve` retains the same tools at `/mcp` and also exposes loopback dashboard data at `/api/v1/dashboard`; browser origins must be allowed explicitly with `--allow-origin`.
223
+ Console chats and saved tool context are local at `~/.catence/console/chat-history.sqlite3`.
300
224
 
301
- ### Codex
225
+ ### Docker and Cloudflare Tunnel
302
226
 
303
- For a first look, add the generated demo server from the terminal:
227
+ The Console image contains the Node runtime, Catence, and Console. Its MCP server remains loopback-only inside the container; only the Console port is exposed.
304
228
 
305
229
  ```sh
306
- codex mcp add catence-demo -- npx --yes catence@0.1.0 demo
230
+ docker build -f console/Dockerfile -t catence-console .
231
+ docker run --rm -p 127.0.0.1:8000:8000 -v catence-data:/data \
232
+ -e CATENCE_CONSOLE_USERNAME='coach' \
233
+ -e CATENCE_CONSOLE_PASSWORD_HASH='replace-with-bcrypt-hash' \
234
+ -e CHAINLIT_AUTH_SECRET='replace-with-a-long-random-secret' \
235
+ -e OPENAI_API_KEY='…' \
236
+ catence-console
307
237
  ```
308
238
 
309
- For a live npm install, use:
239
+ Initialize the mounted volume before its first production sync, for example:
310
240
 
311
241
  ```sh
312
- codex mcp add catence -- catence --data-dir /absolute/path/to/catence-data
242
+ docker run --rm -it -v catence-data:/data --entrypoint catence-data catence-console \
243
+ setup --athlete alex --label "Alex"
313
244
  ```
314
245
 
315
- For a source checkout, use:
246
+ Use the same `--entrypoint catence-data` pattern for `secret set` and `sync`. Route a Cloudflare Tunnel to `http://127.0.0.1:8000`; do not run a tunnel client in this image. Use HTTPS at the tunnel and retain the Console login—the dashboard is fetched through the authenticated Console origin rather than directly from port 8787.
247
+
248
+ [`scripts/deploy-console.sh`](scripts/deploy-console.sh) deploys this stack from
249
+ the public registries and seeds a starter `config.json` (the Console model
250
+ profiles from [`config.example.json`](config.example.json)) into the data
251
+ volume, so the settings panel works before the first chat. Edit
252
+ `/data/config.json` in the volume (or the bind-mounted `--home` directory) to
253
+ change models and defaults, or discover OpenCode Go profiles from the image:
316
254
 
317
255
  ```sh
318
- codex mcp add catence -- npm --prefix /absolute/path/to/catence run mcp -- --data-dir /absolute/path/to/catence-data
256
+ docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm \
257
+ --entrypoint node console \
258
+ /usr/local/lib/node_modules/catence/scripts/discover-opencode-go.mjs --write /data/config.json
319
259
  ```
320
260
 
321
- Alternatively, add this to `~/.codex/config.toml` for a personal installation, or `.codex/config.toml` in a trusted repository:
261
+ ## Configuration
322
262
 
323
- ```toml
324
- [mcp_servers.catence]
325
- command = "npm"
326
- args = ["--prefix", "/absolute/path/to/catence", "run", "mcp", "--", "--data-dir", "/absolute/path/to/catence-data"]
327
- startup_timeout_sec = 30
328
- ```
263
+ [`config.example.json`](config.example.json) documents rate limits, Strava budgets, and Console model profiles. It intentionally contains environment-variable names rather than credential values. Keep per-athlete provider values in `catence-data secret set`, not in `.env` or `config.json`.
329
264
 
330
- ### OpenCode
265
+ ### Per-model reasoning effort
331
266
 
332
- Add this local MCP definition to `opencode.json` or `opencode.jsonc`:
267
+ Models that support reasoning effort can set a fixed level per model in `config.json`, or expose a per-chat **Thinking effort** dropdown with custom levels via `variants`.
333
268
 
334
- ```jsonc
269
+ **Fixed level** (all requests for the model use it):
270
+
271
+ ```json
335
272
  {
336
- "$schema": "https://opencode.ai/config.json",
337
- "mcp": {
338
- "catence": {
339
- "type": "local",
340
- "command": [
341
- "npm",
342
- "--prefix",
343
- "/absolute/path/to/catence",
344
- "run",
345
- "mcp",
346
- "--",
347
- "--data-dir",
348
- "/absolute/path/to/catence-data"
349
- ],
350
- "enabled": true
273
+ "console": {
274
+ "profiles": {
275
+ "opencode-zen": {
276
+ "label": "OpenCode Zen",
277
+ "defaultModel": "deepseek-v4-flash-free",
278
+ "models": {
279
+ "deepseek-v4-flash-free": {
280
+ "label": "DeepSeek V4 Flash Free",
281
+ "model": "openai/deepseek-v4-flash-free",
282
+ "reasoningEffort": "high"
283
+ }
284
+ },
285
+ "apiKeyEnv": "OPENCODE_API_KEY",
286
+ "apiBaseEnv": "OPENCODE_ZEN_API_BASE"
287
+ }
351
288
  }
352
289
  }
353
290
  }
354
291
  ```
355
292
 
356
- ### Claude Code
357
-
358
- Add the local stdio server from a terminal:
359
-
360
- ```sh
361
- claude mcp add --transport stdio catence -- npm --prefix /absolute/path/to/catence run mcp -- --data-dir /absolute/path/to/catence-data
362
- ```
363
-
364
- Use Claude Code's MCP listing command to confirm it was added:
293
+ **Custom variants** (per-chat dropdown): `variants` maps each dropdown label to the value sent to the provider. This is how non-OpenAI models (for example DeepSeek V4 Flash, which uses its own Default/High/Max scheme rather than the OpenAI-standard list) expose their native levels:
365
294
 
366
- ```sh
367
- claude mcp list
295
+ ```json
296
+ "deepseek-v4-flash-free": {
297
+ "label": "DeepSeek V4 Flash Free",
298
+ "model": "openai/deepseek-v4-flash-free",
299
+ "variants": { "High": "high", "Max": "max" }
300
+ }
368
301
  ```
369
302
 
370
- ### APM and MCPB
303
+ With `variants`, the chat **Thinking effort** dropdown shows `Provider default` plus each label; selecting one sends that exact value (e.g. `reasoning_effort: "high"`). Without `variants`, the dropdown falls back to the OpenAI-standard set (`minimal`, `low`, `medium`, `high`, `xhigh`).
371
304
 
372
- The repository's [`apm.yml`](apm.yml) registers both `catence` and the safe `catence-demo` server with supported agent clients. After a tag is published:
305
+ **Disabling reasoning effort**: an explicitly empty `variants` map (`"variants": {}`) disables the feature for that model — no `reasoning_effort` is ever sent and the **Thinking effort** dropdown is greyed out. This is how models that reject the parameter (for example MiMo, which expects its own `thinking` toggle) are configured:
373
306
 
374
- ```sh
375
- apm install rifusaki/catence#v0.1.0
307
+ ```json
308
+ "mimo-v2.5": {
309
+ "label": "MiMo V2.5",
310
+ "model": "openai/mimo-v2.5",
311
+ "variants": {}
312
+ }
376
313
  ```
377
314
 
378
- Each release also attaches platform-specific `catence-demo-*.mcpb` files. Open the matching file in an MCPB-capable desktop client (including Claude Desktop) to install the one-click generated demo. Live-source setup remains an explicit CLI step, so the bundle never requests or stores provider credentials.
315
+ A model cannot combine `variants: {}` with a fixed `reasoningEffort`.
379
316
 
380
- ## Wellness shortcuts
317
+ A fixed `reasoningEffort` overrides any profile-level `defaultReasoningEffort`; the per-chat dropdown selection (a variant value) overrides both. If nothing is set, the provider default is used. The dropdown options always follow the model currently selected in the **Model** dropdown.
381
318
 
382
- Catence keeps its flexible catalog and analytical tools, and now exposes four small opinionated wellness tools for common recovery questions:
319
+ For source development:
383
320
 
384
- - `wellness_correlate` compares curated recovery/training metrics and can scan lags from −7 to +7 days.
385
- - `wellness_baselines` returns a trailing mean, standard-deviation band, latest value, and latest z-score.
386
- - `wellness_anomalies` finds statistical outliers and dates with multiple unusual signals; it does not diagnose or prescribe.
387
- - `wellness_coverage` shows absent dates and unresolved extraction errors without assuming that missing data means rest or a health outcome.
321
+ ```sh
322
+ npm ci
323
+ npm run check
324
+ npm test
325
+ UV_CACHE_DIR=$PWD/.cache/uv uv run --project console --group dev python -m pytest console/tests -q
326
+ ```
388
327
 
389
- They use the same source-aware `daily_health` and `canonical_activity_training` projections as the general query tools. See [distribution and release notes](docs/distribution.md) for the current tooling.
328
+ ## Beta releases
329
+
330
+ Registry betas use distinct npm and Python prerelease formats: for example,
331
+ `catence@0.2.0-beta.1` pairs with `catence-console==0.2.0b1` and
332
+ `catence-chainlit==0.2.0b1`. The protected `beta` branch verifies candidates;
333
+ a `v0.2.0-beta.1` tag publishes to npm's `beta` channel and creates a GitHub
334
+ prerelease. Follow [the beta release checklist](release/beta-checklist.md) to
335
+ publish the UI wheel first, deploy the registry artifacts with Compose, and
336
+ test the MCP and authenticated Console on a server without touching production
337
+ data.
338
+
339
+ ## Data and caveats
340
+
341
+ Garmin is the primary source for activity and wellness data; Intervals.icu supplements training analysis; Strava is used for targeted segments, efforts, and gear. Missing fields mean a provider did not supply a value. The source credentials and source APIs determine the data available. Catence does not use the official Garmin API; Garmin sync relies on the selected athlete’s email/password credentials.
342
+
343
+ See [distribution notes](docs/distribution.md) for release artifacts, including APM and MCPB demo bundles.
344
+
345
+ ##
346
+
347
+ ```bash
348
+ cd catence-deploy
349
+ docker compose -f docker-compose.yml --env-file .env exec console \
350
+ catence-data sync --athlete martina --provider all --home /data # incremental sync, all providers
351
+ docker compose -f docker-compose.yml --env-file .env exec console \
352
+ catence-data backfill --athlete martina --from 2026-01-01 --home /data # full backfill from a date
353
+ docker compose -f docker-compose.yml --env-file .env exec console \
354
+ catence-data status --athlete martina --home /data # coverage/errors
355
+ docker compose -f docker-compose.yml --env-file .env exec console \
356
+ catence-data progress --athlete martina --home /data --watch # live sync progress
357
+ docker compose -f docker-compose.yml --env-file .env exec console \
358
+ catence-data athlete list --home /data # list athlete IDs
359
+ ```