catence 0.0.0 → 0.2.0-beta.1
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/LICENSE +661 -0
- package/README.md +201 -29
- package/config.example.json +63 -0
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.js +5 -0
- package/dist/contracts/release.d.ts +20 -0
- package/dist/contracts/release.js +17 -0
- package/dist/contracts/runtime.d.ts +11 -0
- package/dist/contracts/runtime.js +2 -0
- package/dist/contracts/staging.d.ts +183 -0
- package/dist/contracts/staging.js +36 -0
- package/dist/contracts/storage.d.ts +15 -0
- package/dist/contracts/storage.js +2 -0
- package/dist/core/dashboard/snapshot.d.ts +16 -0
- package/dist/core/dashboard/snapshot.js +65 -0
- package/dist/core/query/activity-discovery.d.ts +21 -0
- package/dist/core/query/activity-discovery.js +133 -0
- package/dist/core/query/analytics.d.ts +59 -0
- package/dist/core/query/analytics.js +395 -0
- package/dist/core/query/catalog.d.ts +26 -0
- package/dist/core/query/catalog.js +141 -0
- package/dist/core/query/fitness.d.ts +40 -0
- package/dist/core/query/fitness.js +297 -0
- package/dist/core/query/repository.d.ts +19 -0
- package/dist/core/query/repository.js +182 -0
- package/dist/core/query/sql-guard.d.ts +7 -0
- package/dist/core/query/sql-guard.js +121 -0
- package/dist/core/query/sql.d.ts +2 -0
- package/dist/core/query/sql.js +5 -0
- package/dist/core/query/swimming.d.ts +19 -0
- package/dist/core/query/swimming.js +184 -0
- package/dist/core/query/wellness.d.ts +45 -0
- package/dist/core/query/wellness.js +292 -0
- package/dist/core/retrieval/index.d.ts +13 -0
- package/dist/core/retrieval/index.js +248 -0
- package/dist/core/runtime/catalog.d.ts +41 -0
- package/dist/core/runtime/catalog.js +111 -0
- package/dist/core/runtime/configuration.d.ts +71 -0
- package/dist/core/runtime/configuration.js +106 -0
- package/dist/core/runtime/limiter.d.ts +13 -0
- package/dist/core/runtime/limiter.js +17 -0
- package/dist/core/runtime/secrets.d.ts +24 -0
- package/dist/core/runtime/secrets.js +68 -0
- package/dist/elt/application/demo.d.ts +23 -0
- package/dist/elt/application/demo.js +165 -0
- package/dist/elt/application/management.d.ts +25 -0
- package/dist/elt/application/management.js +285 -0
- package/dist/elt/ingestion/importer.d.ts +8 -0
- package/dist/elt/ingestion/importer.js +87 -0
- package/dist/elt/ingestion/providers/intervals.d.ts +21 -0
- package/dist/elt/ingestion/providers/intervals.js +224 -0
- package/dist/elt/ingestion/providers/registry.d.ts +19 -0
- package/dist/elt/ingestion/providers/registry.js +62 -0
- package/dist/elt/ingestion/providers/strava/service.d.ts +42 -0
- package/dist/elt/ingestion/providers/strava/service.js +366 -0
- package/dist/elt/normalization/activities/linking.d.ts +5 -0
- package/dist/elt/normalization/activities/linking.js +165 -0
- package/dist/elt/normalization/normalizers.d.ts +5 -0
- package/dist/elt/normalization/normalizers.js +816 -0
- package/dist/elt/normalization/segments/strava-normalizer.d.ts +3 -0
- package/dist/elt/normalization/segments/strava-normalizer.js +93 -0
- package/dist/elt/storage/artifacts.d.ts +13 -0
- package/dist/elt/storage/artifacts.js +48 -0
- package/dist/elt/storage/database.d.ts +74 -0
- package/dist/elt/storage/database.js +296 -0
- package/dist/elt/storage/migrations.d.ts +3 -0
- package/dist/elt/storage/migrations.js +832 -0
- package/dist/elt/storage/migrations.legacy.d.ts +12 -0
- package/dist/elt/storage/migrations.legacy.js +332 -0
- package/dist/elt/storage/sql.d.ts +2 -0
- package/dist/elt/storage/sql.js +7 -0
- package/dist/elt/storage/write-lock.d.ts +6 -0
- package/dist/elt/storage/write-lock.js +28 -0
- package/dist/elt/streams.d.ts +24 -0
- package/dist/elt/streams.js +62 -0
- package/dist/interfaces/cli/commands.d.ts +2 -0
- package/dist/interfaces/cli/commands.js +166 -0
- package/dist/interfaces/cli/main.d.ts +2 -0
- package/dist/interfaces/cli/main.js +3 -0
- package/dist/interfaces/http/cli-options.d.ts +9 -0
- package/dist/interfaces/http/cli-options.js +81 -0
- package/dist/interfaces/http/server.d.ts +17 -0
- package/dist/interfaces/http/server.js +196 -0
- package/dist/interfaces/mcp/cli-options.d.ts +6 -0
- package/dist/interfaces/mcp/cli-options.js +36 -0
- package/dist/interfaces/mcp/main.d.ts +2 -0
- package/dist/interfaces/mcp/main.js +66 -0
- package/dist/interfaces/mcp/server.d.ts +8 -0
- package/dist/interfaces/mcp/server.js +572 -0
- package/dist/runtime/index.d.ts +33 -0
- package/dist/runtime/index.js +21 -0
- package/docs/architecture.md +100 -0
- package/docs/assets/image.png +0 -0
- package/docs/mcp-activity-retrieval.md +377 -0
- package/package.json +81 -6
- package/pyproject.toml +28 -0
- package/python/__init__.py +1 -0
- package/python/catence/__init__.py +0 -0
- package/python/catence/providers/__init__.py +0 -0
- package/python/catence/providers/garmin/__init__.py +3 -0
- package/python/catence/providers/garmin/cli.py +62 -0
- package/python/catence/providers/garmin/contracts.py +23 -0
- package/python/catence/providers/garmin/registry.py +48 -0
- package/python/catence/providers/garmin/staging.py +87 -0
- package/python/catence/providers/garmin/streams.py +195 -0
- package/python/catence/providers/garmin/worker.py +450 -0
- package/python/catence/providers/strava/__init__.py +3 -0
- package/python/catence/providers/strava/cli.py +404 -0
- package/python/catence/providers/strava/staging.py +73 -0
- package/uv.lock +708 -0
package/README.md
CHANGED
|
@@ -1,45 +1,217 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Catence
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## What is included
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `catence`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
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.
|
|
15
16
|
|
|
16
|
-
##
|
|
17
|
+
## Requirements
|
|
17
18
|
|
|
18
|
-
|
|
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
|
|
19
22
|
|
|
20
|
-
##
|
|
23
|
+
## Quick start
|
|
21
24
|
|
|
22
|
-
|
|
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.
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
```sh
|
|
28
|
+
npm install --global catence@0.2.0
|
|
29
|
+
catence-data setup --athlete alex --label "Alex"
|
|
30
|
+
```
|
|
28
31
|
|
|
29
|
-
|
|
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.
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
34
|
+
```sh
|
|
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
|
|
39
|
+
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
Sync and create retrieval context:
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
43
|
+
```sh
|
|
44
|
+
catence-data --athlete alex sync --provider all
|
|
45
|
+
catence-data --athlete alex build-retrieval-index
|
|
46
|
+
```
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
Start the stdio MCP server:
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
```sh
|
|
51
|
+
catence
|
|
52
|
+
```
|
|
53
|
+
|
|
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.
|
|
55
|
+
|
|
56
|
+
### Add another athlete
|
|
57
|
+
|
|
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
|
+
```
|
|
65
|
+
|
|
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
|
+
```
|
|
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.
|
|
81
|
+
|
|
82
|
+
### Common operations
|
|
83
|
+
|
|
84
|
+
```sh
|
|
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
|
+
|
|
92
|
+
# Strava OAuth uses clientId and clientSecret previously stored for this athlete
|
|
93
|
+
catence-data --athlete alex auth strava --callback
|
|
94
|
+
catence-data --athlete alex disconnect strava
|
|
95
|
+
|
|
96
|
+
# Choose a non-default catalog location
|
|
97
|
+
catence-data --home /srv/catence setup --athlete alex --label "Alex"
|
|
98
|
+
catence --home /srv/catence
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
## Safe demo and Glama
|
|
104
|
+
|
|
105
|
+
Run a clearly marked generated dataset without provider accounts:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
npx --yes catence@0.2.0 demo
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
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.
|
|
112
|
+
|
|
113
|
+
From a checkout:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
npm run mcp -- demo
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## MCP clients and HTTP
|
|
120
|
+
|
|
121
|
+
For a packaged installation, point a client at `catence`:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
codex mcp add catence -- catence
|
|
125
|
+
claude mcp add --transport stdio catence -- catence
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
For a source checkout:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
codex mcp add catence -- npm --prefix /absolute/path/to/catence run mcp
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Optional local Streamable HTTP MCP and dashboard APIs:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
catence serve --host 127.0.0.1 --port 8787
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`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.
|
|
141
|
+
|
|
142
|
+
## Catence Console
|
|
143
|
+
|
|
144
|
+
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.
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
export CATENCE_HOME="$HOME/.catence"
|
|
148
|
+
export CATENCE_CONSOLE_USERNAME='coach'
|
|
149
|
+
export CATENCE_CONSOLE_PASSWORD_HASH="$(catence-console auth hash-password)"
|
|
150
|
+
export CHAINLIT_AUTH_SECRET="$(openssl rand -hex 32)"
|
|
151
|
+
export OPENAI_API_KEY='…'
|
|
152
|
+
|
|
153
|
+
uvx catence-console@0.2.0 serve
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
On the first authenticated visit, the Console wizard asks for an Azure, OpenAI, or Anthropic model and writes only the non-secret model configuration to `~/.catence/config.json`. 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.
|
|
157
|
+
|
|
158
|
+
To preflight the Console and a running runtime:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
uvx catence-console@0.2.0 doctor --home "$HOME/.catence" --mcp-url http://127.0.0.1:8787/mcp
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Console chats and saved tool context are local at `~/.catence/console/chat-history.sqlite3`.
|
|
165
|
+
|
|
166
|
+
### Docker and Cloudflare Tunnel
|
|
167
|
+
|
|
168
|
+
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.
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
docker build -f console/Dockerfile -t catence-console .
|
|
172
|
+
docker run --rm -p 127.0.0.1:8000:8000 -v catence-data:/data \
|
|
173
|
+
-e CATENCE_CONSOLE_USERNAME='coach' \
|
|
174
|
+
-e CATENCE_CONSOLE_PASSWORD_HASH='replace-with-bcrypt-hash' \
|
|
175
|
+
-e CHAINLIT_AUTH_SECRET='replace-with-a-long-random-secret' \
|
|
176
|
+
-e OPENAI_API_KEY='…' \
|
|
177
|
+
catence-console
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Initialize the mounted volume before its first production sync, for example:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
docker run --rm -it -v catence-data:/data --entrypoint catence-data catence-console \
|
|
184
|
+
setup --athlete alex --label "Alex"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
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.
|
|
188
|
+
|
|
189
|
+
## Configuration
|
|
190
|
+
|
|
191
|
+
[`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`.
|
|
192
|
+
|
|
193
|
+
For source development:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
npm ci
|
|
197
|
+
npm run check
|
|
198
|
+
npm test
|
|
199
|
+
UV_CACHE_DIR=$PWD/.cache/uv uv run --project console --group dev python -m pytest console/tests -q
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Beta releases
|
|
203
|
+
|
|
204
|
+
Registry betas use distinct npm and Python prerelease formats: for example,
|
|
205
|
+
`catence@0.2.0-beta.1` pairs with `catence-console==0.2.0b1` and
|
|
206
|
+
`catence-chainlit==0.2.0b1`. The protected `beta` branch verifies candidates;
|
|
207
|
+
a `v0.2.0-beta.1` tag publishes to npm's `beta` channel and creates a GitHub
|
|
208
|
+
prerelease. Follow [the beta release checklist](release/beta-checklist.md) to
|
|
209
|
+
publish the UI wheel first, deploy the registry artifacts with Compose, and
|
|
210
|
+
test the MCP and authenticated Console on a server without touching production
|
|
211
|
+
data.
|
|
212
|
+
|
|
213
|
+
## Data and caveats
|
|
214
|
+
|
|
215
|
+
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.
|
|
216
|
+
|
|
217
|
+
See [distribution notes](docs/distribution.md) for release artifacts, including APM and MCPB demo bundles.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcp": {
|
|
3
|
+
"rateLimits": {
|
|
4
|
+
"server": { "requests": 240, "windowSeconds": 60 },
|
|
5
|
+
"tools": {
|
|
6
|
+
"*": { "requests": 60, "windowSeconds": 60 },
|
|
7
|
+
"hydrate_strava_activity": { "requests": 6, "windowSeconds": 60 },
|
|
8
|
+
"hydrate_recent_strava_activities": { "requests": 1, "windowSeconds": 60 },
|
|
9
|
+
"hydrate_strava_segment_history": { "requests": 4, "windowSeconds": 60 }
|
|
10
|
+
},
|
|
11
|
+
"resources": {
|
|
12
|
+
"*": { "requests": 120, "windowSeconds": 60 }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"providers": {
|
|
17
|
+
"strava": {
|
|
18
|
+
"budget": {
|
|
19
|
+
"maxConcurrentRequests": 1,
|
|
20
|
+
"readRequestsPer15Minutes": 80,
|
|
21
|
+
"readRequestsPerDay": 800
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"console": {
|
|
26
|
+
"defaultProfile": "azure-foundry",
|
|
27
|
+
"limits": {
|
|
28
|
+
"toolRounds": 8,
|
|
29
|
+
"toolResultCharacters": 24000
|
|
30
|
+
},
|
|
31
|
+
"profiles": {
|
|
32
|
+
"azure-foundry": {
|
|
33
|
+
"label": "Azure Foundry",
|
|
34
|
+
"defaultModel": "terra",
|
|
35
|
+
"defaultReasoningEffort": "medium",
|
|
36
|
+
"models": {
|
|
37
|
+
"terra": { "label": "GPT-5.6 Terra", "model": "azure_ai/gpt-5.6-terra" },
|
|
38
|
+
"luna": { "label": "GPT-5.6 Luna", "model": "azure_ai/gpt-5.6-luna" },
|
|
39
|
+
"sol": { "label": "GPT-5.6 Sol", "model": "azure_ai/gpt-5.6-sol" }
|
|
40
|
+
},
|
|
41
|
+
"apiKeyEnv": "AZURE_API_KEY",
|
|
42
|
+
"apiBaseEnv": "AZURE_API_BASE",
|
|
43
|
+
"apiVersionEnv": "AZURE_API_VERSION"
|
|
44
|
+
},
|
|
45
|
+
"opencode": {
|
|
46
|
+
"label": "OpenCode-compatible",
|
|
47
|
+
"model": "openai/your-model-name",
|
|
48
|
+
"apiKeyEnv": "OPENCODE_API_KEY",
|
|
49
|
+
"apiBaseEnv": "OPENCODE_API_BASE"
|
|
50
|
+
},
|
|
51
|
+
"openai": {
|
|
52
|
+
"label": "OpenAI",
|
|
53
|
+
"model": "openai/gpt-5-mini",
|
|
54
|
+
"apiKeyEnv": "OPENAI_API_KEY"
|
|
55
|
+
},
|
|
56
|
+
"anthropic": {
|
|
57
|
+
"label": "Anthropic",
|
|
58
|
+
"model": "anthropic/claude-sonnet-4-5",
|
|
59
|
+
"apiKeyEnv": "ANTHROPIC_API_KEY"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable runtime contract used by independently distributed Catence clients.
|
|
3
|
+
* Keep these values synchronized with release/manifest.json; release checks
|
|
4
|
+
* deliberately reject drift before a package can be published.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CATENCE_RUNTIME_VERSION = "0.2.0-beta.1";
|
|
7
|
+
export declare const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
|
+
export type CatenceRuntimeCapabilities = {
|
|
9
|
+
mcp: true;
|
|
10
|
+
dashboardApi: 1;
|
|
11
|
+
demoStore: true;
|
|
12
|
+
};
|
|
13
|
+
export type CatenceRuntimeHealth = {
|
|
14
|
+
status: 'ok';
|
|
15
|
+
service: 'catence';
|
|
16
|
+
runtimeVersion: string;
|
|
17
|
+
protocolVersion: number;
|
|
18
|
+
capabilities: CatenceRuntimeCapabilities;
|
|
19
|
+
};
|
|
20
|
+
export declare function catenceRuntimeHealth(): CatenceRuntimeHealth;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable runtime contract used by independently distributed Catence clients.
|
|
3
|
+
* Keep these values synchronized with release/manifest.json; release checks
|
|
4
|
+
* deliberately reject drift before a package can be published.
|
|
5
|
+
*/
|
|
6
|
+
export const CATENCE_RUNTIME_VERSION = '0.2.0-beta.1';
|
|
7
|
+
export const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
|
+
export function catenceRuntimeHealth() {
|
|
9
|
+
return {
|
|
10
|
+
status: 'ok',
|
|
11
|
+
service: 'catence',
|
|
12
|
+
runtimeVersion: CATENCE_RUNTIME_VERSION,
|
|
13
|
+
protocolVersion: CATENCE_PROTOCOL_VERSION,
|
|
14
|
+
capabilities: { mcp: true, dashboardApi: 1, demoStore: true },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=release.js.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const PROVIDERS: readonly ["intervals", "garmin", "strava"];
|
|
3
|
+
export type Provider = (typeof PROVIDERS)[number];
|
|
4
|
+
export declare const STAGING_SCHEMA_VERSION = 1;
|
|
5
|
+
export declare const rawObjectSchema: z.ZodObject<{
|
|
6
|
+
kind: z.ZodLiteral<"raw_object">;
|
|
7
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
8
|
+
provider: z.ZodEnum<{
|
|
9
|
+
intervals: "intervals";
|
|
10
|
+
garmin: "garmin";
|
|
11
|
+
strava: "strava";
|
|
12
|
+
}>;
|
|
13
|
+
endpoint: z.ZodString;
|
|
14
|
+
remoteId: z.ZodNullable<z.ZodString>;
|
|
15
|
+
fetchedAt: z.ZodString;
|
|
16
|
+
contentHash: z.ZodString;
|
|
17
|
+
contentType: z.ZodString;
|
|
18
|
+
relativePath: z.ZodString;
|
|
19
|
+
scope: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export declare const runManifestSchema: z.ZodObject<{
|
|
22
|
+
kind: z.ZodLiteral<"run_manifest">;
|
|
23
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
24
|
+
provider: z.ZodEnum<{
|
|
25
|
+
intervals: "intervals";
|
|
26
|
+
garmin: "garmin";
|
|
27
|
+
strava: "strava";
|
|
28
|
+
}>;
|
|
29
|
+
runId: z.ZodString;
|
|
30
|
+
fromDate: z.ZodString;
|
|
31
|
+
createdAt: z.ZodString;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export declare const sourceEntitySchema: z.ZodObject<{
|
|
34
|
+
kind: z.ZodLiteral<"source_entity">;
|
|
35
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
36
|
+
provider: z.ZodEnum<{
|
|
37
|
+
intervals: "intervals";
|
|
38
|
+
garmin: "garmin";
|
|
39
|
+
strava: "strava";
|
|
40
|
+
}>;
|
|
41
|
+
entityType: z.ZodString;
|
|
42
|
+
remoteId: z.ZodString;
|
|
43
|
+
parentRemoteId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
44
|
+
occurredOn: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
45
|
+
sourceUpdatedAt: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
46
|
+
rawObjectHash: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
47
|
+
payload: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
48
|
+
extension: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
export declare const streamManifestSchema: z.ZodObject<{
|
|
51
|
+
kind: z.ZodLiteral<"stream_manifest">;
|
|
52
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
53
|
+
provider: z.ZodEnum<{
|
|
54
|
+
intervals: "intervals";
|
|
55
|
+
garmin: "garmin";
|
|
56
|
+
strava: "strava";
|
|
57
|
+
}>;
|
|
58
|
+
activityRemoteId: z.ZodString;
|
|
59
|
+
relativePath: z.ZodString;
|
|
60
|
+
contentHash: z.ZodString;
|
|
61
|
+
rowCount: z.ZodNumber;
|
|
62
|
+
startAt: z.ZodNullable<z.ZodString>;
|
|
63
|
+
endAt: z.ZodNullable<z.ZodString>;
|
|
64
|
+
columns: z.ZodArray<z.ZodString>;
|
|
65
|
+
rawObjectHash: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
66
|
+
}, z.core.$strip>;
|
|
67
|
+
export declare const activitySyncStateSchema: z.ZodObject<{
|
|
68
|
+
kind: z.ZodLiteral<"activity_sync_state">;
|
|
69
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
70
|
+
provider: z.ZodEnum<{
|
|
71
|
+
intervals: "intervals";
|
|
72
|
+
garmin: "garmin";
|
|
73
|
+
strava: "strava";
|
|
74
|
+
}>;
|
|
75
|
+
activityRemoteId: z.ZodString;
|
|
76
|
+
summaryHash: z.ZodString;
|
|
77
|
+
detailsFetched: z.ZodBoolean;
|
|
78
|
+
}, z.core.$strip>;
|
|
79
|
+
export declare const extractionErrorSchema: z.ZodObject<{
|
|
80
|
+
kind: z.ZodLiteral<"extraction_error">;
|
|
81
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
82
|
+
provider: z.ZodEnum<{
|
|
83
|
+
intervals: "intervals";
|
|
84
|
+
garmin: "garmin";
|
|
85
|
+
strava: "strava";
|
|
86
|
+
}>;
|
|
87
|
+
endpoint: z.ZodString;
|
|
88
|
+
remoteId: z.ZodNullable<z.ZodString>;
|
|
89
|
+
message: z.ZodString;
|
|
90
|
+
retryable: z.ZodDefault<z.ZodBoolean>;
|
|
91
|
+
}, z.core.$strip>;
|
|
92
|
+
export declare const stagingRecordSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
93
|
+
kind: z.ZodLiteral<"run_manifest">;
|
|
94
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
95
|
+
provider: z.ZodEnum<{
|
|
96
|
+
intervals: "intervals";
|
|
97
|
+
garmin: "garmin";
|
|
98
|
+
strava: "strava";
|
|
99
|
+
}>;
|
|
100
|
+
runId: z.ZodString;
|
|
101
|
+
fromDate: z.ZodString;
|
|
102
|
+
createdAt: z.ZodString;
|
|
103
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
104
|
+
kind: z.ZodLiteral<"raw_object">;
|
|
105
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
106
|
+
provider: z.ZodEnum<{
|
|
107
|
+
intervals: "intervals";
|
|
108
|
+
garmin: "garmin";
|
|
109
|
+
strava: "strava";
|
|
110
|
+
}>;
|
|
111
|
+
endpoint: z.ZodString;
|
|
112
|
+
remoteId: z.ZodNullable<z.ZodString>;
|
|
113
|
+
fetchedAt: z.ZodString;
|
|
114
|
+
contentHash: z.ZodString;
|
|
115
|
+
contentType: z.ZodString;
|
|
116
|
+
relativePath: z.ZodString;
|
|
117
|
+
scope: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
118
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
119
|
+
kind: z.ZodLiteral<"source_entity">;
|
|
120
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
121
|
+
provider: z.ZodEnum<{
|
|
122
|
+
intervals: "intervals";
|
|
123
|
+
garmin: "garmin";
|
|
124
|
+
strava: "strava";
|
|
125
|
+
}>;
|
|
126
|
+
entityType: z.ZodString;
|
|
127
|
+
remoteId: z.ZodString;
|
|
128
|
+
parentRemoteId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
129
|
+
occurredOn: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
130
|
+
sourceUpdatedAt: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
131
|
+
rawObjectHash: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
132
|
+
payload: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
133
|
+
extension: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
134
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
135
|
+
kind: z.ZodLiteral<"stream_manifest">;
|
|
136
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
137
|
+
provider: z.ZodEnum<{
|
|
138
|
+
intervals: "intervals";
|
|
139
|
+
garmin: "garmin";
|
|
140
|
+
strava: "strava";
|
|
141
|
+
}>;
|
|
142
|
+
activityRemoteId: z.ZodString;
|
|
143
|
+
relativePath: z.ZodString;
|
|
144
|
+
contentHash: z.ZodString;
|
|
145
|
+
rowCount: z.ZodNumber;
|
|
146
|
+
startAt: z.ZodNullable<z.ZodString>;
|
|
147
|
+
endAt: z.ZodNullable<z.ZodString>;
|
|
148
|
+
columns: z.ZodArray<z.ZodString>;
|
|
149
|
+
rawObjectHash: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
150
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
151
|
+
kind: z.ZodLiteral<"activity_sync_state">;
|
|
152
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
153
|
+
provider: z.ZodEnum<{
|
|
154
|
+
intervals: "intervals";
|
|
155
|
+
garmin: "garmin";
|
|
156
|
+
strava: "strava";
|
|
157
|
+
}>;
|
|
158
|
+
activityRemoteId: z.ZodString;
|
|
159
|
+
summaryHash: z.ZodString;
|
|
160
|
+
detailsFetched: z.ZodBoolean;
|
|
161
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
162
|
+
kind: z.ZodLiteral<"extraction_error">;
|
|
163
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
164
|
+
provider: z.ZodEnum<{
|
|
165
|
+
intervals: "intervals";
|
|
166
|
+
garmin: "garmin";
|
|
167
|
+
strava: "strava";
|
|
168
|
+
}>;
|
|
169
|
+
endpoint: z.ZodString;
|
|
170
|
+
remoteId: z.ZodNullable<z.ZodString>;
|
|
171
|
+
message: z.ZodString;
|
|
172
|
+
retryable: z.ZodDefault<z.ZodBoolean>;
|
|
173
|
+
}, z.core.$strip>], "kind">;
|
|
174
|
+
export type StagingRecord = z.infer<typeof stagingRecordSchema>;
|
|
175
|
+
export type SourceEntity = z.infer<typeof sourceEntitySchema>;
|
|
176
|
+
export type EndpointSpec = {
|
|
177
|
+
readonly provider: Provider;
|
|
178
|
+
readonly name: string;
|
|
179
|
+
readonly scope: 'singleton' | 'date_range' | 'activity' | 'collection' | 'child';
|
|
180
|
+
readonly entityType: string;
|
|
181
|
+
readonly readOnly: true;
|
|
182
|
+
readonly pagination: 'none' | 'offset' | 'provider';
|
|
183
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const PROVIDERS = ['intervals', 'garmin', 'strava'];
|
|
3
|
+
export const STAGING_SCHEMA_VERSION = 1;
|
|
4
|
+
export const rawObjectSchema = z.object({
|
|
5
|
+
kind: z.literal('raw_object'), schemaVersion: z.literal(STAGING_SCHEMA_VERSION), provider: z.enum(PROVIDERS),
|
|
6
|
+
endpoint: z.string().min(1), remoteId: z.string().nullable(), fetchedAt: z.string().datetime(),
|
|
7
|
+
contentHash: z.string().length(64), contentType: z.string().min(1), relativePath: z.string().min(1),
|
|
8
|
+
scope: z.record(z.string(), z.unknown()).default({}),
|
|
9
|
+
});
|
|
10
|
+
export const runManifestSchema = z.object({
|
|
11
|
+
kind: z.literal('run_manifest'), schemaVersion: z.literal(STAGING_SCHEMA_VERSION), provider: z.enum(PROVIDERS),
|
|
12
|
+
runId: z.string().uuid(), fromDate: z.string().date(), createdAt: z.string().datetime(),
|
|
13
|
+
});
|
|
14
|
+
export const sourceEntitySchema = z.object({
|
|
15
|
+
kind: z.literal('source_entity'), schemaVersion: z.literal(STAGING_SCHEMA_VERSION), provider: z.enum(PROVIDERS),
|
|
16
|
+
entityType: z.string().min(1), remoteId: z.string().min(1), parentRemoteId: z.string().nullable().default(null),
|
|
17
|
+
occurredOn: z.string().nullable().default(null), sourceUpdatedAt: z.string().nullable().default(null),
|
|
18
|
+
rawObjectHash: z.string().length(64).nullable().default(null), payload: z.record(z.string(), z.unknown()),
|
|
19
|
+
extension: z.record(z.string(), z.unknown()).default({}),
|
|
20
|
+
});
|
|
21
|
+
export const streamManifestSchema = z.object({
|
|
22
|
+
kind: z.literal('stream_manifest'), schemaVersion: z.literal(STAGING_SCHEMA_VERSION), provider: z.enum(PROVIDERS),
|
|
23
|
+
activityRemoteId: z.string().min(1), relativePath: z.string().min(1), contentHash: z.string().length(64),
|
|
24
|
+
rowCount: z.number().int().nonnegative(), startAt: z.string().nullable(), endAt: z.string().nullable(),
|
|
25
|
+
columns: z.array(z.string()), rawObjectHash: z.string().length(64).nullable().default(null),
|
|
26
|
+
});
|
|
27
|
+
export const activitySyncStateSchema = z.object({
|
|
28
|
+
kind: z.literal('activity_sync_state'), schemaVersion: z.literal(STAGING_SCHEMA_VERSION), provider: z.enum(PROVIDERS),
|
|
29
|
+
activityRemoteId: z.string().min(1), summaryHash: z.string().length(64), detailsFetched: z.boolean(),
|
|
30
|
+
});
|
|
31
|
+
export const extractionErrorSchema = z.object({
|
|
32
|
+
kind: z.literal('extraction_error'), schemaVersion: z.literal(STAGING_SCHEMA_VERSION), provider: z.enum(PROVIDERS),
|
|
33
|
+
endpoint: z.string().min(1), remoteId: z.string().nullable(), message: z.string().min(1), retryable: z.boolean().default(true),
|
|
34
|
+
});
|
|
35
|
+
export const stagingRecordSchema = z.discriminatedUnion('kind', [runManifestSchema, rawObjectSchema, sourceEntitySchema, streamManifestSchema, activitySyncStateSchema, extractionErrorSchema]);
|
|
36
|
+
//# sourceMappingURL=staging.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage boundary used by Catence core. Implementations may use DuckDB or a
|
|
3
|
+
* test double, but core logic does not depend on a database driver.
|
|
4
|
+
*/
|
|
5
|
+
export type QueryValues = Record<string, unknown>;
|
|
6
|
+
export type QueryBindings = QueryValues | unknown[];
|
|
7
|
+
export interface ReadDataStore {
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
rows<T extends Record<string, unknown>>(sql: string, values?: QueryBindings): Promise<T[]>;
|
|
10
|
+
tableNames(query: string): Promise<string[]>;
|
|
11
|
+
interrupt(): void;
|
|
12
|
+
}
|
|
13
|
+
export interface WriteDataStore extends ReadDataStore {
|
|
14
|
+
run(sql: string, values?: QueryBindings): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReadOnlyRepository } from '../query/repository.js';
|
|
2
|
+
export type DashboardOptions = {
|
|
3
|
+
endDate: string;
|
|
4
|
+
days: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Fixed dashboard facts for the local Console.
|
|
8
|
+
*
|
|
9
|
+
* The service deliberately reads Catence's existing canonical projections
|
|
10
|
+
* instead of adding a visualization-specific store or materialized mirror.
|
|
11
|
+
*/
|
|
12
|
+
export declare class DashboardSnapshotService {
|
|
13
|
+
private readonly repository;
|
|
14
|
+
constructor(repository: ReadOnlyRepository);
|
|
15
|
+
snapshot(options: DashboardOptions): Promise<Record<string, unknown>>;
|
|
16
|
+
}
|