catence 0.2.0-beta.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +38 -166
  2. package/config.example.json +5 -1
  3. package/dist/contracts/index.d.ts +1 -0
  4. package/dist/contracts/index.js +1 -0
  5. package/dist/contracts/progress.d.ts +23 -0
  6. package/dist/contracts/progress.js +49 -0
  7. package/dist/contracts/release.d.ts +1 -1
  8. package/dist/contracts/release.js +1 -1
  9. package/dist/core/logging.d.ts +16 -0
  10. package/dist/core/logging.js +68 -0
  11. package/dist/core/query/activity-discovery.d.ts +2 -0
  12. package/dist/core/query/activity-discovery.js +1 -1
  13. package/dist/core/query/analytics.d.ts +10 -0
  14. package/dist/core/query/analytics.js +494 -0
  15. package/dist/core/query/catalog.d.ts +1 -1
  16. package/dist/core/query/catalog.js +8 -4
  17. package/dist/core/query/events.d.ts +34 -0
  18. package/dist/core/query/events.js +121 -0
  19. package/dist/core/query/fitness.d.ts +4 -0
  20. package/dist/core/query/fitness.js +60 -0
  21. package/dist/core/query/repository.d.ts +2 -0
  22. package/dist/core/query/repository.js +48 -0
  23. package/dist/core/query/swimming.js +2 -2
  24. package/dist/core/runtime/configuration.d.ts +3 -1
  25. package/dist/core/runtime/configuration.js +5 -1
  26. package/dist/core/runtime/opencode-go.d.ts +58 -0
  27. package/dist/core/runtime/opencode-go.js +246 -0
  28. package/dist/core/runtime/secrets.js +51 -13
  29. package/dist/elt/application/detached-sync.d.ts +50 -0
  30. package/dist/elt/application/detached-sync.js +99 -0
  31. package/dist/elt/application/management.d.ts +15 -0
  32. package/dist/elt/application/management.js +435 -19
  33. package/dist/elt/ingestion/importer.d.ts +3 -2
  34. package/dist/elt/ingestion/importer.js +5 -4
  35. package/dist/elt/ingestion/providers/intervals.d.ts +12 -1
  36. package/dist/elt/ingestion/providers/intervals.js +37 -2
  37. package/dist/elt/ingestion/providers/registry.d.ts +13 -0
  38. package/dist/elt/ingestion/providers/registry.js +19 -1
  39. package/dist/elt/ingestion/providers/strava/service.js +7 -1
  40. package/dist/elt/normalization/normalizers.js +207 -15
  41. package/dist/elt/storage/database.d.ts +21 -2
  42. package/dist/elt/storage/database.js +184 -6
  43. package/dist/elt/storage/migrations.js +145 -1
  44. package/dist/elt/storage/progress-sidecar.d.ts +61 -0
  45. package/dist/elt/storage/progress-sidecar.js +200 -0
  46. package/dist/interfaces/cli/commands.js +94 -1
  47. package/dist/interfaces/http/generation-status.d.ts +19 -0
  48. package/dist/interfaces/http/generation-status.js +55 -0
  49. package/dist/interfaces/http/server.d.ts +7 -1
  50. package/dist/interfaces/http/server.js +184 -1
  51. package/dist/interfaces/mcp/main.js +33 -1
  52. package/dist/interfaces/mcp/server.d.ts +2 -1
  53. package/dist/interfaces/mcp/server.js +168 -45
  54. package/dist/runtime/index.d.ts +9 -2
  55. package/dist/runtime/index.js +6 -2
  56. package/docs/architecture.md +13 -2
  57. package/docs/assets/image.png +0 -0
  58. package/docs/configuration.md +243 -0
  59. package/docs/deployment/docker.md +528 -0
  60. package/docs/deployment/local-console.md +263 -0
  61. package/docs/deployment/local-mcp.md +329 -0
  62. package/docs/development.md +337 -0
  63. package/docs/distribution.md +45 -0
  64. package/docs/llm-providers.md +66 -1
  65. package/docs/mcp-activity-retrieval.md +90 -210
  66. package/package.json +1 -1
  67. package/pyproject.toml +1 -1
  68. package/python/catence/providers/garmin/cli.py +43 -9
  69. package/python/catence/providers/garmin/progress.py +85 -0
  70. package/python/catence/providers/garmin/registry.py +2 -0
  71. package/python/catence/providers/garmin/streams.py +38 -0
  72. package/python/catence/providers/garmin/swim_backfill.py +83 -0
  73. package/python/catence/providers/garmin/worker.py +415 -12
  74. package/scripts/discover-opencode-go.mjs +36 -144
  75. package/uv.lock +3 -3
  76. package/docs/deployment.md +0 -338
