catence 0.2.0-beta.2 → 0.2.0-beta.21

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 (66) hide show
  1. package/README.md +28 -82
  2. package/config.example.json +25 -0
  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 +49 -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 +48 -0
  13. package/dist/core/runtime/catalog.js +17 -7
  14. package/dist/core/runtime/configuration.d.ts +3 -1
  15. package/dist/core/runtime/configuration.js +5 -1
  16. package/dist/core/runtime/opencode-go.d.ts +58 -0
  17. package/dist/core/runtime/opencode-go.js +246 -0
  18. package/dist/core/runtime/secrets.js +51 -13
  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/detached-sync.d.ts +50 -0
  22. package/dist/elt/application/detached-sync.js +99 -0
  23. package/dist/elt/application/management.d.ts +15 -0
  24. package/dist/elt/application/management.js +435 -19
  25. package/dist/elt/ingestion/importer.d.ts +3 -2
  26. package/dist/elt/ingestion/importer.js +5 -4
  27. package/dist/elt/ingestion/providers/intervals.d.ts +12 -1
  28. package/dist/elt/ingestion/providers/intervals.js +37 -2
  29. package/dist/elt/ingestion/providers/registry.d.ts +13 -0
  30. package/dist/elt/ingestion/providers/registry.js +18 -1
  31. package/dist/elt/ingestion/providers/strava/service.js +7 -1
  32. package/dist/elt/normalization/normalizers.js +111 -15
  33. package/dist/elt/storage/database.d.ts +21 -2
  34. package/dist/elt/storage/database.js +184 -6
  35. package/dist/elt/storage/migrations.js +25 -1
  36. package/dist/elt/storage/progress-sidecar.d.ts +61 -0
  37. package/dist/elt/storage/progress-sidecar.js +200 -0
  38. package/dist/interfaces/cli/commands.js +116 -1
  39. package/dist/interfaces/http/generation-status.d.ts +19 -0
  40. package/dist/interfaces/http/generation-status.js +55 -0
  41. package/dist/interfaces/http/server.d.ts +7 -1
  42. package/dist/interfaces/http/server.js +184 -1
  43. package/dist/interfaces/mcp/server.d.ts +2 -1
  44. package/dist/interfaces/mcp/server.js +50 -13
  45. package/dist/runtime/index.d.ts +8 -1
  46. package/dist/runtime/index.js +4 -1
  47. package/docs/architecture.md +11 -1
  48. package/docs/configuration.md +243 -0
  49. package/docs/deployment/docker.md +528 -0
  50. package/docs/deployment/local-console.md +263 -0
  51. package/docs/deployment/local-mcp.md +329 -0
  52. package/docs/development.md +337 -0
  53. package/docs/distribution.md +58 -0
  54. package/docs/llm-providers.md +249 -6
  55. package/docs/mcp-activity-retrieval.md +37 -30
  56. package/package.json +5 -1
  57. package/pyproject.toml +1 -1
  58. package/python/catence/providers/garmin/cli.py +43 -9
  59. package/python/catence/providers/garmin/progress.py +85 -0
  60. package/python/catence/providers/garmin/registry.py +1 -0
  61. package/python/catence/providers/garmin/streams.py +38 -0
  62. package/python/catence/providers/garmin/swim_backfill.py +83 -0
  63. package/python/catence/providers/garmin/worker.py +372 -11
  64. package/scripts/discover-opencode-go.mjs +102 -0
  65. package/scripts/discover-opencode-zen.mjs +218 -0
  66. package/uv.lock +3 -3
