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,337 @@
|
|
|
1
|
+
# Catence development guide
|
|
2
|
+
|
|
3
|
+
This is the from-source counterpart to the three deployment guides. Use it to run, edit, and verify Catence without publishing a beta — locally on your machine and on a remote Docker host where you cannot live-debug until a new image is built.
|
|
4
|
+
|
|
5
|
+
Production installs use published artifacts (`npm install catence@beta` / `uv tool install catence-console` and `scripts/deploy-console.sh` → `console/Dockerfile.registry`). Everything below uses **your checkout directly**, so you can iterate on runtime, ELT, and Console code and test the real volume/config/secret paths before any release.
|
|
6
|
+
|
|
7
|
+
Related references: [`deployment/local-mcp.md`](deployment/local-mcp.md), [`deployment/local-console.md`](deployment/local-console.md), [`deployment/docker.md`](deployment/docker.md), [`configuration.md`](configuration.md), [`llm-providers.md`](llm-providers.md).
|
|
8
|
+
|
|
9
|
+
## Which path when
|
|
10
|
+
|
|
11
|
+
| Goal | Path | Why |
|
|
12
|
+
|------|------|-----|
|
|
13
|
+
| Test ELT, MCP tools, queries, or TypeScript/Python logic | **Local from source** (`tsx` + `uv run`) | Fastest loop; no Docker, no image build |
|
|
14
|
+
| Test the Console UI, Chainlit auth, or the authenticated dashboard proxy | **Local Console from source** (`catence-console serve` against a live `CATENCE_HOME`) | Same stack as production but without a container |
|
|
15
|
+
| Test the Docker volume layout, `/data` seeding, `config.json` / `chat-history.sqlite3` persistence, or `deploy-console.sh`-adjacent wiring | **Docker source build** (`console/Dockerfile`) | One `docker build` from the checkout — no registry, no `CATENCE_NPM_VERSION` |
|
|
16
|
+
| Iterate inside a container without rebuilding (host file → container) | **Docker live bind-mount** (override entrypoint/command, keep `/data` on a volume) | Edit on host, restart container, re-test |
|
|
17
|
+
|
|
18
|
+
Most changes do not need Docker at all. Use Docker only when you are explicitly testing the container boundary (file permissions, volume seeding, Console networking, MCP bind).
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
- Node.js 22+
|
|
23
|
+
- Python 3.12 ≤ version < 3.14, with [`uv`](https://docs.astral.sh/uv/)
|
|
24
|
+
- `git`, `curl`
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
git clone https://github.com/rifusaki/catence.git
|
|
28
|
+
cd catence
|
|
29
|
+
npm ci
|
|
30
|
+
uv sync --project console
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
No install step writes to a data home. The data home is created only by `catence-data setup` / `catence-data demo`.
|
|
34
|
+
|
|
35
|
+
## Local from source (no Docker, no beta)
|
|
36
|
+
|
|
37
|
+
### Choose a catalog home
|
|
38
|
+
|
|
39
|
+
The runtime, CLI, and Console all resolve the same home (`~/.catence` by default). Point it elsewhere with `CATENCE_HOME` or per-command `--home` so development never touches a personal catalog.
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
# Isolated home for this checkout/session (recommended for development)
|
|
43
|
+
export CATENCE_HOME="$PWD/.catence-dev"
|
|
44
|
+
catence-data setup --athlete alex --label "Alex"
|
|
45
|
+
# or explicitly: catence-data --home "$PWD/.catence-dev" setup --athlete alex --label "Alex"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The layout after `setup` is the same one Docker later mounts at `/data` — see [`configuration.md`](configuration.md) and [`deployment/local-console.md`](deployment/local-console.md#what-gets-stored-where):
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
$CATENCE_HOME/
|
|
52
|
+
catalog.json
|
|
53
|
+
config.json # Console model profiles — no secrets
|
|
54
|
+
console/chat-history.sqlite3
|
|
55
|
+
athletes/alex/
|
|
56
|
+
secrets/providers.json # mode 0600, written only via `secret set --value-stdin`
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Tear down a dev home with `rm -rf "$CATENCE_HOME"`; it is never committed (covered by `.gitignore`).
|
|
60
|
+
|
|
61
|
+
For a no-credentials smoke test, use the generated dataset instead:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
catence-data demo # creates/reuses ~/.catence-demo with athlete `demo`
|
|
65
|
+
npm run mcp -- demo # stdio MCP against the demo catalog
|
|
66
|
+
# The demo home is fixed to ~/.catence-demo; pass --home to move it.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Provider credentials — file (production) or env (dev)
|
|
70
|
+
|
|
71
|
+
**Preferred (production):** credentials are stored per athlete in `secrets/providers.json` (mode 0600) — never in `config.json`. Same interface as [`deployment/local-mcp.md`](deployment/local-mcp.md#3-store-provider-credentials-stdin-only-never-shell-history):
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
# Garmin
|
|
75
|
+
printf %s 'alex@example.com' | npm run catence-data -- --athlete alex secret set --provider garmin --field email --value-stdin
|
|
76
|
+
printf %s 'your-garmin-password' | npm run catence-data -- --athlete alex secret set --provider garmin --field password --value-stdin
|
|
77
|
+
# Intervals.icu
|
|
78
|
+
printf %s 'intervals-api-key' | npm run catence-data -- --athlete alex secret set --provider intervals --field apiKey --value-stdin
|
|
79
|
+
printf %s '12345' | npm run catence-data -- --athlete alex secret set --provider intervals --field athleteId --value-stdin
|
|
80
|
+
# Strava (create an API application at https://www.strava.com/settings/api)
|
|
81
|
+
printf %s 'strava-client-id' | npm run catence-data -- --athlete alex secret set --provider strava --field clientId --value-stdin
|
|
82
|
+
printf %s 'strava-client-secret' | npm run catence-data -- --athlete alex secret set --provider strava --field clientSecret --value-stdin
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
File values always win. Without a file entry the worker's inherited `GARMIN_*` / `INTERVALS_*` / `STRAVA_*` are normally stripped so an unset provider cannot silently inherit another athlete's credentials.
|
|
86
|
+
|
|
87
|
+
**Easier for local/Docker dev (`src/core/runtime/secrets.ts:51`):** when a `providers.json` field is missing, the worker also checks env fallbacks:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
# Option A — per-athlete scoped (always allowed, multi-athlete safe, no flag)
|
|
91
|
+
export CATENCE_ATHLETE_ALEX_GARMIN_EMAIL='alex@example.com'
|
|
92
|
+
export CATENCE_ATHLETE_ALEX_GARMIN_PASSWORD='…'
|
|
93
|
+
export CATENCE_ATHLETE_ALEX_INTERVALS_API_KEY='…'
|
|
94
|
+
export CATENCE_ATHLETE_ALEX_INTERVALS_ATHLETE_ID='12345'
|
|
95
|
+
export CATENCE_ATHLETE_ALEX_STRAVA_CLIENT_ID='…'
|
|
96
|
+
export CATENCE_ATHLETE_ALEX_STRAVA_CLIENT_SECRET='…'
|
|
97
|
+
# Prefix is CATENCE_ATHLETE_<ID>_ where <ID> is the athlete id upper-cased with '-' → '_'.
|
|
98
|
+
|
|
99
|
+
# Option B — generic (single-athlete dev, opt-in via flag)
|
|
100
|
+
export CATENCE_ALLOW_ENV_SECRETS=1 # or CATENCE_SECRETS_FROM_ENV=1 — values 1/true/yes/on
|
|
101
|
+
export GARMIN_EMAIL='alex@example.com'
|
|
102
|
+
export GARMIN_PASSWORD='…'
|
|
103
|
+
export INTERVALS_API_KEY='…'
|
|
104
|
+
export INTERVALS_ATHLETE_ID='12345'
|
|
105
|
+
export STRAVA_CLIENT_ID='…'
|
|
106
|
+
export STRAVA_CLIENT_SECRET='…'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Resolution per field: `providers.json` → `CATENCE_ATHLETE_<ID>_<VAR>` → (if `CATENCE_ALLOW_ENV_SECRETS` set) `VAR`. For Docker, put these in `.env.dev` and pass through `environment:` in `docker-compose.dev.yml` (or `--env-file .env.dev`); the `secret set --value-stdin` flow still works inside `docker compose run --entrypoint catence-data` for one-off seeding. See [`configuration.md`](configuration.md#provider-credentials-via-env-dev-fallback) and `.env.example`.
|
|
110
|
+
|
|
111
|
+
### Model profiles (where model definitions live)
|
|
112
|
+
|
|
113
|
+
`config.json` in the data home holds **definitions only** — profile ids, LiteLLM model names, and the *names* of environment variables that hold keys/URLs (`apiKeyEnv`, `apiBaseEnv`, `apiVersionEnv`). The values stay in the process environment. See [`configuration.md`](configuration.md#consoleprofiles) and [`llm-providers.md`](llm-providers.md) for the schema.
|
|
114
|
+
|
|
115
|
+
For development, three options (all write to `config.json` in `CATENCE_HOME`):
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
# 1) Start from the documented example
|
|
119
|
+
cp config.example.json "$CATENCE_HOME/config.json"
|
|
120
|
+
# then set the env var the profile names, e.g. OPENAI_API_KEY / OPENCODE_API_KEY
|
|
121
|
+
|
|
122
|
+
# 2) Let the Console wizard create one on first chat (replaces the whole `console` section)
|
|
123
|
+
# 3) Merge OpenCode Go / Zen profiles without clobbering existing ones
|
|
124
|
+
npm run discover:opencode-go -- --write "$CATENCE_HOME/config.json"
|
|
125
|
+
npm run discover:opencode-zen -- --write "$CATENCE_HOME/config.json" --free-only
|
|
126
|
+
|
|
127
|
+
# Verify
|
|
128
|
+
npm run console:doctor -- --home "$CATENCE_HOME" --mcp-url http://127.0.0.1:8787/mcp
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Changing `config.json` is picked up on the next Console request — no repository file tracks deployment-specific models or keys. Keep `config.example.json` as the canonical checked-in example.
|
|
132
|
+
|
|
133
|
+
### Running MCP, ELT, and tests without building `dist`
|
|
134
|
+
|
|
135
|
+
`package.json` exposes `tsx` dev scripts so edits are live without `npm run build`. Large edits should still pass `npm run check` (`tsc --noEmit` plus the runtime-boundary guard):
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
npm run mcp -- --help
|
|
139
|
+
npm run mcp -- --home "$CATENCE_HOME" # stdio MCP (same wire as the packaged `catence` bin)
|
|
140
|
+
npm run catence-data -- --home "$CATENCE_HOME" status --athlete alex
|
|
141
|
+
npm run catence-data -- --home "$CATENCE_HOME" sync --athlete alex --provider garmin --from 2025-07-29
|
|
142
|
+
npm run catence-data -- --home "$CATENCE_HOME" build-retrieval-index --athlete alex
|
|
143
|
+
|
|
144
|
+
# Hot-reload loop for MCP iteration (restarts on src/ change)
|
|
145
|
+
npx tsx watch src/interfaces/mcp/main.ts --home "$CATENCE_HOME"
|
|
146
|
+
|
|
147
|
+
# Tests (DuckDB integration tests use a 15s per-test timeout)
|
|
148
|
+
npm test
|
|
149
|
+
npm run test:watch
|
|
150
|
+
npm run check
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`npm run mcp` and `npm run catence-data` are thin wrappers for `tsx src/interfaces/{mcp,cli}/main.ts`; the packaged `dist/` is only needed for `npm pack` / `prepublishOnly` validation (`npm run release:check`).
|
|
154
|
+
|
|
155
|
+
### Running the Console locally against the dev catalog
|
|
156
|
+
|
|
157
|
+
The Console normally auto-starts a matching packaged runtime (`npx catence@<pinned>`). For development, point it at your live `tsx` runtime instead via `--mcp-url`:
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
# Terminal 1 — live MCP/ELT runtime on loopback (no `--` separator; tsx
|
|
161
|
+
# forwards arguments verbatim, so a bare `--` would reach the CLI unparsed)
|
|
162
|
+
npx tsx watch src/interfaces/mcp/main.ts serve --home "$CATENCE_HOME" --host 127.0.0.1 --port 8787
|
|
163
|
+
|
|
164
|
+
# Terminal 2 — Console using that runtime (no auto-started `npx` runtime).
|
|
165
|
+
# The `console` npm script already ends in the `serve` subcommand, so extra
|
|
166
|
+
# arguments after `--` are appended AFTER `serve` — never repeat it here.
|
|
167
|
+
export CATENCE_CONSOLE_USERNAME='coach'
|
|
168
|
+
export CATENCE_CONSOLE_PASSWORD_HASH="$(uv run --project console catence-console auth hash-password)"
|
|
169
|
+
export CHAINLIT_AUTH_SECRET="$(openssl rand -hex 32)"
|
|
170
|
+
export OPENAI_API_KEY='…' # or ANTHROPIC_API_KEY / OPENCODE_API_KEY …
|
|
171
|
+
npm run console -- --home "$CATENCE_HOME" --mcp-url http://127.0.0.1:8787/mcp --ui-port 8000
|
|
172
|
+
# open http://127.0.0.1:8000
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`--mcp-url` is the feature that makes the live bind-mount loop below possible — the same flag works when the runtime lives inside a container (see next section).
|
|
176
|
+
|
|
177
|
+
## Docker without publishing a beta
|
|
178
|
+
|
|
179
|
+
### Why `scripts/deploy-console.sh` forces a publish
|
|
180
|
+
|
|
181
|
+
The production scaffold (`scripts/deploy-console.sh`) resolves `CATENCE_NPM_VERSION` / `CATENCE_CONSOLE_VERSION` from the **public registries** and bakes `console/Dockerfile.registry` — a three-line image that runs `npm install --global catence@${CATENCE_NPM_VERSION}` and `uv pip install catence-console==${CATENCE_CONSOLE_VERSION}`. Rebuilding that scaffold on a server cannot test local edits until they exist on npm/PyPI, which is the bottleneck this guide removes.
|
|
182
|
+
|
|
183
|
+
### Source build — `console/Dockerfile` (reliable, still requires a rebuild)
|
|
184
|
+
|
|
185
|
+
`console/Dockerfile` is the from-source image: build context is the **repository root**, it runs `npm ci && npm run build && npm install --global .`, then `uv sync --project console`. Build it directly in any checkout, on any server with Docker:
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
# From the repository root — tagged independently from the registry channel tags
|
|
189
|
+
docker build -f console/Dockerfile -t catence-console:dev .
|
|
190
|
+
|
|
191
|
+
# Run it the same way `docker.md` does, but with your local env
|
|
192
|
+
docker run --rm -p 127.0.0.1:8000:8000 -v catence-dev-data:/data \
|
|
193
|
+
-e CATENCE_CONSOLE_USERNAME='coach' \
|
|
194
|
+
-e CATENCE_CONSOLE_PASSWORD_HASH='replace-with-bcrypt-hash' \
|
|
195
|
+
-e CHAINLIT_AUTH_SECRET='replace-with-a-long-random-secret' \
|
|
196
|
+
-e OPENAI_API_KEY='…' \
|
|
197
|
+
catence-console:dev
|
|
198
|
+
|
|
199
|
+
# Or bind-mount a host data directory for easy inspection
|
|
200
|
+
mkdir -p /srv/catence-dev
|
|
201
|
+
docker run --rm -p 127.0.0.1:8000:8000 -v /srv/catence-dev:/data \
|
|
202
|
+
--env-file .env.dev \
|
|
203
|
+
catence-console:dev
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Seed and inspect the volume exactly as in [`deployment/docker.md`](deployment/docker.md):
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
docker run --rm -v catence-dev-data:/data --entrypoint catence-data catence-console:dev setup --athlete alex --label "Alex" --home /data
|
|
210
|
+
docker run --rm -v catence-dev-data:/data --entrypoint sh catence-console:dev -c 'ls -R /data; cat /data/config.json'
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Rebuilding the image is the only step that reflects code changes under this path; the data volume and `config.json` are preserved across rebuilds.
|
|
214
|
+
|
|
215
|
+
### Live loop — bind-mount the checkout into the container (no rebuild per edit)
|
|
216
|
+
|
|
217
|
+
When you need to edit on the server itself, mount the checkout into a dev container and have the entrypoint run the `tsx` watcher / `uv run` directly. The scaffold below is intentionally small — it keeps the **data volume** as the only state, so Console auth and `config.json` behave identically to production.
|
|
218
|
+
|
|
219
|
+
Minimal override compose (save as `docker-compose.dev.yml` alongside the checkout):
|
|
220
|
+
|
|
221
|
+
```yaml
|
|
222
|
+
services:
|
|
223
|
+
console:
|
|
224
|
+
build:
|
|
225
|
+
context: .
|
|
226
|
+
dockerfile: console/Dockerfile
|
|
227
|
+
image: catence-console:dev
|
|
228
|
+
ports:
|
|
229
|
+
- "127.0.0.1:8000:8000"
|
|
230
|
+
- "127.0.0.1:8787:8787"
|
|
231
|
+
environment:
|
|
232
|
+
CATENCE_HOME: /data
|
|
233
|
+
CATENCE_CONSOLE_USERNAME: ${CATENCE_CONSOLE_USERNAME:-}
|
|
234
|
+
CATENCE_CONSOLE_PASSWORD_HASH: ${CATENCE_CONSOLE_PASSWORD_HASH:-}
|
|
235
|
+
CHAINLIT_AUTH_SECRET: ${CHAINLIT_AUTH_SECRET:-}
|
|
236
|
+
# Provider credentials via env (see "Provider credentials" above):
|
|
237
|
+
# Per-athlete scoped vars work without a flag; generic vars need CATENCE_ALLOW_ENV_SECRETS=1.
|
|
238
|
+
CATENCE_ALLOW_ENV_SECRETS: ${CATENCE_ALLOW_ENV_SECRETS:-}
|
|
239
|
+
GARMIN_EMAIL: ${GARMIN_EMAIL:-}
|
|
240
|
+
GARMIN_PASSWORD: ${GARMIN_PASSWORD:-}
|
|
241
|
+
INTERVALS_API_KEY: ${INTERVALS_API_KEY:-}
|
|
242
|
+
INTERVALS_ATHLETE_ID: ${INTERVALS_ATHLETE_ID:-}
|
|
243
|
+
STRAVA_CLIENT_ID: ${STRAVA_CLIENT_ID:-}
|
|
244
|
+
STRAVA_CLIENT_SECRET: ${STRAVA_CLIENT_SECRET:-}
|
|
245
|
+
# Per-athlete example (add your athlete id upper-cased, '-' → '_'):
|
|
246
|
+
# CATENCE_ATHLETE_ALEX_GARMIN_EMAIL: ${CATENCE_ATHLETE_ALEX_GARMIN_EMAIL:-}
|
|
247
|
+
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
|
248
|
+
OPENAI_API_BASE: ${OPENAI_API_BASE:-}
|
|
249
|
+
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
|
250
|
+
OPENCODE_API_KEY: ${OPENCODE_API_KEY:-}
|
|
251
|
+
OPENCODE_GO_API_KEY: ${OPENCODE_GO_API_KEY:-}
|
|
252
|
+
OPENCODE_GO_API_BASE: ${OPENCODE_GO_API_BASE:-}
|
|
253
|
+
OPENCODE_GO_MESSAGES_API_BASE: ${OPENCODE_GO_MESSAGES_API_BASE:-}
|
|
254
|
+
OPENCODE_ZEN_API_BASE: ${OPENCODE_ZEN_API_BASE:-}
|
|
255
|
+
OPENCODE_ZEN_RESPONSES_API_BASE: ${OPENCODE_ZEN_RESPONSES_API_BASE:-}
|
|
256
|
+
volumes:
|
|
257
|
+
- catence-dev-data:/data # keep Console data on the volume
|
|
258
|
+
- .:/app # live source
|
|
259
|
+
- /app/node_modules # keep container's installed deps
|
|
260
|
+
- /app/console/.venv # keep container's venv
|
|
261
|
+
- /app/dist # keep container build isolated (see .dockerignore)
|
|
262
|
+
- /app/.catence-dev # hide host dev home from container
|
|
263
|
+
- /app/.catence # hide host home from container
|
|
264
|
+
command: >-
|
|
265
|
+
sh -c "npm run build &&
|
|
266
|
+
uv run --project /app/console catence-console serve
|
|
267
|
+
--home /data --ui-host 0.0.0.0 --ui-port 8000
|
|
268
|
+
--mcp-host 0.0.0.0 --mcp-port 8787"
|
|
269
|
+
# for no-restart TS hot-reload, replace the command with:
|
|
270
|
+
# npx tsx watch src/interfaces/mcp/main.ts serve --home /data --host 0.0.0.0 --port 8787
|
|
271
|
+
# and run Console separately with --mcp-url
|
|
272
|
+
|
|
273
|
+
volumes:
|
|
274
|
+
catence-dev-data:
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
# .env.dev — never committed
|
|
279
|
+
cat > .env.dev <<'EOF'
|
|
280
|
+
CATENCE_CONSOLE_USERNAME=coach
|
|
281
|
+
CATENCE_CONSOLE_PASSWORD_HASH='replace-with-bcrypt-hash'
|
|
282
|
+
CHAINLIT_AUTH_SECRET='replace-with-long-random-string'
|
|
283
|
+
OPENAI_API_KEY='…'
|
|
284
|
+
# Provider creds via env (easier than secret set for dev) — either:
|
|
285
|
+
# Per-athlete (always, multi-athlete safe): CATENCE_ATHLETE_ALEX_GARMIN_EMAIL=…
|
|
286
|
+
# Generic (needs flag): CATENCE_ALLOW_ENV_SECRETS=1 + GARMIN_EMAIL=…
|
|
287
|
+
EOF
|
|
288
|
+
|
|
289
|
+
docker compose -f docker-compose.dev.yml --env-file .env.dev up --build
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Edits to `src/` and `console/catence_console/` on the host are visible inside; `docker compose restart console` picks up TypeScript changes after `npm run build`, while adding `npx tsx watch` to the command removes even that step. The `/data` volume is still the authority for `config.json`, `secrets/providers.json`, and `console/chat-history.sqlite3`, so the same `catence-data secret set --value-stdin --home /data` flow works via:
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
docker compose -f docker-compose.dev.yml --env-file .env.dev exec console \
|
|
296
|
+
catence-data --home /data status --athlete alex
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
An alternative that avoids running `npm` inside the mount loop is to keep the runtime on the host entirely: run `npx tsx watch src/interfaces/mcp/main.ts serve --home /srv/catence-dev ...` on the host and start the Console container with `--mcp-url http://host.docker.internal:8787/mcp` (or the host's Tailscale IP) and `--env-file .env.dev` — the same `--mcp-url` trick from the local Console section.
|
|
300
|
+
|
|
301
|
+
## Where secrets and model definitions live (dev and Docker)
|
|
302
|
+
|
|
303
|
+
| What | Local dev | Docker dev/prod | Notes |
|
|
304
|
+
|------|-----------|-----------------|-------|
|
|
305
|
+
| Catalog + athlete stores | `CATENCE_HOME` (default `~/.catence`, dev: `$PWD/.catence-dev`) | `/data` (volume or bind mount) | Same on-disk layout; `CATENCE_HOME` controls it in both modes |
|
|
306
|
+
| Provider credentials (Garmin, Intervals, Strava) | `CATENCE_HOME/athletes/<id>/secrets/providers.json` (mode 0600) **or** env fallback when file field missing | `/data/athletes/<id>/secrets/providers.json` or same env fallback inside container | File wins; env fallback is `CATENCE_ATHLETE_<ID>_<VAR>` (always) or bare `GARMIN_*`/`INTERVALS_*`/`STRAVA_*` when `CATENCE_ALLOW_ENV_SECRETS=1` — see § Provider credentials above. Bare provider env vars are otherwise stripped before workers start |
|
|
307
|
+
| Model profiles + limits (`console.profiles`, `console.limits`) | `CATENCE_HOME/config.json` | `/data/config.json` (seeded from `config.example.json` on first deploy) | Versioned definitions + env-var names only. Credentials are read from the environment at serve time |
|
|
308
|
+
| API keys / base URLs | Process env (shell `export`, Docker `--env-file .env.dev`) | `.env` / `.env.dev` passed via `environment:` / `--env-file` | Named by the profile's `apiKeyEnv`/`apiBaseEnv`/`apiVersionEnv` (`OPENAI_API_KEY`, `OPENCODE_API_KEY`, `OPENCODE_GO_API_KEY`, …). `catence-console doctor` reports missing vars without printing values |
|
|
309
|
+
| Chat history | `CATENCE_HOME/console/chat-history.sqlite3` | `/data/console/chat-history.sqlite3` | Shared between runs when the volume is reused |
|
|
310
|
+
|
|
311
|
+
Recommended dev defaults:
|
|
312
|
+
|
|
313
|
+
- Keep a dedicated `CATENCE_HOME` (or local bind-mount path) per checkout or per test run; do not point development at a personal `~/.catence` catalog with real provider tokens.
|
|
314
|
+
- Commit no secrets: `.env*` (including `.env.dev`/`.env.beta`) and checkout-local `.catence-*` homes (which contain `athletes/<id>/secrets/providers.json`) are already ignored by `.gitignore`; the default `~/.catence` is outside the repo and naturally untracked.
|
|
315
|
+
- Treat `config.example.json` as the canonical model-definition template — copy its `console` section into the dev home's `config.json` and set only the one provider key you are testing.
|
|
316
|
+
|
|
317
|
+
## Iteration notes
|
|
318
|
+
|
|
319
|
+
- Re-run `npm run check && npm test` before pushing; `vitest` integration tests create and migrate real DuckDB stores with a 15 s per-test timeout.
|
|
320
|
+
- `catence-data update` tracks the registry channel (`latest` vs `beta`); from source the update path is `git pull`, `npm ci && npm run build`, `uv sync --project console`.
|
|
321
|
+
- The deployed scaffold's `sync.sh` / `hash-password.sh` / `doctor.sh` helpers are generated by `scripts/deploy-console.sh`; the dev container does not generate them — use the `npm run` / `uv run` equivalents above, or run the same `catence-data --home /data` commands via `docker compose exec`.
|
|
322
|
+
- If the Console settings panel shows no Model dropdown, the data home's `config.json` has no `console.profiles` — re-copy `config.example.json` or re-run the discovery script, then restart the Console.
|
|
323
|
+
|
|
324
|
+
### Status vs Dashboard
|
|
325
|
+
|
|
326
|
+
The **Dashboard** (`/dashboard`) is now a pure analytics view — it renders charts and recent activities from `GET /api/v1/dashboard` and, when a detached sync holds the DuckDB write lock, shows a non-blocking amber banner (“Data temporarily unavailable — a sync is running. View live progress on the Status page.”) instead of a full-page `dashboard_unavailable` error. The **Status** page (`/status`) is the lock-safe control plane: it reads `GET /api/v1/health` (runtime `catenceRuntimeHealth()`), `GET /api/v1/sync/status` (sidecar heartbeats + `lastSync`), and `GET /api/v1/athletes` (roster), so it never needs a successful `dashboardSnapshot` and never 400s under lock. The interactive **Sync** button (with instant grey-out via `startingRef`) lives only on Status; Dashboard keeps a read-only “Syncing…” pill and a “Sync moved to Status →” banner.
|
|
327
|
+
|
|
328
|
+
## Troubleshooting
|
|
329
|
+
|
|
330
|
+
- **Doctor reports runtime unreachable** — the Console was started without `--mcp-url` and its auto-started runtime is on a different port. Start the runtime explicitly and pass `--mcp-url http://127.0.0.1:8787/mcp` to the Console, or use `uv run --project console catence-console doctor --home "$CATENCE_HOME" --mcp-url http://127.0.0.1:8787/mcp`.
|
|
331
|
+
- **Docker `run` container has a separate loopback** — `docker compose run` cannot see a `docker compose exec` MCP server. Use `exec` for `doctor`/`sync` against the running container, or use `run` only for one-off `setup`/`secret set` with `--value-stdin`.
|
|
332
|
+
- **Refusing to initialize** — the data home contains unrelated files. The error lists them; only `config.json`, `console/`, and Chainlit's `.files/`, `.chainlit/`, `public/` are allowed alongside a catalog.
|
|
333
|
+
- **Is a sync actually running? (no deploy scaffold needed)** — three checks, all safe while a sync holds the write lock:
|
|
334
|
+
1. Runtime status API: `curl -s "http://127.0.0.1:8787/api/v1/sync/status?athleteId=<id>" | jq '.progress.running'` (through the Console instead: `/api/v1/sync/status` behind your login).
|
|
335
|
+
2. Detached-child log files: `ls -t "$CATENCE_HOME/logs" | head`. A successful `POST /api/v1/sync` creates `logs/sync-<athlete>-<stamp>.log` before spawning, so an absent or empty `logs/` directory proves no detached sync ever started.
|
|
336
|
+
3. The MCP progress tools (`catence_sync_progress`) read the same heartbeats.
|
|
337
|
+
- **Models page clutter** — unused shipped profiles can be hidden per device from the Models page (`Hide` on the profile card); hidden profiles disappear from the chat Model dropdown but stay configured, and are restored with `Unhide`. Hiding lives in the Console database, never in `config.json`.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Distribution and release readiness
|
|
2
|
+
|
|
3
|
+
Catence now has a repeatable distribution path.
|
|
4
|
+
|
|
5
|
+
## Artifacts
|
|
6
|
+
|
|
7
|
+
| Surface | Source | Consumer path |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| npm | `package.json` and compiled `dist/` | `npm install --global catence` or `npx --yes catence@<version> demo` |
|
|
10
|
+
| PyPI Console | `console/pyproject.toml` | `uvx catence-console@<python-version> serve`; it starts the matching npm runtime unless `--mcp-url` is supplied |
|
|
11
|
+
| PyPI UI runtime | `catence-ui` fork | `catence-chainlit==<python-version>` provides the prebuilt maintained Chainlit frontend used by Console |
|
|
12
|
+
| APM | `apm.yml` | `apm install rifusaki/catence#v<version>` configures normal and generated-demo MCP entries |
|
|
13
|
+
| MCPB | `mcpb/manifest.json` | Attach the platform-specific `catence-demo-<version>-<platform>-<arch>.mcpb` release asset in an MCPB-capable desktop client |
|
|
14
|
+
|
|
15
|
+
The package exposes two binaries:
|
|
16
|
+
|
|
17
|
+
- `catence` starts the stdio MCP server, `catence serve` starts the loopback Streamable HTTP server, and `catence demo` creates a generated store before starting stdio.
|
|
18
|
+
- `catence-data` manages catalog `setup`, athlete selection, per-athlete secrets,
|
|
19
|
+
`sync`, `backfill`, provider authentication, retrieval-index rebuilding, and
|
|
20
|
+
`demo` generation.
|
|
21
|
+
|
|
22
|
+
`catence-data demo` and `catence demo` use `~/.catence-demo` by default. Both
|
|
23
|
+
commands refuse to modify an existing directory unless it contains the Catence
|
|
24
|
+
demo marker. Generated data is marked in every MCP tool response.
|
|
25
|
+
|
|
26
|
+
## Release gates
|
|
27
|
+
|
|
28
|
+
`release/manifest.json` maps the npm version to its PEP 440 Console and
|
|
29
|
+
Chainlit versions. The release check requires those fields and the protocol
|
|
30
|
+
version to agree with the npm package, Console package, runtime health
|
|
31
|
+
contract, APM manifest, and MCPB manifest. When invoked for a Git tag, it also
|
|
32
|
+
requires the tag to be `v<package-version>`.
|
|
33
|
+
|
|
34
|
+
The verification workflow runs TypeScript checking, a moderate-or-higher npm dependency audit, all tests, a npm-pack dry run, and MCPB-manifest validation. The tag workflow then:
|
|
35
|
+
|
|
36
|
+
1. builds an MCPB on Linux, macOS, and Windows, with the bundle manifest restricted to its native platform because DuckDB includes a native binding;
|
|
37
|
+
2. publishes npm with provenance;
|
|
38
|
+
3. validates that the versioned APM manifest installs the two intended MCP entries in a clean consumer root; and
|
|
39
|
+
4. builds and publishes the matching `catence-console` wheel after confirming that the manifest's exact `catence-chainlit` version is already available; and
|
|
40
|
+
5. uploads the MCPB artifacts to the GitHub release. The tagged source tree is itself the APM package.
|
|
41
|
+
|
|
42
|
+
The `release` GitHub environment is intentional: publishing waits for that environment's approval rules. Configure npm trusted publishing for `rifusaki/catence` before creating the first `v0.1.0` tag. Its publish job upgrades to npm 11.5.1 or newer and deliberately avoids `setup-node`'s registry authentication so npm uses the GitHub Actions OIDC identity. Before releasing the two Python artifacts, configure PyPI Trusted Publishing for the `pypi` environment in both the maintained `catence-ui` fork (`catence-chainlit`) and this repository (`catence-console`).
|
|
43
|
+
|
|
44
|
+
For beta tags, the dedicated `Beta` workflow publishes npm with the `beta`
|
|
45
|
+
dist-tag and creates a GitHub prerelease after the corresponding Chainlit beta
|
|
46
|
+
is available. The server procedure and acceptance criteria are in
|
|
47
|
+
[release/beta-checklist.md](../release/beta-checklist.md).
|
|
48
|
+
|
|
49
|
+
## Current operational gaps
|
|
50
|
+
|
|
51
|
+
- The npm package name is available at the time this document was written, but it is not claimed until the first successful npm publish.
|
|
52
|
+
- The MCPB demo bundle includes Node dependencies for its target platform. It is intentionally a generated-data experience; live Garmin/Intervals/Strava setup remains explicit so a desktop extension never receives provider credentials by surprise.
|
|
53
|
+
- Live Garmin and Strava extraction still require Python 3.12 and `uv`; a future fully bundled provider runtime would remove that prerequisite.
|
|
54
|
+
- MCPB artifacts are unsigned until a production code-signing certificate is configured. MCPB-capable clients may show an unsigned-extension warning.
|
|
55
|
+
- `catence-chainlit` must be released from the maintained UI fork before each matching Catence tag. Until the first public wheel exists, Console source development uses a locally built wheel.
|
|
56
|
+
- CI produces native MCPB assets for the standard GitHub runner architectures. ARM Linux and Windows-on-ARM need additional runners before assets can be advertised for those targets.
|
|
57
|
+
|
|
58
|
+
These gaps are product/release work, not silent runtime fallbacks: the npm and APM paths make their assumptions explicit, and the demo stays synthetic until the user runs the separate live sync workflow.
|