@@ -1,12 +1,11 @@
1
+ #!/usr/bin/env node
1
2
  // Discover OpenCode Go models and emit Catence Console profiles for them.
2
3
  //
3
- // OpenCode Go serves its catalog through three APIs: most models speak the
4
- // OpenAI Chat Completions API, Grok and GPT-5.6 Luna speak the OpenAI
5
- // Responses API, and MiniMax and Qwen speak the Anthropic Messages API. The
4
+ // Thin CLI wrapper around the runtime's src/core/runtime/opencode-go.ts
5
+ // module, which owns the "live discovery + trusted metadata" logic: the
6
6
  // public `GET /models` endpoint returns only model ids, not which API each
7
- // model needs, so routing comes from the trusted table below and is intersected
8
- // with the live list (the same "live discovery + trusted metadata" approach
9
- // OpenClaw uses).
7
+ // model needs, so routing comes from a trusted table intersected with the
8
+ // live list (the same approach OpenClaw uses).
10
9
  //
11
10
  // Usage:
12
11
  // node scripts/discover-opencode-go.mjs [--base-url <url>] [--write <config.json>] [--set-default]
@@ -17,62 +16,19 @@
17
16
  // --set-default With --write, set console.defaultProfile to opencode-go even when one already exists.
18
17
 
19
18
  import process from 'node:process';
20
- import { readFile, writeFile } from 'node:fs/promises';
21
19
 