@@ -0,0 +1,263 @@
1
+ # Local Console deployment
2
+
3
+ Catence Console is a password-protected local web chat that starts a matching
4
+ Catence runtime on loopback. It uses Chainlit password login and fails closed
5
+ unless all three authentication variables are supplied.
6
+
7
+ This guide covers the **local** path: the npm package plus the Python Console
8
+ on your machine. The Docker path (one container built from the public
9
+ registries) is covered in [`docker.md`](docker.md). Both paths end with the
10
+ same Console, data directory layout, model configuration, and credentials
11
+ model.
12
+
13
+ ## Prerequisites
14
+
15
+ - Node.js 22+ and Python 3.12+ with [uv](https://docs.astral.sh/uv/).
16
+ - Provider credentials for the athletes you choose to sync (Garmin,
17
+ Intervals.icu, Strava).
18
+ - One model-provider key (OpenAI, Anthropic, OpenCode Go, Azure, or any
19
+ OpenAI-compatible endpoint).
20
+
21
+ ## What gets stored where
22
+
23
+ | What | Where | Example |
24
+ | --- | --- | --- |
25
+ | Catalog + athlete stores | data home | `~/.catence/` |
26
+ | Model profiles (no secrets) | `config.json` in the data home | `console.profiles` |
27
+ | Provider credentials | per-athlete secret file, mode 0600 | `catence-data secret set` |
28
+ | API keys | process environment only | shell exports |
29
+ | Chat history + preferences | data home | `console/chat-history.sqlite3` |
30
+
31
+ Credentials never land in `config.json`; profiles reference environment
32
+ variable *names* (`apiKeyEnv`, `apiBaseEnv`, `apiVersionEnv`). See
33
+ [`llm-providers.md`](../llm-providers.md) for the profile reference and
34
+ [`configuration.md`](../configuration.md) for the complete `config.json`
35
+ schema.
36
+
37
+ ## 1. Install the runtime and the Console
38
+
39
+ ```sh
40
+ npm install --global catence@beta # or catence@latest for stable
41
+ uv tool install catence-console # persistent `catence-console` command
42
+ # alternative to the last line: uvx catence-console@<version> serve (ephemeral)
43
+ ```
44
+
45
+ `catence-console serve` launches a matching Catence runtime automatically: it
46
+ uses a globally installed `catence` if present, otherwise
47
+ `npx catence@<pinned>`.
48
+
49
+ ## 2. Create the catalog and first athlete
50
+
51
+ ```sh
52
+ catence-data setup --athlete alex --label "Alex"
53
+ ```
54
+
55
+ This creates `~/.catence/` (or `$CATENCE_HOME`). It also tolerates a directory
56
+ that already holds only Console artifacts (`config.json`, `console/`).
57
+
58
+ ## 3. Store provider credentials (stdin only, never shell history)
59
+
60
+ Garmin (`email`, `password`), Intervals.icu (`apiKey`, `athleteId`), and Strava
61
+ (`clientId`, `clientSecret`):
62
+
63
+ ```sh
64
+ # Garmin
65
+ printf %s 'alex@example.com' | catence-data --athlete alex secret set --provider garmin --field email --value-stdin
66
+ printf %s 'your-garmin-password' | catence-data --athlete alex secret set --provider garmin --field password --value-stdin
67
+ # Intervals.icu
68
+ printf %s 'intervals-api-key' | catence-data --athlete alex secret set --provider intervals --field apiKey --value-stdin
69
+ printf %s '12345' | catence-data --athlete alex secret set --provider intervals --field athleteId --value-stdin
70
+ # Strava (create an API application at https://www.strava.com/settings/api)
71
+ printf %s 'strava-client-id' | catence-data --athlete alex secret set --provider strava --field clientId --value-stdin
72
+ printf %s 'strava-client-secret' | catence-data --athlete alex secret set --provider strava --field clientSecret --value-stdin
73
+ ```
74
+
75
+ Set only the providers you actually sync; `secret set` accepts each field
76
+ independently.
77
+
78
+ ## 4. Sync data and build retrieval context
79
+
80
+ ```sh
81
+ catence-data --athlete alex sync --provider all
82
+ catence-data --athlete alex build-retrieval-index
83
+ ```
84
+
85
+ ## 5. Configure the model
86
+
87
+ On the first chat, the Console wizard asks for a provider and model and writes
88
+ a starter profile to `~/.catence/config.json`. To start from the documented
89
+ profiles instead, copy the `console` section from
90
+ [`config.example.json`](../../config.example.json) into `~/.catence/config.json`.
91
+ To add OpenCode Go models, run the
92
+ [model discovery script](../llm-providers.md#opencode).
93
+
94
+ ## 6. Set the model credentials and Console login
95
+
96
+ ```sh
97
+ export OPENAI_API_KEY='…' # or ANTHROPIC_API_KEY / OPENCODE_GO_API_KEY …
98
+ export CATENCE_CONSOLE_USERNAME='coach'
99
+ export CATENCE_CONSOLE_PASSWORD_HASH="$(catence-console auth hash-password)"
100
+ export CHAINLIT_AUTH_SECRET="$(openssl rand -hex 32)"
101
+ ```
102
+
103
+ The Console fails closed unless `CATENCE_CONSOLE_USERNAME`,
104
+ `CATENCE_CONSOLE_PASSWORD_HASH` (a bcrypt hash), and `CHAINLIT_AUTH_SECRET`
105
+ are all set. There is one shared account; the username/password pair grants a
106
+ single `console-owner` role.
107
+
108
+ ## 7. Start the Console
109
+
110
+ ```sh
111
+ catence-console serve
112
+ # open http://127.0.0.1:8000
113
+ ```
114
+
115
+ The Model dropdown in the settings panel lists every profile's deployments.
116
+ Preflight with:
117
+
118
+ ```sh
119
+ catence-console doctor
120
+ ```
121
+
122
+ ### Manage models in the Console
123
+
124
+ The **Models** page (header button) manages the model list without editing
125
+ `config.json` by hand:
126
+
127
+ - Enable/disable toggles hide models from the chat's Model dropdown. Disabled
128
+ choices are stored per machine in the Console database
129
+ (`console/chat-history.sqlite3`), never in `config.json`, and the Console
130
+ refuses to disable the only enabled model.
131
+ - **Add a custom model** appends one deployment to an existing profile — id,
132
+ display label, LiteLLM reference (`openai/…`, `anthropic/…`,
133
+ `openai/responses/…`), and optional fixed reasoning effort or a custom
134
+ variants map.
135
+ - **Remove / Make default** edit `config.json`'s console section directly;
136
+ every other section is preserved, the result is re-validated with the same
137
+ strict parser used at startup before it replaces the file, and secrets stay
138
+ forbidden (only environment-variable names are ever stored).
139
+
140
+ The dashboard header has a **Sync data** button that starts the same detached
141
+ sync as `catence-data sync --provider all` through the authenticated Console
142
+ origin, shows live progress while the run is active, and displays the last
143
+ completed sync afterwards. Each manual sync also refreshes OpenCode Go model
144
+ profiles first; a discovery failure never blocks the data sync.
145
+
146
+ ## Console serve options
147
+
148
+ `catence-console serve` accepts:
149
+
150
+ | Option | Default | Meaning |
151
+ | --- | --- | --- |
152
+ | `--home <dir>` | `$CATENCE_HOME` or `~/.catence` | Catalog home the runtime serves |
153
+ | `--mcp-url <url>` | `$CATENCE_MCP_URL` or auto-start | If given, waits for an existing runtime instead of starting one |
154
+ | `--ui-host <host>` | `$CATENCE_CONSOLE_HOST` or `127.0.0.1` | Web UI bind address |
155
+ | `--mcp-host <host>` | `127.0.0.1` | Loopback host for the auto-started runtime |
156
+ | `--mcp-port <port>` | `8787` | Loopback port for the auto-started runtime |
157
+ | `--ui-port <port>` | `8000` | Web UI port |
158
+ | `--no-build-ui` | — | Deprecated no-op |
159
+ | `--external-mcp` | — | Deprecated alias for `--mcp-url` |
160
+
161
+ When no `--mcp-url` is given, the Console spawns a matching runtime
162
+ (`catence serve --home <home> --host <mcp-host> --port <mcp-port>
163
+ --allow-origin http://127.0.0.1:<ui-port> --allow-origin http://localhost:<ui-port>`)
164
+ and waits for `GET /health` to succeed within 20 seconds. The runtime and
165
+ Console must agree on the protocol version; a mismatch is rejected before a
166
+ chat starts.
167
+
168
+ ## How multi-athlete works in the Console
169
+
170
+ The Console shares the same per-athlete stores as MCP, but scoping is
171
+ **server-owned, not model-owned**:
172
+
173
+ - The settings panel has an **Athlete** selector built from
174
+ `GET /api/v1/athletes` on the runtime. The default is the catalog's
175
+ `defaultAthleteId`.
176
+ - On every personal-data tool call, the Console **forces** the selected
177
+ athleteId onto the arguments — the model cannot name or switch athletes, and
178
+ a chat's system message states: *"This Console chat is scoped to athleteId X.
179
+ Every Catence data tool call is forced to that athlete; do not try to select
180
+ or compare another athlete."*
181
+ - The athlete choice is persisted per chat thread in the Console's
182
+ `console_preferences` table (`chat-history.sqlite3`), and the chat header
183
+ announces *"This chat is scoped to athlete **<id>**."*
184
+ - `list_athletes` is exempt from forcing so the roster can be rendered.
185
+
186
+ The dashboard is fetched through the authenticated Console origin: Chainlit
187
+ middleware proxies `GET /api/v1/dashboard` and `GET /api/v1/athletes` to the
188
+ runtime only when the Console's JWT cookie is valid (otherwise 401), so raw
189
+ port 8787 does not need to be exposed.
190
+
191
+ ## Model discovery (OpenCode Go)
192
+
193
+ OpenCode Go publishes an OpenAI-compatible API at
194
+ `https://opencode.ai/zen/go/v1`; its model list is public. The discovery
195
+ script fetches it and merges two ready-made profiles into `config.json`:
196
+ `opencode-go` (chat + responses models, base `…/zen/go/v1`) and
197
+ `opencode-go-messages` (messages models, base `…/zen/go`). Existing profiles,
198
+ limits, and `defaultProfile` are preserved unless `--set-default` is passed.
199
+ Re-discovery refreshes each known model's routing reference but keeps your
200
+ custom label, `reasoningEffort`, and `variants`, and keeps models that have
201
+ disappeared from the live catalog.
202
+
203
+ ```sh
204
+ # Local (from a checkout)
205
+ npm run discover:opencode-go -- --write ~/.catence/config.json
206
+ ```
207
+
208
+ Inside the Console's Models page, **Discover OpenCode Go models** runs the
209
+ same merge for the current home without touching athlete data (runtime route
210
+ `POST /api/v1/models/discover`, proxied behind the Console login). Models can
211
+ also be edited in place there — the update action rewrites the `console`
212
+ section atomically and never stores secret values.
213
+
214
+ Then set the credentials and verify:
215
+
216
+ ```sh
217
+ export OPENCODE_GO_API_KEY='…' # any non-empty value passes the key check
218
+ export OPENCODE_GO_API_BASE='https://opencode.ai/zen/go/v1'
219
+ export OPENCODE_GO_MESSAGES_API_BASE='https://opencode.ai/zen/go'
220
+ catence-console doctor
221
+ ```
222
+
223
+ The `openai/responses/…` models (for example `grok-4.5`) go through LiteLLM's
224
+ responses bridge and are the least battle-tested path; smoke-test one chat turn
225
+ before relying on them.
226
+
227
+ ## Updating
228
+
229
+ From `catence-data update`-capable releases (0.2.0-beta.3 and later), the
230
+ runtime updates both components on the tracked channel — beta installs follow
231
+ the npm `beta` tag and matching PyPI prereleases:
232
+
233
+ ```sh
234
+ catence-data update --check # report only; exit 1 when updates are pending
235
+ catence-data update # npm runtime + uv tool catence-console upgrade
236
+ catence-data update --channel stable # move off a beta onto the stable channel
237
+ ```
238
+
239
+ Older betas (for example a beta 2 install) predate the command; upgrade them
240
+ manually once:
241
+
242
+ ```sh
243
+ npm install --global catence@beta
244
+ uv tool install --upgrade catence-console
245
+ ```
246
+
247
+ ## Troubleshooting
248
+
249
+ - **"No Catence config exists"** — should not happen after a wizard run; ensure
250
+ `~/.catence/config.json` exists and contains a `console` section (copy it
251
+ from [`config.example.json`](../../config.example.json)).
252
+ - **"Refusing to initialize"** — the data home contains unrelated files; the
253
+ error lists them. Console artifacts (`config.json`, `console/`, and
254
+ Chainlit's `.files/`, `.chainlit/`, `public/`) are allowed; anything else
255
+ blocks `setup`.
256
+ - **Settings panel has no Model dropdown** — the Console loads profiles from
257
+ `config.json`; write one via the wizard, a manual copy, or the discovery
258
+ script.
259
+ - **Profile not ready** — `catence-console doctor` lists the missing
260
+ environment variables; set them in the shell and restart the Console.
261
+ - **Runtime not reachable** — `catence-console doctor --home "$HOME/.catence"
262
+ --mcp-url http://127.0.0.1:8787/mcp` checks the handshake; confirm the
263
+ runtime is running on that port and the protocol versions agree.
@@ -0,0 +1,329 @@
1
+ # Local MCP deployment
2
+
3
+ This guide runs the Catence MCP server on your machine: install the runtime,
4
+ create a catalog of athlete stores, sync provider data, and connect MCP clients
5
+ over stdio or Streamable HTTP. It does **not** set up the web Console — see
6
+ [`local-console.md`](local-console.md) for that.
7
+
8
+ ## Prerequisites
9
+
10
+ - Node.js 22+
11
+ - Python 3.12+ and [uv](https://docs.astral.sh/uv/) for the Garmin and Strava
12
+ provider workers
13
+ - Provider credentials for the athletes you choose to sync
14
+
15
+ The default catalog home is `~/.catence`. Set `CATENCE_HOME` or pass `--home`
16
+ to use a different location.
17
+
18
+ ## 1. Install
19
+
20
+ ```sh
21
+ npm install --global catence@beta # or catence@latest for stable
22
+ ```
23
+
24
+ This installs two binaries:
25
+
26
+ - `catence` — the MCP server (`catence` starts stdio, `catence serve` starts
27
+ Streamable HTTP, `catence demo` creates a generated store before starting
28
+ stdio).
29
+ - `catence-data` — catalog setup, athlete selection, per-athlete secrets,
30
+ `sync`, `backfill`, provider authentication, retrieval-index rebuilding, and
31
+ `demo` generation.
32
+
33
+ ## 2. Create the catalog and first athlete
34
+
35
+ ```sh
36
+ catence-data setup --athlete alex --label "Alex"
37
+ ```
38
+
39
+ This creates `~/.catence/` (or `$CATENCE_HOME`) with a catalog and the first
40
+ athlete store. It also tolerates a directory that already holds only Console
41
+ artifacts (`config.json`, `console/`).
42
+
43
+ ## 3. Store provider credentials (stdin only, never shell history)
44
+
45
+ Provider values are written to the selected athlete's owner-only local secret
46
+ file (`secrets/providers.json`, mode 0600). Read them through stdin so they do
47
+ not enter shell history:
48
+
49
+ ```sh
50
+ # Garmin
51
+ printf %s 'alex@example.com' | catence-data --athlete alex secret set --provider garmin --field email --value-stdin
52
+ printf %s 'your-garmin-password' | catence-data --athlete alex secret set --provider garmin --field password --value-stdin
53
+ # Intervals.icu
54
+ printf %s 'intervals-api-key' | catence-data --athlete alex secret set --provider intervals --field apiKey --value-stdin
55
+ printf %s '12345' | catence-data --athlete alex secret set --provider intervals --field athleteId --value-stdin
56
+ # Strava (create an API application at https://www.strava.com/settings/api)
57
+ printf %s 'strava-client-id' | catence-data --athlete alex secret set --provider strava --field clientId --value-stdin
58
+ printf %s 'strava-client-secret' | catence-data --athlete alex secret set --provider strava --field clientSecret --value-stdin
59
+ ```
60
+
61
+ Supported fields per provider: Garmin `email` and `password`; Intervals.icu
62
+ `apiKey` and `athleteId`; Strava `clientId` and `clientSecret`. Set only the
63
+ providers you actually sync; `secret set` accepts each field independently.
64
+
65
+ Secrets are injected into provider workers **only** from this per-athlete file;
66
+ any `GARMIN_*`/`INTERVALS_*`/`STRAVA_*` values inherited by the shared process
67
+ are deliberately stripped. An athlete that has not configured a provider
68
+ cannot silently inherit its credentials.
69
+
70
+ ## 4. Sync data and build retrieval context
71
+
72
+ ```sh
73
+ catence-data --athlete alex sync --provider all
74
+ catence-data --athlete alex build-retrieval-index
75
+ ```
76
+
77
+ ## 5. Start the stdio MCP server
78
+
79
+ ```sh
80
+ catence
81
+ ```
82
+
83
+ An agent first calls `list_athletes`, then includes `athleteId` with every
84
+ data tool. This is intentional: a shared agent may access the stores you
85
+ configured, but no tool implicitly aggregates or crosses between athletes.
86
+
87
+ ## Multi-athlete catalog
88
+
89
+ Catence is designed to serve **several isolated athlete stores** from one
90
+ shared server. Each store has its own DuckDB, raw data, and tokens, so
91
+ athletes never leak into each other.
92
+
93
+ ### MCP behavior
94
+
95
+ - `list_athletes` returns `{ defaultAthleteId, athletes: [{ id, label }] }`.
96
+ - Every personal-data tool in catalog mode requires an `athleteId` input (the
97
+ server auto-prepends it to every tool schema). Calls without it fail with
98
+ `athleteId is required. Call list_athletes to inspect the configured catalog.`
99
+ - The `catence://athletes` resource returns the same catalog.
100
+ - Rate limits are tracked **per athlete**: each athlete has its own sliding
101
+ window for server, tool, and resource limits.
102
+ - MCP responses carry an `athlete` field identifying which store served them.
103
+ - `start_detached_sync` is the one explicitly named write tool besides the
104
+ Strava hydration tools: it spawns a detached `catence-data sync` child for
105
+ the selected athlete and returns immediately with the run handle and log
106
+ file. It refuses to start while another sync run is active (`sync_in_progress`);
107
+ follow the run with `catence_sync_progress`.
108
+
109
+ ### Adding another athlete
110
+
111
+ ```sh
112
+ catence-data athlete add --id sam --label "Sam"
113
+ printf %s 'sam@example.com' | catence-data --athlete sam secret set --provider garmin --field email --value-stdin
114
+ printf %s 'sam-password' | catence-data --athlete sam secret set --provider garmin --field password --value-stdin
115
+ catence-data --athlete sam sync --provider garmin
116
+ catence-data athlete list
117
+ ```
118
+
119
+ The catalog layout is deliberately simple:
120
+
121
+ ```text
122
+ ~/.catence/
123
+ catalog.json
124
+ config.json # shared Console configuration; no secrets
125
+ console/chat-history.sqlite3
126
+ athletes/
127
+ alex/ # isolated DuckDB, raw data, and tokens
128
+ secrets/providers.json # mode 0600
129
+ sam/
130
+ ...
131
+ ```
132
+
133
+ Garmin, Intervals, and Strava client credentials are isolated per athlete.
134
+ Strava OAuth tokens remain in that athlete's own store. The old 0.1
135
+ single-store directory cannot be migrated safely: create a fresh home and
136
+ re-sync each athlete.
137
+
138
+ ## Common operations
139
+
140
+ ```sh
141
+ # Incremental or explicit data work for one athlete
142
+ catence-data --athlete alex status
143
+ catence-data --athlete alex sync --provider intervals
144
+ catence-data --athlete alex sync --provider garmin --from 2025-07-29
145
+ catence-data --athlete alex backfill --provider garmin --from 2020-01-01 --refresh
146
+ catence-data --athlete alex retry --run <run-id>
147
+ catence-data --athlete alex progress --watch # follow an active sync run live
148
+
149
+ # Strava OAuth uses clientId and clientSecret previously stored for this athlete
150
+ catence-data --athlete alex auth strava --callback
151
+ catence-data --athlete alex disconnect strava
152
+
153
+ # Update the runtime and Console together on the tracked release channel
154
+ catence-data update --check
155
+ catence-data update
156
+
157
+ # Choose a non-default catalog location
158
+ catence-data --home /srv/catence setup --athlete alex --label "Alex"
159
+ catence --home /srv/catence
160
+ ```
161
+
162
+ `catence-data update` follows the channel of the installed runtime: a beta
163
+ install tracks the npm `beta` tag and the matching PyPI prereleases, a stable
164
+ install tracks `latest`. Pass `--channel stable` to move off a beta. The
165
+ command replaces the global `catence` package and installs or upgrades
166
+ `catence-console` with `uv tool` when `uv` is available; `--check` only
167
+ reports.
168
+
169
+ Register `http://127.0.0.1:8765/strava/callback` with the Strava application
170
+ for the callback flow. The manual `auth strava --code <authorization-code>`
171
+ flow remains available for headless environments.
172
+
173
+ ## Background sync and live progress
174
+
175
+ Long Garmin backfills fetch hundreds of days of data and can take 20-30+
176
+ minutes. Sync runs stream worker output to the console in real time, tolerate
177
+ Ctrl+C and SSH/terminal deaths cleanly, and expose a live progress heartbeat
178
+ you can follow from the CLI, the MCP server, or a Docker helper.
179
+
180
+ ### What happens during a run
181
+
182
+ When a sync starts, Catence creates a row in `sync_runs` with status `running`
183
+ and begins persisting heartbeats to the `sync_run_progress` table (schema
184
+ migration 14). The Garmin staging worker emits one compact JSON progress record
185
+ every few seconds on stdout; the runtime parses it, normalizes it, and upserts
186
+ it. Even if the worker cannot emit — for example while stuck in login — the
187
+ runtime persists a 30-second fallback heartbeat so a run is always observable
188
+ and never falsely looks stale.
189
+
190
+ Each progress record follows the same contract:
191
+
192
+ - `runId`, `provider` — which run the record belongs to
193
+ - `stage` — `starting`, `login`, `singletons`, `daily`, `range`,
194
+ `ftp_history`, `max_metrics`, `hrv_history`, `scores`, `activities`,
195
+ `collections` while extracting; `completed` on success; `failed`,
196
+ `interrupted`, or `timed_out` as terminal states written by the runtime
197
+ - `currentStep` — the endpoint, date, or activity being fetched right now
198
+ - `completedUnits` / `totalUnits` — for example days fetched out of the total
199
+ days in the window
200
+ - `percentComplete` — 0 to 100
201
+ - `elapsedSeconds` / `estimatedRemainingSeconds` — the ETA is computed once the
202
+ run has a known total and has completed at least one unit
203
+ - `heartbeatAt` — UTC ISO timestamp of the record
204
+
205
+ #### Live progress never needs the database lock
206
+
207
+ DuckDB is a single-writer database: a running sync holds an exclusive lock on
208
+ the store file, so no other process — not even a read-only one — can open it
209
+ until the run finishes. To keep progress observable during a run, every
210
+ heartbeat is also written to a sidecar file at
211
+ `staging/garmin/<run-id>.progress.json` (atomic write, no database access).
212
+ `progress`, `catence_sync_progress`, and the MCP server read the sidecar files
213
+ for live runs and only touch the `sync_run_progress` table for recent-run
214
+ history once the lock is free — so they keep working while a backfill holds the
215
+ lock, and never crash with a lock error.
216
+
217
+ Progress also stays live through the entire run, not just worker extraction: a
218
+ 30-second keep-alive covers the JSONL import phase (which emits no worker
219
+ output) and the `intervals` phase of a provider-all sync or backfill.
220
+
221
+ Other read-only tools (`status`, `query_read_only_data`, and friends) still
222
+ need the database and therefore return a clean `data_sync_in_progress` error
223
+ while a sync run is active. That is expected behavior: the store is locked
224
+ until the run completes, then they are served from the same snapshot.
225
+
226
+ ### Interruption and stale-run recovery
227
+
228
+ - SIGINT, SIGTERM, and SIGHUP (Ctrl+C, terminal close, `kill`) are trapped on
229
+ both the Node runtime and the Python worker. The child stops between days and
230
+ the run is marked `interrupted` in `sync_runs` — never a zombie `running`
231
+ row — and the process exits with 130/143/129.
232
+ - Before every sync or retry, runs still stuck in `running` for more than 15
233
+ minutes are marked `timed_out` (with `completed_at` set), so a dead SSH
234
+ session cannot block later syncs.
235
+ - Resume a failed or interrupted run without re-reading history with
236
+ `retry --run <run-id>`.
237
+
238
+ ### Watching progress
239
+
240
+ ```sh
241
+ catence-data --athlete alex progress # one-shot JSON snapshot
242
+ catence-data --athlete alex progress --watch # live view; exits when no run is active
243
+ ```
244
+
245
+ `--watch` renders a compact progress table on a TTY and exits when the last
246
+ active run finishes; piped output stays JSON.
247
+
248
+ The read-only MCP tool `catence_sync_progress` returns the same snapshot —
249
+ active runs with their latest heartbeat plus the most recent runs — and takes
250
+ `athleteId` like every other personal-data tool.
251
+
252
+ ## Safe demo and Glama
253
+
254
+ Run a clearly marked generated dataset without provider accounts:
255
+
256
+ ```sh
257
+ npx --yes catence@beta demo
258
+ ```
259
+
260
+ It creates or reuses `~/.catence-demo` with one `demo` athlete. The generated
261
+ data has explicit caveats in every tool response and never contains personal
262
+ measurements. The Glama registry entry uses this command for **Try in
263
+ Browser**, so the hosted sandbox has no access to local credentials or
264
+ personal data.
265
+
266
+ From a checkout:
267
+
268
+ ```sh
269
+ npm run mcp -- demo
270
+ ```
271
+
272
+ `catence-data demo` and `catence demo` both use `~/.catence-demo` by default
273
+ and refuse to modify an existing directory unless it contains the Catence demo
274
+ marker.
275
+
276
+ ## MCP clients and HTTP
277
+
278
+ For a packaged installation, point a client at `catence`:
279
+
280
+ ```sh
281
+ codex mcp add catence -- catence
282
+ claude mcp add --transport stdio catence -- catence
283
+ ```
284
+
285
+ For a source checkout:
286
+
287
+ ```sh
288
+ codex mcp add catence -- npm --prefix /absolute/path/to/catence run mcp
289
+ ```
290
+
291
+ ### Streamable HTTP server
292
+
293
+ Optional local Streamable HTTP MCP and dashboard APIs:
294
+
295
+ ```sh
296
+ catence serve --host 127.0.0.1 --port 8787
297
+ ```
298
+
299
+ By default it is loopback-only. `CATENCE_HTTP_HOST` and `CATENCE_HTTP_PORT`
300
+ override the host and port when the flags are omitted.
301
+
302
+ - `GET /api/v1/athletes` returns IDs and labels only.
303
+ - `GET /api/v1/dashboard` requires `athleteId`, for example
304
+ `http://127.0.0.1:8787/api/v1/dashboard?athleteId=alex&days=28` (`endDate`
305
+ is an optional `YYYY-MM-DD`; `days` ranges 1-90 and defaults to 28).
306
+ - `POST /api/v1/sync` starts a detached data sync for one athlete:
307
+ `{"athleteId": "alex", "provider": "all", "from": "2026-01-01", "refresh": false}`.
308
+ The spawned child outlives the request, logs to `<home>/logs/`, and the call
309
+ returns `202` with the run handle immediately; an already-active run is
310
+ refused with `409 sync_in_progress`. Pass `"refreshModels": true` to also
311
+ refresh OpenCode Go model profiles first (failure is a non-fatal warning in
312
+ the response).
313
+ - `GET /api/v1/sync/status?athleteId=alex` reports live progress heartbeats,
314
+ recent runs, and best-effort last-completion timestamps per provider. It
315
+ keeps working while a sync holds the write lock (`lastSync` becomes `null`
316
+ then). Finished runs are never reported as active, so a completed or
317
+ crashed run can never wedge the busy guard.
318
+ - `POST /api/v1/models/discover` refreshes the OpenCode Go profiles in
319
+ `<home>/config.json` without starting a data sync:
320
+ `{"setDefault": false}` (both fields optional). Returns the merged profile
321
+ ids, per-route counts, and any prefix-guessed routes.
322
+ - Browser origins must be listed with `--allow-origin` (repeatable). The
323
+ packaged Console instead proxies the dashboard through its authenticated
324
+ same-origin route.
325
+
326
+ Connect HTTP-based clients to `http://127.0.0.1:8787/mcp`. There is **no
327
+ authentication on the MCP server itself** — it trusts the caller's `athleteId`
328
+ parameter — so keep it on loopback or restrict it at the network layer. For
329
+ remote access over SSH: `ssh -L 8787:127.0.0.1:8787 server`.