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.
- package/README.md +28 -82
- package/config.example.json +25 -0
- 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/logging.d.ts +16 -0
- package/dist/core/logging.js +68 -0
- package/dist/core/query/repository.d.ts +2 -0
- package/dist/core/query/repository.js +48 -0
- package/dist/core/runtime/catalog.js +17 -7
- 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/core/runtime/self-update.d.ts +44 -0
- package/dist/core/runtime/self-update.js +138 -0
- 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 +435 -19
- package/dist/elt/ingestion/importer.d.ts +3 -2
- package/dist/elt/ingestion/importer.js +5 -4
- package/dist/elt/ingestion/providers/intervals.d.ts +12 -1
- package/dist/elt/ingestion/providers/intervals.js +37 -2
- package/dist/elt/ingestion/providers/registry.d.ts +13 -0
- package/dist/elt/ingestion/providers/registry.js +18 -1
- package/dist/elt/ingestion/providers/strava/service.js +7 -1
- package/dist/elt/normalization/normalizers.js +111 -15
- package/dist/elt/storage/database.d.ts +21 -2
- package/dist/elt/storage/database.js +184 -6
- package/dist/elt/storage/migrations.js +25 -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 +116 -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/server.d.ts +2 -1
- package/dist/interfaces/mcp/server.js +50 -13
- package/dist/runtime/index.d.ts +8 -1
- package/dist/runtime/index.js +4 -1
- package/docs/architecture.md +11 -1
- 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 +58 -0
- package/docs/llm-providers.md +249 -6
- package/docs/mcp-activity-retrieval.md +37 -30
- package/package.json +5 -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 +1 -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 +372 -11
- package/scripts/discover-opencode-go.mjs +102 -0
- package/scripts/discover-opencode-zen.mjs +218 -0
- 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`.
|