22
- const DEFAULT_BASE_URL = 'https://opencode.ai/zen/go/v1';
23
- const API_KEY_ENV = 'OPENCODE_GO_API_KEY';
24
- const API_BASE_ENV = 'OPENCODE_GO_API_BASE';
25
- const MESSAGES_BASE_ENV = 'OPENCODE_GO_MESSAGES_API_BASE';
26
-
27
- // LiteLLM appends a fixed suffix to the profile base URL per provider, so the
28
- // three APIs need different prefixes and two base URLs:
29
- // openai/<id> -> {base}/chat/completions
30
- // openai/responses/<id> -> {base}/responses
31
- // anthropic/<id> -> {base minus /v1}/v1/messages
32
- const RESPONSES_MODELS = new Set(['grok-4.5', 'gpt-5.6-luna']);
33
- const MESSAGES_MODELS = new Set([
34
- 'minimax-m2.5',
35
- 'minimax-m2.7',
36
- 'minimax-m3',
37
- 'qwen3.5-plus',
38
- 'qwen3.6-plus',
39
- 'qwen3.7-plus',
40
- 'qwen3.7-max',
41
- 'qwen3.8-max',
42
- ]);
43
-
44
- const BRAND_NAMES = new Map([
45
- ['deepseek', 'DeepSeek'],
46
- ['glm', 'GLM'],
47
- ['gpt', 'GPT'],
48
- ['grok', 'Grok'],
49
- ['hy3', 'Hy3'],
50
- ['kimi', 'Kimi'],
51
- ['mimo', 'MiMo'],
52
- ['minimax', 'MiniMax'],
53
- ['qwen', 'Qwen'],
54
- ]);
55
-
56
- function classify(id) {
57
- if (RESPONSES_MODELS.has(id)) return { route: 'responses', guessed: false };
58
- if (MESSAGES_MODELS.has(id)) return { route: 'messages', guessed: false };
59
- if (/^(grok-|gpt-)/.test(id)) return { route: 'responses', guessed: true };
60
- if (/^(minimax-|qwen)/.test(id)) return { route: 'messages', guessed: true };
61
- return { route: 'chat', guessed: false };
20
+ let runtime;
21
+ try {
22
+ runtime = await import(new URL('../dist/core/runtime/opencode-go.js', import.meta.url).href);
23
+ } catch (error) {
24
+ if (error?.code !== 'ERR_MODULE_NOT_FOUND') throw error;
25
+ process.stderr.write(
26
+ 'The discovery module is not built yet. Run `npm run build` in the catence checkout first, or use an installed `catence` package.\n',
27
+ );
28
+ process.exit(1);
62
29
  }
63
30
 
64
- function modelReference(id, route) {
65
- if (route === 'responses') return `openai/responses/${id}`;
66
- if (route === 'messages') return `anthropic/${id}`;
67
- return `openai/${id}`;
68
- }
69
-
70
- function labelFor(id) {
71
- return id
72
- .split('-')
73
- .map((segment) => BRAND_NAMES.get(segment) ?? segment.charAt(0).toUpperCase() + segment.slice(1))
74
- .join(' ');
75
- }
31
+ const { OPENCODE_GO_API_BASE_ENV, OPENCODE_GO_API_KEY_ENV, OPENCODE_GO_MESSAGES_API_BASE_ENV } = runtime;
76
32
 
77
33
  function usage() {
78
34
  process.stderr.write(
@@ -82,7 +38,7 @@ function usage() {
82
38
 
83
39
  async function main() {
84
40
  const args = process.argv.slice(2);
85
- let baseUrl = DEFAULT_BASE_URL;
41
+ let baseUrl = runtime.OPENCODE_GO_DEFAULT_BASE_URL;
86
42
  let writePath = null;
87
43
  let setDefault = false;
88
44
  for (let i = 0; i < args.length; i += 1) {
@@ -108,100 +64,36 @@ async function main() {
108
64
  if (!baseUrl.endsWith('/v1')) {
109
65
  process.stderr.write(`Warning: --base-url ${baseUrl} does not end in /v1; chat and responses URLs may be wrong.\n`);
110
66
  }
111
- const modelsUrl = `${baseUrl.replace(/\/$/, '')}/models`;
112
67
  const messagesBase = baseUrl.replace(/\/v1\/?$/, '');
113
68
 
114
- let response;
115
- try {
116
- response = await fetch(modelsUrl, { headers: { accept: 'application/json' } });
117
- } catch (error) {
118
- throw new Error(`Could not reach ${modelsUrl}: ${error.message}`);
119
- }
120
- if (!response.ok) {
121
- throw new Error(`GET ${modelsUrl} returned HTTP ${response.status}.`);
122
- }
123
- const payload = await response.json();
124
- const ids = (payload.data ?? []).map((entry) => entry.id).filter((id) => typeof id === 'string' && id);
125
-
126
- const groups = { chat: [], responses: [], messages: [] };
127
- const guessed = [];
128
- for (const id of ids) {
129
- const { route, guessed: wasGuessed } = classify(id);
130
- if (!(route in groups)) continue;
131
- groups[route].push(id);
132
- if (wasGuessed) guessed.push(`${id} -> ${route}`);
133
- }
134
- for (const route of Object.keys(groups)) groups[route].sort();
135
-
136
- const chatProfileModels = {};
137
- for (const id of groups.chat) {
138
- chatProfileModels[id] = { label: labelFor(id), model: modelReference(id, 'chat') };
139
- }
140
- for (const id of groups.responses) {
141
- chatProfileModels[id] = { label: labelFor(id), model: modelReference(id, 'responses') };
142
- }
143
- const messagesProfileModels = {};
144
- for (const id of groups.messages) {
145
- messagesProfileModels[id] = { label: labelFor(id), model: modelReference(id, 'messages') };
146
- }
147
-
148
- const chatDefault = groups.chat[0] ?? groups.responses[0];
149
- const profiles = {};
150
- profiles['opencode-go'] = {
151
- label: 'OpenCode Go',
152
- ...(chatDefault ? { defaultModel: chatDefault } : {}),
153
- models: chatProfileModels,
154
- apiKeyEnv: API_KEY_ENV,
155
- apiBaseEnv: API_BASE_ENV,
156
- };
157
- if (messagesProfileModels && groups.messages.length) {
158
- profiles['opencode-go-messages'] = {
159
- label: 'OpenCode Go (Anthropic)',
160
- defaultModel: groups.messages[0],
161
- models: messagesProfileModels,
162
- apiKeyEnv: API_KEY_ENV,
163
- apiBaseEnv: MESSAGES_BASE_ENV,
164
- };
165
- }
166
-
167
- const consoleSection = { defaultProfile: 'opencode-go', profiles };
168
-
169
- if (guessed.length) {
170
- process.stderr.write('Routed by prefix heuristic (verify before use):\n');
171
- for (const line of guessed) process.stderr.write(` ${line}\n`);
172
- }
173
- process.stderr.write(
174
- `Discovered ${ids.length} models (${groups.chat.length} chat, ${groups.responses.length} responses, ${groups.messages.length} messages).\n`,
175
- );
176
-
177
69
  if (writePath) {
178
- let root = {};
179
- try {
180
- root = JSON.parse(await readFile(writePath, 'utf8'));
181
- } catch (error) {
182
- if (error.code !== 'ENOENT') throw new Error(`Could not read ${writePath}: ${error.message}`);
70
+ const result = await runtime.mergeOpenCodeGoConsoleProfiles({ configPath: writePath, baseUrl, setDefault });
71
+ if (result.guessedRoutes.length) {
72
+ process.stderr.write('Routed by prefix heuristic (verify before use):\n');
73
+ for (const line of result.guessedRoutes) process.stderr.write(` ${line}\n`);
183
74
  }
184
- if (typeof root !== 'object' || root === null || Array.isArray(root)) {
185
- throw new Error(`${writePath} must contain a JSON object.`);
186
- }
187
- const existingConsole = typeof root.console === 'object' && root.console !== null ? root.console : {};
188
- const mergedProfiles = { ...(existingConsole.profiles ?? {}), ...profiles };
189
- const defaultProfile =
190
- existingConsole.defaultProfile && !setDefault ? existingConsole.defaultProfile : 'opencode-go';
191
- root.console = { ...existingConsole, defaultProfile, profiles: mergedProfiles };
192
- await writeFile(writePath, `${JSON.stringify(root, null, 2)}\n`);
193
- process.stderr.write(`Merged profiles into ${writePath} (defaultProfile: ${defaultProfile}).\n`);
75
+ process.stderr.write(
76
+ `Merged ${result.counts.chat} chat, ${result.counts.responses} responses, and ${result.counts.messages} messages models into ${result.configPath}` +
77
+ ` as ${result.mergedProfileIds.join(' + ')} (defaultProfile: ${result.defaultProfile}).\n`,
78
+ );
194
79
  return;
195
80
  }
196
81
 
82
+ const ids = await runtime.fetchOpenCodeGoModelIds(baseUrl);
83
+ const built = runtime.buildOpenCodeGoConsoleProfiles(ids, baseUrl);
84
+ if (built.guessedRoutes.length) {
85
+ process.stderr.write('Routed by prefix heuristic (verify before use):\n');
86
+ for (const line of built.guessedRoutes) process.stderr.write(` ${line}\n`);
87
+ }
197
88
  process.stderr.write(
198
- `Export the key and base URLs, then add the JSON below under "console" in config.json:\n` +
199
- ` export ${API_KEY_ENV}='sk-…'\n` +
200
- ` export ${API_BASE_ENV}='${baseUrl}'\n` +
201
- ` export ${MESSAGES_BASE_ENV}='${messagesBase}'\n` +
89
+ `Discovered ${ids.length} models (${built.counts.chat} chat, ${built.counts.responses} responses, ${built.counts.messages} messages).\n` +
90
+ `Export the key and base URLs, then add the JSON below under "console" in config.json:\n` +
91
+ ` export ${OPENCODE_GO_API_KEY_ENV}='sk-…'\n` +
92
+ ` export ${OPENCODE_GO_API_BASE_ENV}='${baseUrl}'\n` +
93
+ ` export ${OPENCODE_GO_MESSAGES_API_BASE_ENV}='${messagesBase}'\n` +
202
94
  `(or rerun with --write ~/.catence/config.json to merge into an existing config.)\n`,
203
95
  );
204
- process.stdout.write(`${JSON.stringify(consoleSection, null, 2)}\n`);
96
+ process.stdout.write(`${JSON.stringify({ defaultProfile: built.defaultProfile, profiles: built.profiles }, null, 2)}\n`);
205
97
  }
206
98
 
207
99
  await main().catch((error) => {
package/uv.lock CHANGED
@@ -47,7 +47,7 @@ dev = [
47
47
  requires-dist = [
48
48
  { name = "duckdb", specifier = ">=1.4.0" },
49
49
  { name = "fitdecode", specifier = ">=0.10.0" },
50
- { name = "garminconnect", git = "https://github.com/rifusaki/python-garminconnect.git?rev=732bf14602bce2ca22cb569ba6583c3229bf904d" },
50
+ { name = "garminconnect", git = "https://github.com/rifusaki/python-garminconnect.git?rev=cd4e3d9ef81a93681950138e08f57af226fd5735" },
51
51
  { name = "polars", specifier = ">=1.0.0" },
52
52
  { name = "pyarrow", specifier = ">=18.0.0" },
53
53
  { name = "pydantic", specifier = ">=2.0.0" },
@@ -318,8 +318,8 @@ wheels = [
318
318
 
319
319
  [[package]]
320
320
  name = "garminconnect"
321
- version = "0.3.8"
322
- source = { git = "https://github.com/rifusaki/python-garminconnect.git?rev=732bf14602bce2ca22cb569ba6583c3229bf904d#732bf14602bce2ca22cb569ba6583c3229bf904d" }
321
+ version = "0.3.12"
322
+ source = { git = "https://github.com/rifusaki/python-garminconnect.git?rev=cd4e3d9ef81a93681950138e08f57af226fd5735#cd4e3d9ef81a93681950138e08f57af226fd5735" }
323
323
  dependencies = [
324
324
  { name = "curl-cffi" },
325
325
  { name = "requests" },
@@ -1,338 +0,0 @@
1
- # Deploying Catence Console
2
-
3
- Catence Console is a password-protected local web chat that starts a matching
4
- Catence runtime on loopback. There are two supported ways to run it: **locally**
5
- (npm package + Python Console on your machine) or **in Docker** (one container
6
- built from the public registries). Both paths end with the same Console, data
7
- directory layout, model configuration, and credentials model.
8
-
9
- Everything below assumes a recent beta or stable release; channels are
10
- explained in the [beta release notes](../README.md#beta-releases).
11
-
12
- ## Prerequisites
13
-
14
- - Node.js 22+ and Python 3.12+ with [uv](https://docs.astral.sh/uv/)
15
- (local path), or Docker Engine with the `docker compose` plugin (Docker path).
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/` or `/data` in Docker |
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 | `.env` in Docker, shell exports locally |
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
- [`config.example.json`](../config.example.json) for a complete example.
35
-
36
- ---
37
-
38
- ## Path A — Local deployment
39
-
40
- ### 1. Install the runtime and the Console
41
-
42
- ```sh
43
- npm install --global catence@beta # or catence@latest for stable
44
- uv tool install catence-console # persistent `catence-console` command
45
- # alternative to the last line: uvx catence-console@<version> serve (ephemeral)
46
- ```
47
-
48
- `catence-console serve` launches a matching Catence runtime automatically: it
49
- uses a globally installed `catence` if present, otherwise `npx catence@<pinned>`.
50
-
51
- ### 2. Create the catalog and first athlete
52
-
53
- ```sh
54
- catence-data setup --athlete alex --label "Alex"
55
- ```
56
-
57
- This creates `~/.catence/` (or `$CATENCE_HOME`). It also tolerates a directory
58
- that already holds only Console artifacts (`config.json`, `console/`).
59
-
60
- ### 3. Store provider credentials (stdin only, never shell history)
61
-
62
- Garmin (`email`, `password`), Intervals.icu (`apiKey`, `athleteId`), and Strava
63
- (`clientId`, `clientSecret`):
64
-
65
- ```sh
66
- # Garmin
67
- printf %s 'alex@example.com' | catence-data --athlete alex secret set --provider garmin --field email --value-stdin
68
- printf %s 'your-garmin-password' | catence-data --athlete alex secret set --provider garmin --field password --value-stdin
69
- # Intervals.icu
70
- printf %s 'intervals-api-key' | catence-data --athlete alex secret set --provider intervals --field apiKey --value-stdin
71
- printf %s '12345' | catence-data --athlete alex secret set --provider intervals --field athleteId --value-stdin
72
- # Strava (create an API application at https://www.strava.com/settings/api)
73
- printf %s 'strava-client-id' | catence-data --athlete alex secret set --provider strava --field clientId --value-stdin
74
- printf %s 'strava-client-secret' | catence-data --athlete alex secret set --provider strava --field clientSecret --value-stdin
75
- ```
76
-
77
- Set only the providers you actually sync; `secret set` accepts each field
78
- independently.
79
-
80
- ### 4. Sync data and build retrieval context
81
-
82
- ```sh
83
- catence-data --athlete alex sync --provider all
84
- catence-data --athlete alex build-retrieval-index
85
- ```
86
-
87
- ### 5. Configure the model
88
-
89
- On the first chat, the Console wizard asks for a provider and model and writes
90
- a starter profile. To start from the documented profiles instead, copy the
91
- `console` section from [`config.example.json`](../config.example.json) into
92
- `~/.catence/config.json`. To add OpenCode Go models, run the
93
- [model discovery script](#model-discovery-opencode-go) below.
94
-
95
- ### 6. Set the model credentials and Console login
96
-
97
- ```sh
98
- export OPENAI_API_KEY='…' # or ANTHROPIC_API_KEY / OPENCODE_GO_API_KEY …
99
- export CATENCE_CONSOLE_USERNAME='coach'
100
- export CATENCE_CONSOLE_PASSWORD_HASH="$(catence-console auth hash-password)"
101
- export CHAINLIT_AUTH_SECRET="$(openssl rand -hex 32)"
102
- ```
103
-
104
- ### 7. Start the Console
105
-
106
- ```sh
107
- catence-console serve
108
- # open http://127.0.0.1:8000
109
- ```
110
-
111
- The Model dropdown in the settings panel lists every profile's deployments.
112
- Preflight with:
113
-
114
- ```sh
115
- catence-console doctor
116
- ```
117
-
118
- ---
119
-
120
- ## Path B — Docker deployment
121
-
122
- ### 1. Deploy with the script
123
-
124
- ```sh
125
- curl -fsSL https://raw.githubusercontent.com/rifusaki/catence/beta/scripts/deploy-console.sh | bash -s beta
126
- # or from a checkout: ./scripts/deploy-console.sh beta
127
- # use `stable` instead of `beta` for the stable channel
128
- ```
129
-
130
- The script resolves the newest matching versions from npm and PyPI, writes a
131
- self-contained `catence-deploy/` project (Dockerfile, docker-compose.yml, and
132
- `.env` with placeholders), builds the image, **seeds a starter
133
- `/data/config.json`** into the data volume, and starts the stack — or, when
134
- secrets are missing, prints the remaining steps and exits.
135
-
136
- ### 2. Fill in `catence-deploy/.env`
137
-
138
- ```sh
139
- CATENCE_CONSOLE_USERNAME=coach
140
- CATENCE_CONSOLE_PASSWORD_HASH='…' # generate: ./catence-deploy/hash-password.sh
141
- CHAINLIT_AUTH_SECRET='…' # generate: openssl rand -hex 32
142
- OPENAI_API_KEY='…' # or ANTHROPIC_API_KEY / OPENCODE_GO_API_KEY …
143
- OPENAI_API_BASE='' # only for OpenAI-compatible endpoints
144
- OPENCODE_GO_API_KEY='…' # the OpenCode Go base URLs are pre-filled below
145
- OPENCODE_GO_API_BASE='https://opencode.ai/zen/go/v1'
146
- OPENCODE_GO_MESSAGES_API_BASE='https://opencode.ai/zen/go'
147
- ```
148
-
149
- Then start the stack (or re-run the script, which re-reads the saved `.env`):
150
-
151
- ```sh
152
- docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env up -d
153
- ```
154
-
155
- ### 3. Initialize an athlete store
156
-
157
- The data volume already contains a seeded `config.json`, so `setup` accepts
158
- it. Run through the container:
159
-
160
- ```sh
161
- docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm \
162
- --entrypoint catence-data console setup --athlete alex --label "Alex"
163
- ```
164
-
165
- ### 4. Store provider credentials
166
-
167
- Garmin (`email`, `password`), Intervals.icu (`apiKey`, `athleteId`), and Strava
168
- (`clientId`, `clientSecret`), one `printf` per field:
169
-
170
- ```sh
171
- # Garmin
172
- printf %s 'alex@example.com' | docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm -T \
173
- --entrypoint catence-data console --athlete alex secret set \
174
- --provider garmin --field email --value-stdin
175
- printf %s 'your-garmin-password' | docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm -T \
176
- --entrypoint catence-data console --athlete alex secret set \
177
- --provider garmin --field password --value-stdin
178
- # Intervals.icu
179
- printf %s 'intervals-api-key' | docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm -T \
180
- --entrypoint catence-data console --athlete alex secret set \
181
- --provider intervals --field apiKey --value-stdin
182
- printf %s '12345' | docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm -T \
183
- --entrypoint catence-data console --athlete alex secret set \
184
- --provider intervals --field athleteId --value-stdin
185
- # Strava (create an API application at https://www.strava.com/settings/api)
186
- printf %s 'strava-client-id' | docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm -T \
187
- --entrypoint catence-data console --athlete alex secret set \
188
- --provider strava --field clientId --value-stdin
189
- printf %s 'strava-client-secret' | docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm -T \
190
- --entrypoint catence-data console --athlete alex secret set \
191
- --provider strava --field clientSecret --value-stdin
192
- ```
193
-
194
- Set only the providers you actually sync. Use the same `--entrypoint
195
- catence-data` pattern for `sync`, `backfill`, and `auth strava`.
196
-
197
- ### 5. Configure the model
198
-
199
- The seeded `config.json` already offers OpenAI, OpenAI-compatible, and
200
- Anthropic starter profiles, so the settings panel works before the first chat.
201
- Edit `/data/config.json` inside the volume:
202
-
203
- ```sh
204
- docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm \
205
- --entrypoint sh console -c 'cat > /data/config.json' # paste JSON, then Ctrl-D
206
- ```
207
-
208
- With `--home <dir>` (bind mount), edit `<dir>/config.json` on the host
209
- instead. To add OpenCode Go models, see [model discovery](#model-discovery-opencode-go).
210
-
211
- ### 6. Verify with doctor
212
-
213
- `catence-console doctor` checks each profile's environment variables and the
214
- local Catence runtime. The deploy scaffold includes a helper for it:
215
-
216
- ```sh
217
- ./catence-deploy/doctor.sh
218
- ```
219
-
220
- The helper runs the doctor *inside the running container* with `exec` — the
221
- container's own loopback holds the live MCP server at `127.0.0.1:8787`, and the
222
- provider keys from `.env` are already in its environment. (A `docker compose
223
- run` container has a separate loopback and would report the runtime as
224
- unreachable.) The equivalent long form is:
225
-
226
- ```sh
227
- docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env exec console \
228
- /opt/catence-console/bin/catence-console doctor --home /data --mcp-url http://127.0.0.1:8787/mcp
229
- ```
230
-
231
- Expect `"ready": true` once the model key is set; missing keys show up per
232
- profile without ever printing their values.
233
-
234
- ### 7. Sync, backfill, and inspect athlete data
235
-
236
- The deploy scaffold includes a helper that runs `catence-data` *inside the
237
- running container* with `exec`, so the athlete's provider secrets from the
238
- data volume are already available:
239
-
240
- ```sh
241
- ./catence-deploy/sync.sh --athlete alex sync # incremental sync, all providers
242
- ./catence-deploy/sync.sh --athlete alex backfill 2026-01-01 # full backfill from a date
243
- ./catence-deploy/sync.sh --athlete alex status # coverage and errors
244
- ./catence-deploy/sync.sh athletes # list athlete IDs
245
- ```
246
-
247
- `sync` defaults to `--provider all`; extra flags pass through, for example
248
- `./catence-deploy/sync.sh --athlete alex sync --refresh` or
249
- `./catence-deploy/sync.sh --athlete alex backfill 2026-01-01 --provider garmin`.
250
- Set a default athlete with `export CATENCE_ATHLETE=alex` to drop the
251
- `--athlete` argument. The equivalent long form is:
252
-
253
- ```sh
254
- docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env exec console \
255
- catence-data sync --athlete alex --provider all --home /data
256
- ```
257
-
258
- ### 8. Expose the Console safely
259
-
260
- Keep the Console port bound to `127.0.0.1` (the default) and front it with a
261
- Cloudflare Tunnel or reverse proxy at HTTPS. Never expose Catence's MCP port
262
- 8787 — it stays loopback-only inside the container. For remote MCP checks use
263
- SSH port forwarding (`ssh -L 8787:127.0.0.1:8787 server`).
264
-
265
- ---
266
-
267
- ## Model discovery (OpenCode Go)
268
-
269
- OpenCode Go publishes an OpenAI-compatible API at
270
- `https://opencode.ai/zen/go/v1`; its model list is public. The discovery
271
- script fetches it and merges two ready-made profiles into `config.json`:
272
- `opencode-go` (chat + responses models, base `…/zen/go/v1`) and
273
- `opencode-go-messages` (messages models, base `…/zen/go`). Existing profiles,
274
- limits, and `defaultProfile` are preserved unless `--set-default` is passed.
275
-
276
- ```sh
277
- # Local (from a checkout)
278
- npm run discover:opencode-go -- --write ~/.catence/config.json
279
-
280
- # Docker (the script ships inside the npm package in the image)
281
- docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env run --rm \
282
- --entrypoint node console \
283
- /usr/local/lib/node_modules/catence/scripts/discover-opencode-go.mjs --write /data/config.json
284
- ```
285
-
286
- Then set the credentials and verify. The generated `.env` already contains
287
- `OPENCODE_GO_API_BASE` and `OPENCODE_GO_MESSAGES_API_BASE`, so on Docker only
288
- the key is missing:
289
-
290
- ```sh
291
- export OPENCODE_GO_API_KEY='…' # any non-empty value passes the key check
292
- catence-console doctor
293
- ```
294
-
295
- The `openai/responses/…` models (for example `grok-4.5`) go through LiteLLM's
296
- responses bridge and are the least battle-tested path; smoke-test one chat turn
297
- before relying on them.
298
-
299
- ---
300
-
301
- ## Updating
302
-
303
- From `catence-data update`-capable releases (0.2.0-beta.3 and later), the
304
- runtime updates both components on the tracked channel — beta installs follow
305
- the npm `beta` tag and matching PyPI prereleases:
306
-
307
- ```sh
308
- catence-data update --check # report only; exit 1 when updates are pending
309
- catence-data update # npm runtime + uv tool catence-console upgrade
310
- catence-data update --channel stable # move off a beta onto the stable channel
311
- ```
312
-
313
- Older betas (for example a beta 2 install) predate the command; upgrade them
314
- manually once:
315
-
316
- ```sh
317
- npm install --global catence@beta
318
- uv tool install --upgrade catence-console
319
- ```
320
-
321
- Docker deployments: re-run `deploy-console.sh beta` (or `stable`); it
322
- re-resolves the newest versions from the registries and rebuilds. The data
323
- volume and your `config.json` are preserved.
324
-
325
- ## Troubleshooting
326
-
327
- - **"No Catence config exists"** — should not happen after a seeded deploy or
328
- wizard run; ensure `/data/config.json` (Docker) or `~/.catence/config.json`
329
- exists and contains a `console` section.
330
- - **"Refusing to initialize"** — the data home contains unrelated files; the
331
- error lists them. Console artifacts (`config.json`, `console/`, and Chainlit's
332
- `.files/`, `.chainlit/`, `public/`) are allowed; anything else blocks `setup`.
333
- - **Settings panel has no Model dropdown** — the Console loads profiles from
334
- `config.json`; write one via the wizard, a manual copy, or the discovery
335
- script.
336
- - **Profile not ready** — `catence-console doctor` lists the missing
337
- environment variables; set them in `.env` (Docker) or the shell (local) and
338
- restart the Console.