catence 0.2.0-beta.9 → 0.2.0
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.
- package/README.md +38 -180
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/progress.d.ts +23 -0
- package/dist/contracts/progress.js +49 -0
- package/dist/contracts/release.d.ts +1 -1
- package/dist/contracts/release.js +1 -1
- package/dist/core/query/activity-discovery.d.ts +2 -0
- package/dist/core/query/activity-discovery.js +1 -1
- package/dist/core/query/analytics.d.ts +10 -0
- package/dist/core/query/analytics.js +494 -0
- package/dist/core/query/catalog.d.ts +1 -1
- package/dist/core/query/catalog.js +8 -4
- package/dist/core/query/events.d.ts +34 -0
- package/dist/core/query/events.js +121 -0
- package/dist/core/query/fitness.d.ts +4 -0
- package/dist/core/query/fitness.js +60 -0
- package/dist/core/query/repository.d.ts +2 -0
- package/dist/core/query/repository.js +48 -0
- package/dist/core/query/swimming.js +2 -2
- package/dist/core/runtime/configuration.d.ts +3 -1
- package/dist/core/runtime/configuration.js +5 -1
- package/dist/core/runtime/opencode-go.d.ts +58 -0
- package/dist/core/runtime/opencode-go.js +246 -0
- package/dist/core/runtime/secrets.js +51 -13
- package/dist/elt/application/detached-sync.d.ts +50 -0
- package/dist/elt/application/detached-sync.js +99 -0
- package/dist/elt/application/management.d.ts +15 -0
- package/dist/elt/application/management.js +420 -18
- package/dist/elt/ingestion/importer.js +1 -1
- package/dist/elt/ingestion/providers/intervals.d.ts +9 -0
- package/dist/elt/ingestion/providers/intervals.js +33 -1
- package/dist/elt/ingestion/providers/registry.d.ts +13 -0
- package/dist/elt/ingestion/providers/registry.js +19 -1
- package/dist/elt/normalization/normalizers.js +207 -15
- package/dist/elt/storage/database.d.ts +21 -2
- package/dist/elt/storage/database.js +182 -6
- package/dist/elt/storage/migrations.js +145 -1
- package/dist/elt/storage/progress-sidecar.d.ts +61 -0
- package/dist/elt/storage/progress-sidecar.js +200 -0
- package/dist/interfaces/cli/commands.js +94 -1
- package/dist/interfaces/http/generation-status.d.ts +19 -0
- package/dist/interfaces/http/generation-status.js +55 -0
- package/dist/interfaces/http/server.d.ts +7 -1
- package/dist/interfaces/http/server.js +184 -1
- package/dist/interfaces/mcp/main.js +33 -1
- package/dist/interfaces/mcp/server.d.ts +2 -1
- package/dist/interfaces/mcp/server.js +168 -45
- package/dist/runtime/index.d.ts +9 -2
- package/dist/runtime/index.js +6 -2
- package/docs/architecture.md +13 -2
- package/docs/assets/image.png +0 -0
- package/docs/configuration.md +243 -0
- package/docs/deployment/docker.md +528 -0
- package/docs/deployment/local-console.md +263 -0
- package/docs/deployment/local-mcp.md +329 -0
- package/docs/development.md +337 -0
- package/docs/distribution.md +45 -0
- package/docs/llm-providers.md +24 -2
- package/docs/mcp-activity-retrieval.md +90 -210
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/python/catence/providers/garmin/cli.py +43 -9
- package/python/catence/providers/garmin/progress.py +85 -0
- package/python/catence/providers/garmin/registry.py +2 -0
- package/python/catence/providers/garmin/streams.py +38 -0
- package/python/catence/providers/garmin/swim_backfill.py +83 -0
- package/python/catence/providers/garmin/worker.py +415 -12
- package/scripts/discover-opencode-go.mjs +36 -144
- package/uv.lock +3 -3
- package/docs/deployment.md +0 -338
package/README.md
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
# Catence
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Local MCP server for primary Garmin data, with added Intervals.icu metrics and Strava enrichment.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
+
## ... what?
|
|
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.
|
|
10
|
+
|
|
7
11
|
## What is included
|
|
8
12
|
|
|
9
13
|
- Garmin, Intervals.icu, and Strava ingestion with source-aware normalization.
|
|
10
|
-
- Read-only MCP tools, plus explicitly named, lock-guarded Strava hydration
|
|
14
|
+
- Read-only MCP tools, plus explicitly named, lock-guarded write tools for Strava hydration and detached syncs.
|
|
11
15
|
- 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.
|
|
16
|
+
- A password-protected Chainlit Console with an authenticated dashboard, a data-sync button with live progress, and in-app model management.
|
|
13
17
|
- A generated demo catalog for safe evaluation in Glama, desktop MCP clients, or local development.
|
|
14
18
|
|
|
15
19
|
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.
|
|
@@ -22,14 +26,16 @@ Catence helps explore recovery, training load, trends, activity detail, swimming
|
|
|
22
26
|
|
|
23
27
|
## Quick start
|
|
24
28
|
|
|
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.
|
|
29
|
+
Install Catence and create the first athlete. The default catalog home is `~/.catence`; set `CATENCE_HOME` or pass `--home` to use a different location.
|
|
30
|
+
|
|
31
|
+
Every `catence-data` command requires `--athlete <id>`; there is no default athlete. Pair it with `--home <dir>` to select the catalog when not using the default home.
|
|
26
32
|
|
|
27
33
|
```sh
|
|
28
|
-
npm install --global catence@
|
|
34
|
+
npm install --global catence@beta # or catence@latest for stable
|
|
29
35
|
catence-data setup --athlete alex --label "Alex"
|
|
30
36
|
```
|
|
31
37
|
|
|
32
|
-
Store each provider value through stdin so it does not enter shell history. Values are written to the selected athlete
|
|
38
|
+
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.
|
|
33
39
|
|
|
34
40
|
```sh
|
|
35
41
|
printf %s 'alex@example.com' | catence-data --athlete alex secret set --provider garmin --field email --value-stdin
|
|
@@ -63,68 +69,35 @@ catence-data --athlete sam sync --provider garmin
|
|
|
63
69
|
catence-data athlete list
|
|
64
70
|
```
|
|
65
71
|
|
|
66
|
-
The
|
|
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
|
-
```
|
|
79
|
-
|
|
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.
|
|
72
|
+
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.
|
|
81
73
|
|
|
82
74
|
### Common operations
|
|
83
75
|
|
|
84
76
|
```sh
|
|
85
|
-
# Incremental or explicit data work for one athlete
|
|
86
77
|
catence-data --athlete alex status
|
|
87
78
|
catence-data --athlete alex sync --provider intervals
|
|
88
79
|
catence-data --athlete alex sync --provider garmin --from 2025-07-29
|
|
89
80
|
catence-data --athlete alex backfill --provider garmin --from 2020-01-01 --refresh
|
|
90
81
|
catence-data --athlete alex retry --run <run-id>
|
|
91
|
-
|
|
92
|
-
# Strava OAuth uses clientId and clientSecret previously stored for this athlete
|
|
82
|
+
catence-data --athlete alex progress --watch
|
|
93
83
|
catence-data --athlete alex auth strava --callback
|
|
94
84
|
catence-data --athlete alex disconnect strava
|
|
95
|
-
|
|
96
|
-
# Update the runtime and Console together on the tracked release channel
|
|
97
85
|
catence-data update --check
|
|
98
86
|
catence-data update
|
|
99
|
-
|
|
100
|
-
# Choose a non-default catalog location
|
|
101
|
-
catence-data --home /srv/catence setup --athlete alex --label "Alex"
|
|
102
|
-
catence --home /srv/catence
|
|
103
87
|
```
|
|
104
88
|
|
|
105
|
-
|
|
106
|
-
install tracks the npm `beta` tag and the matching PyPI prereleases, a stable
|
|
107
|
-
install tracks `latest`. Pass `--channel stable` to move off a beta. The command
|
|
108
|
-
replaces the global `catence` package and installs or upgrades
|
|
109
|
-
`catence-console` with `uv tool` when `uv` is available; `--check` only reports.
|
|
110
|
-
|
|
111
|
-
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.
|
|
112
|
-
|
|
113
|
-
## Safe demo and Glama
|
|
114
|
-
|
|
115
|
-
Run a clearly marked generated dataset without provider accounts:
|
|
116
|
-
|
|
117
|
-
```sh
|
|
118
|
-
npx --yes catence@0.2.0 demo
|
|
119
|
-
```
|
|
89
|
+
## Documentation
|
|
120
90
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
91
|
+
| Document | Covers |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| [Deployment: local MCP](docs/deployment/local-mcp.md) | Install, catalog, secrets, sync, background progress, stdio/HTTP MCP, demo, common operations |
|
|
94
|
+
| [Deployment: local Console](docs/deployment/local-console.md) | Local web chat setup, model config, doctor, updates, troubleshooting |
|
|
95
|
+
| [Deployment: Docker](docs/deployment/docker.md) | One-container stack: deploy script, `.env`, sync.sh, MCP exposure (SSH/Tailscale), Cloudflare Tunnel |
|
|
96
|
+
| [Configuration](docs/configuration.md) | Complete `config.json` schema: rate limits, Strava budget, Console profiles, environment variables |
|
|
97
|
+
| [LLM providers](docs/llm-providers.md) | Model providers (OpenAI, Anthropic, OpenAI-compatible/Azure, Opencode Go/Zen), reasoning effort |
|
|
98
|
+
| [Architecture](docs/architecture.md) | Design rationale, layers, data flow, serving |
|
|
99
|
+
| [MCP activity retrieval](docs/mcp-activity-retrieval.md) | Implementation contract for answering questions, current tools, planned endpoints |
|
|
100
|
+
| [Distribution](docs/distribution.md) | Release artifacts: npm, PyPI, APM, MCPB |
|
|
128
101
|
|
|
129
102
|
## MCP clients and HTTP
|
|
130
103
|
|
|
@@ -147,113 +120,11 @@ Optional local Streamable HTTP MCP and dashboard APIs:
|
|
|
147
120
|
catence serve --host 127.0.0.1 --port 8787
|
|
148
121
|
```
|
|
149
122
|
|
|
150
|
-
`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.
|
|
151
|
-
|
|
152
|
-
## Catence Console
|
|
153
|
-
|
|
154
|
-
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.
|
|
155
|
-
|
|
156
|
-
```sh
|
|
157
|
-
export CATENCE_HOME="$HOME/.catence"
|
|
158
|
-
export CATENCE_CONSOLE_USERNAME='coach'
|
|
159
|
-
export CATENCE_CONSOLE_PASSWORD_HASH="$(catence-console auth hash-password)"
|
|
160
|
-
export CHAINLIT_AUTH_SECRET="$(openssl rand -hex 32)"
|
|
161
|
-
export OPENAI_API_KEY='…'
|
|
162
|
-
|
|
163
|
-
uvx catence-console@0.2.0 serve
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
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.
|
|
167
|
-
|
|
168
|
-
To preflight the Console and a running runtime:
|
|
169
|
-
|
|
170
|
-
```sh
|
|
171
|
-
uvx catence-console@0.2.0 doctor --home "$HOME/.catence" --mcp-url http://127.0.0.1:8787/mcp
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Console chats and saved tool context are local at `~/.catence/console/chat-history.sqlite3`.
|
|
175
|
-
|
|
176
|
-
### Docker and Cloudflare Tunnel
|
|
177
|
-
|
|
178
|
-
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.
|
|
179
|
-
|
|
180
|
-
```sh
|
|
181
|
-
docker build -f console/Dockerfile -t catence-console .
|
|
182
|
-
docker run --rm -p 127.0.0.1:8000:8000 -v catence-data:/data \
|
|
183
|
-
-e CATENCE_CONSOLE_USERNAME='coach' \
|
|
184
|
-
-e CATENCE_CONSOLE_PASSWORD_HASH='replace-with-bcrypt-hash' \
|
|
185
|
-
-e CHAINLIT_AUTH_SECRET='replace-with-a-long-random-secret' \
|
|
186
|
-
-e OPENAI_API_KEY='…' \
|
|
187
|
-
catence-console
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
Initialize the mounted volume before its first production sync, for example:
|
|
191
|
-
|
|
192
|
-
```sh
|
|
193
|
-
docker run --rm -it -v catence-data:/data --entrypoint catence-data catence-console \
|
|
194
|
-
setup --athlete alex --label "Alex"
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
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.
|
|
198
|
-
|
|
199
|
-
[`scripts/deploy-console.sh`](scripts/deploy-console.sh) deploys this stack from
|
|
200
|
-
the public registries and seeds a starter `config.json` (the Console model
|
|
201
|
-
profiles from [`config.example.json`](config.example.json)) into the data
|
|
202
|
-
volume, so the settings panel works before the first chat. Edit
|
|
203
|
-
`/data/config.json` in the volume (or the bind-mounted `--home` directory) to
|
|
204
|
-
change models and defaults, or discover OpenCode Go profiles from the image:
|
|
205
|
-
|
|
206
|
-
```sh
|
|
207
|
-
docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm \
|
|
208
|
-
--entrypoint node console \
|
|
209
|
-
/usr/local/lib/node_modules/catence/scripts/discover-opencode-go.mjs --write /data/config.json
|
|
210
|
-
```
|
|
123
|
+
`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. The MCP server has no authentication of its own.
|
|
211
124
|
|
|
212
125
|
## Configuration
|
|
213
126
|
|
|
214
|
-
[`config.example.json`](config.example.json) documents rate limits, Strava budgets, and Console model profiles.
|
|
215
|
-
|
|
216
|
-
### Per-model reasoning effort
|
|
217
|
-
|
|
218
|
-
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`.
|
|
219
|
-
|
|
220
|
-
**Fixed level** (all requests for the model use it):
|
|
221
|
-
|
|
222
|
-
```json
|
|
223
|
-
{
|
|
224
|
-
"console": {
|
|
225
|
-
"profiles": {
|
|
226
|
-
"opencode-zen": {
|
|
227
|
-
"label": "OpenCode Zen",
|
|
228
|
-
"defaultModel": "deepseek-v4-flash-free",
|
|
229
|
-
"models": {
|
|
230
|
-
"deepseek-v4-flash-free": {
|
|
231
|
-
"label": "DeepSeek V4 Flash Free",
|
|
232
|
-
"model": "openai/deepseek-v4-flash-free",
|
|
233
|
-
"reasoningEffort": "high"
|
|
234
|
-
}
|
|
235
|
-
},
|
|
236
|
-
"apiKeyEnv": "OPENCODE_API_KEY",
|
|
237
|
-
"apiBaseEnv": "OPENCODE_ZEN_API_BASE"
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
**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:
|
|
245
|
-
|
|
246
|
-
```json
|
|
247
|
-
"deepseek-v4-flash-free": {
|
|
248
|
-
"label": "DeepSeek V4 Flash Free",
|
|
249
|
-
"model": "openai/deepseek-v4-flash-free",
|
|
250
|
-
"variants": { "High": "high", "Max": "max" }
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
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`).
|
|
255
|
-
|
|
256
|
-
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.
|
|
127
|
+
[`config.example.json`](config.example.json) documents rate limits, Strava budgets, and Console model profiles. Keep per-athlete provider values in `catence-data secret set`, not in `.env` or `config.json`. The full `config.json` schema —including per-model reasoning effort— is documented in [`docs/configuration.md`](docs/configuration.md) and [`docs/llm-providers.md`](docs/llm-providers.md).
|
|
257
128
|
|
|
258
129
|
For source development:
|
|
259
130
|
|
|
@@ -264,33 +135,20 @@ npm test
|
|
|
264
135
|
UV_CACHE_DIR=$PWD/.cache/uv uv run --project console --group dev python -m pytest console/tests -q
|
|
265
136
|
```
|
|
266
137
|
|
|
267
|
-
##
|
|
138
|
+
## Data and caveats
|
|
268
139
|
|
|
269
|
-
|
|
270
|
-
`catence@0.2.0-beta.1` pairs with `catence-console==0.2.0b1` and
|
|
271
|
-
`catence-chainlit==0.2.0b1`. The protected `beta` branch verifies candidates;
|
|
272
|
-
a `v0.2.0-beta.1` tag publishes to npm's `beta` channel and creates a GitHub
|
|
273
|
-
prerelease. Follow [the beta release checklist](release/beta-checklist.md) to
|
|
274
|
-
publish the UI wheel first, deploy the registry artifacts with Compose, and
|
|
275
|
-
test the MCP and authenticated Console on a server without touching production
|
|
276
|
-
data.
|
|
140
|
+
The current available sources respond to my own used platforms. I used some wrappers:
|
|
277
141
|
|
|
278
|
-
|
|
142
|
+
- [cyberjunky/python-garminconnect](https://github.com/cyberjunky/python-garminconnect)
|
|
143
|
+
- [paladini/node-intervals-icu](https://github.com/paladini/node-intervals-icu)
|
|
144
|
+
- [stravalib/stravalib](https://github.com/stravalib/stravalib)
|
|
279
145
|
|
|
280
|
-
|
|
146
|
+
Strava is only used for segments and gear.
|
|
281
147
|
|
|
282
|
-
|
|
148
|
+
### Caveats
|
|
283
149
|
|
|
284
|
-
|
|
150
|
+
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.
|
|
285
151
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
catence-data sync --athlete martina --provider all --home /data # incremental sync, all providers
|
|
290
|
-
docker compose -f docker-compose.yml --env-file .env exec console \
|
|
291
|
-
catence-data backfill --athlete martina --from 2026-01-01 --home /data # full backfill from a date
|
|
292
|
-
docker compose -f docker-compose.yml --env-file .env exec console \
|
|
293
|
-
catence-data status --athlete martina --home /data # coverage/errors
|
|
294
|
-
docker compose -f docker-compose.yml --env-file .env exec console \
|
|
295
|
-
catence-data athlete list --home /data # list athlete IDs
|
|
296
|
-
```
|
|
152
|
+
- I don't use the official Garmin Connect API. The library uses email/pwd.
|
|
153
|
+
- 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
|
|
154
|
+
`<data-dir>/secrets/strava.json`.
|
package/dist/contracts/index.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const STALE_RUN_TIMEOUT_MS: number;
|
|
2
|
+
export declare const SYNC_STAGES: readonly ["starting", "login", "singletons", "daily", "range", "ftp_history", "max_metrics", "hrv_history", "scores", "activities", "collections", "intervals", "importing", "completed", "interrupted", "failed", "timed_out"];
|
|
3
|
+
export type SyncStage = (typeof SYNC_STAGES)[number];
|
|
4
|
+
export interface SyncProgressState {
|
|
5
|
+
runId: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
stage: SyncStage;
|
|
8
|
+
currentStep: string | null;
|
|
9
|
+
completedUnits: number;
|
|
10
|
+
totalUnits: number | null;
|
|
11
|
+
percentComplete: number;
|
|
12
|
+
elapsedSeconds: number;
|
|
13
|
+
estimatedRemainingSeconds: number | null;
|
|
14
|
+
heartbeatAt: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SyncProgressSnapshot {
|
|
17
|
+
running: SyncProgressState[];
|
|
18
|
+
recent: SyncProgressState[];
|
|
19
|
+
}
|
|
20
|
+
export interface InterruptedRuns {
|
|
21
|
+
runIds: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function normalizeProgress(value: unknown): SyncProgressState | null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const STALE_RUN_TIMEOUT_MS = 15 * 60 * 1000;
|
|
2
|
+
export const SYNC_STAGES = [
|
|
3
|
+
'starting',
|
|
4
|
+
'login',
|
|
5
|
+
'singletons',
|
|
6
|
+
'daily',
|
|
7
|
+
'range',
|
|
8
|
+
'ftp_history',
|
|
9
|
+
'max_metrics',
|
|
10
|
+
'hrv_history',
|
|
11
|
+
'scores',
|
|
12
|
+
'activities',
|
|
13
|
+
'collections',
|
|
14
|
+
'intervals',
|
|
15
|
+
'importing',
|
|
16
|
+
'completed',
|
|
17
|
+
'interrupted',
|
|
18
|
+
'failed',
|
|
19
|
+
'timed_out',
|
|
20
|
+
];
|
|
21
|
+
export function normalizeProgress(value) {
|
|
22
|
+
if (typeof value !== 'object' || value === null) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const record = value;
|
|
26
|
+
const runId = typeof record.runId === 'string' ? record.runId : '';
|
|
27
|
+
const provider = typeof record.provider === 'string' ? record.provider : '';
|
|
28
|
+
if (!runId || !provider) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const stage = SYNC_STAGES.includes(record.stage)
|
|
32
|
+
? record.stage
|
|
33
|
+
: 'starting';
|
|
34
|
+
const numberOr = (value, fallback) => typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
35
|
+
const roundOrNull = (value) => typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.round(value)) : null;
|
|
36
|
+
return {
|
|
37
|
+
runId,
|
|
38
|
+
provider,
|
|
39
|
+
stage,
|
|
40
|
+
currentStep: typeof record.currentStep === 'string' ? record.currentStep : null,
|
|
41
|
+
completedUnits: Math.max(0, Math.round(numberOr(record.completedUnits, 0))),
|
|
42
|
+
totalUnits: roundOrNull(record.totalUnits),
|
|
43
|
+
percentComplete: Math.min(100, Math.max(0, numberOr(record.percentComplete, 0))),
|
|
44
|
+
elapsedSeconds: Math.max(0, numberOr(record.elapsedSeconds, 0)),
|
|
45
|
+
estimatedRemainingSeconds: roundOrNull(record.estimatedRemainingSeconds),
|
|
46
|
+
heartbeatAt: typeof record.heartbeatAt === 'string' ? record.heartbeatAt : new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=progress.js.map
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Keep these values synchronized with release/manifest.json; release checks
|
|
4
4
|
* deliberately reject drift before a package can be published.
|
|
5
5
|
*/
|
|
6
|
-
export declare const CATENCE_RUNTIME_VERSION = "0.2.0
|
|
6
|
+
export declare const CATENCE_RUNTIME_VERSION = "0.2.0";
|
|
7
7
|
export declare const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
8
|
export type CatenceRuntimeCapabilities = {
|
|
9
9
|
mcp: true;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Keep these values synchronized with release/manifest.json; release checks
|
|
4
4
|
* deliberately reject drift before a package can be published.
|
|
5
5
|
*/
|
|
6
|
-
export const CATENCE_RUNTIME_VERSION = '0.2.0
|
|
6
|
+
export const CATENCE_RUNTIME_VERSION = '0.2.0';
|
|
7
7
|
export const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
8
|
export function catenceRuntimeHealth() {
|
|
9
9
|
return {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ReadOnlyRepository } from './repository.js';
|
|
2
2
|
export type FindActivitiesRequest = {
|
|
3
3
|
sports?: string[];
|
|
4
|
+
/** Accepted as a convenience alias for `sports: [sport]`. */
|
|
5
|
+
sport?: string;
|
|
4
6
|
distanceKm?: [number, number];
|
|
5
7
|
nameContains?: string[];
|
|
6
8
|
startDate?: string;
|
|
@@ -49,7 +49,7 @@ export class ActivityDiscoveryService {
|
|
|
49
49
|
this.repository = repository;
|
|
50
50
|
}
|
|
51
51
|
async findActivities(request = {}) {
|
|
52
|
-
const sports = [...new Set((request.sports ?? []).map((sport) => sport.trim()).filter(Boolean))];
|
|
52
|
+
const sports = [...new Set((request.sports ?? (request.sport ? [request.sport] : [])).map((sport) => sport.trim()).filter(Boolean))];
|
|
53
53
|
const names = [...new Set((request.nameContains ?? []).map((name) => name.trim()).filter(Boolean))];
|
|
54
54
|
if (sports.length > 12 || names.length > 12)
|
|
55
55
|
throw new QueryValidationError('sports and nameContains each permit at most 12 values.');
|
|
@@ -19,6 +19,12 @@ export type SeriesRequest = {
|
|
|
19
19
|
cursor?: string;
|
|
20
20
|
pageSize?: number;
|
|
21
21
|
};
|
|
22
|
+
export type DeriveDecouplingRequest = {
|
|
23
|
+
activityId: string;
|
|
24
|
+
};
|
|
25
|
+
export type DeriveGapRequest = {
|
|
26
|
+
activityId: string;
|
|
27
|
+
};
|
|
22
28
|
export declare function compileFilters(dataset: DatasetDefinition, filters?: DataFilter[], startDate?: string, endDate?: string, activityId?: string): {
|
|
23
29
|
where: string;
|
|
24
30
|
values: QueryValues;
|
|
@@ -56,4 +62,8 @@ export declare class AnalyticsService {
|
|
|
56
62
|
xMetric?: string;
|
|
57
63
|
yMetric?: string;
|
|
58
64
|
}): Promise<Record<string, unknown>>;
|
|
65
|
+
deriveDecoupling(request: DeriveDecouplingRequest): Promise<Record<string, unknown>>;
|
|
66
|
+
deriveGap(request: DeriveGapRequest): Promise<Record<string, unknown>>;
|
|
67
|
+
private resolveActivitySource;
|
|
68
|
+
private loadActivitySamples;
|
|
59
69
|
}
|