catence 0.2.0-beta.1 → 0.2.0-beta.10
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 +144 -2
- package/config.example.json +32 -20
- 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 +48 -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 +42 -0
- package/dist/core/runtime/catalog.js +17 -7
- package/dist/core/runtime/self-update.d.ts +44 -0
- package/dist/core/runtime/self-update.js +138 -0
- package/dist/elt/application/management.d.ts +2 -0
- package/dist/elt/application/management.js +194 -17
- package/dist/elt/ingestion/importer.d.ts +3 -2
- package/dist/elt/ingestion/importer.js +4 -3
- package/dist/elt/ingestion/providers/intervals.d.ts +3 -1
- package/dist/elt/ingestion/providers/intervals.js +4 -1
- package/dist/elt/ingestion/providers/strava/service.js +7 -1
- package/dist/elt/storage/database.d.ts +7 -2
- package/dist/elt/storage/database.js +81 -4
- package/dist/elt/storage/migrations.js +25 -1
- package/dist/interfaces/cli/commands.js +91 -1
- package/dist/interfaces/mcp/server.js +7 -0
- package/dist/runtime/index.d.ts +4 -1
- package/dist/runtime/index.js +2 -1
- package/docs/deployment.md +338 -0
- package/docs/docker-deployment.md +232 -0
- package/docs/llm-providers.md +467 -0
- package/package.json +5 -1
- package/python/catence/providers/garmin/cli.py +30 -9
- package/python/catence/providers/garmin/progress.py +85 -0
- package/python/catence/providers/garmin/worker.py +132 -5
- package/scripts/discover-opencode-go.mjs +210 -0
- package/scripts/discover-opencode-zen.mjs +218 -0
|
@@ -2,6 +2,7 @@ import { DuckDBInstance } from '@duckdb/node-api';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { ReadOnlyRepository } from '../../core/query/repository.js';
|
|
5
|
+
import { SYNC_STAGES, STALE_RUN_TIMEOUT_MS } from '../../contracts/progress.js';
|
|
5
6
|
import { migrations } from './migrations.js';
|
|
6
7
|
import { json } from './sql.js';
|
|
7
8
|
export class CatenceDatabase {
|
|
@@ -81,6 +82,66 @@ export class CatenceDatabase {
|
|
|
81
82
|
await this.run(`UPDATE sync_runs SET status = CASE WHEN error_count > 0 THEN 'completed_with_errors' ELSE 'completed' END, completed_at = now() WHERE run_id = $runId`, { runId });
|
|
82
83
|
await this.run("UPDATE retrieval_index_state SET status = 'stale' WHERE index_name = 'context'");
|
|
83
84
|
}
|
|
85
|
+
async heartbeatRun(runId, progress) {
|
|
86
|
+
await this.run(`INSERT INTO sync_run_progress (run_id, provider, stage, current_step, completed_units, total_units, percent, elapsed_seconds, estimated_remaining_seconds, heartbeat_at, detail_json)
|
|
87
|
+
VALUES ($runId, $provider, $stage, $currentStep, $completedUnits, $totalUnits, $percent, $elapsedSeconds, $estimatedRemainingSeconds, now(), '{}')
|
|
88
|
+
ON CONFLICT (run_id) DO UPDATE SET
|
|
89
|
+
provider = EXCLUDED.provider,
|
|
90
|
+
stage = EXCLUDED.stage,
|
|
91
|
+
current_step = EXCLUDED.current_step,
|
|
92
|
+
completed_units = EXCLUDED.completed_units,
|
|
93
|
+
total_units = EXCLUDED.total_units,
|
|
94
|
+
percent = EXCLUDED.percent,
|
|
95
|
+
elapsed_seconds = EXCLUDED.elapsed_seconds,
|
|
96
|
+
estimated_remaining_seconds = EXCLUDED.estimated_remaining_seconds,
|
|
97
|
+
heartbeat_at = now(),
|
|
98
|
+
detail_json = '{}'`, {
|
|
99
|
+
runId,
|
|
100
|
+
provider: progress.provider,
|
|
101
|
+
stage: progress.stage,
|
|
102
|
+
currentStep: progress.currentStep,
|
|
103
|
+
completedUnits: progress.completedUnits,
|
|
104
|
+
totalUnits: progress.totalUnits,
|
|
105
|
+
percent: progress.percentComplete,
|
|
106
|
+
elapsedSeconds: progress.elapsedSeconds,
|
|
107
|
+
estimatedRemainingSeconds: progress.estimatedRemainingSeconds,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async markRunInterrupted(runId, stage = 'interrupted') {
|
|
111
|
+
await this.run("UPDATE sync_runs SET status = 'interrupted', completed_at = now() WHERE run_id = $runId AND status = 'running'", { runId });
|
|
112
|
+
await this.run('UPDATE sync_run_progress SET stage = $stage, heartbeat_at = now() WHERE run_id = $runId', { runId, stage });
|
|
113
|
+
}
|
|
114
|
+
async interruptStaleSyncRuns(timeoutMs = STALE_RUN_TIMEOUT_MS) {
|
|
115
|
+
const cutoff = new Date(Date.now() - timeoutMs).toISOString();
|
|
116
|
+
const stale = await this.rows(`SELECT run_id FROM sync_runs WHERE status = 'running' AND started_at < $cutoff`, { cutoff });
|
|
117
|
+
for (const row of stale) {
|
|
118
|
+
await this.run("UPDATE sync_runs SET status = 'timed_out', completed_at = now() WHERE run_id = $runId AND status = 'running'", { runId: row.run_id });
|
|
119
|
+
await this.run("UPDATE sync_run_progress SET stage = 'timed_out', heartbeat_at = now() WHERE run_id = $runId", { runId: row.run_id });
|
|
120
|
+
}
|
|
121
|
+
return { runIds: stale.map((row) => row.run_id) };
|
|
122
|
+
}
|
|
123
|
+
async syncProgress() {
|
|
124
|
+
const running = await this.rows(`SELECT runs.run_id, runs.provider,
|
|
125
|
+
coalesce(progress.stage, 'starting') AS stage,
|
|
126
|
+
progress.current_step AS current_step,
|
|
127
|
+
coalesce(progress.completed_units, 0) AS completed_units,
|
|
128
|
+
progress.total_units AS total_units,
|
|
129
|
+
coalesce(progress.percent, 0) AS percent,
|
|
130
|
+
coalesce(progress.elapsed_seconds, 0) AS elapsed_seconds,
|
|
131
|
+
progress.estimated_remaining_seconds AS estimated_remaining_seconds,
|
|
132
|
+
cast(coalesce(progress.heartbeat_at, runs.started_at) AS VARCHAR) AS heartbeat_at
|
|
133
|
+
FROM sync_runs AS runs
|
|
134
|
+
LEFT JOIN sync_run_progress AS progress USING (run_id)
|
|
135
|
+
WHERE runs.status = 'running'
|
|
136
|
+
ORDER BY runs.started_at DESC`);
|
|
137
|
+
const recent = await this.rows(`SELECT progress.run_id, progress.provider, progress.stage, progress.current_step,
|
|
138
|
+
progress.completed_units, progress.total_units, progress.percent, progress.elapsed_seconds,
|
|
139
|
+
progress.estimated_remaining_seconds, cast(progress.heartbeat_at AS VARCHAR) AS heartbeat_at
|
|
140
|
+
FROM sync_run_progress AS progress
|
|
141
|
+
ORDER BY progress.heartbeat_at DESC
|
|
142
|
+
LIMIT 10`);
|
|
143
|
+
return { running: running.map(toSyncProgressState), recent: recent.map(toSyncProgressState) };
|
|
144
|
+
}
|
|
84
145
|
async addError(runId, provider, endpoint, remoteId, message, retryable) {
|
|
85
146
|
await this.run(`INSERT INTO normalization_errors (error_id, run_id, provider, endpoint, remote_id, message, retryable)
|
|
86
147
|
VALUES ($errorId, $runId, $provider, $endpoint, $remoteId, $message, $retryable)`, { errorId: randomUUID(), runId, provider, endpoint, remoteId, message, retryable });
|
|
@@ -94,9 +155,11 @@ export class CatenceDatabase {
|
|
|
94
155
|
const rawObjects = await this.rows('SELECT count(*)::INTEGER AS count FROM raw_objects');
|
|
95
156
|
const activities = await this.rows('SELECT count(*)::INTEGER AS count FROM activities');
|
|
96
157
|
const streams = await this.rows('SELECT count(*)::INTEGER AS count FROM stream_manifest');
|
|
158
|
+
const errorDetails = await this.rows('SELECT run_id, provider, endpoint, remote_id, message, cast(created_at AS VARCHAR) AS created_at FROM normalization_errors WHERE resolved_at IS NULL ORDER BY created_at DESC LIMIT 100');
|
|
97
159
|
const cursors = await this.rows('SELECT provider, cursor_name, cast(covered_through_date AS VARCHAR) AS covered_through_date, cast(latest_source_date AS VARCHAR) AS latest_source_date, lookback_days, last_successful_run_id, cast(last_completed_at AS VARCHAR) AS last_completed_at, status FROM sync_cursors ORDER BY provider, cursor_name');
|
|
98
160
|
return {
|
|
99
161
|
runs, unresolvedErrors: errors[0]?.count ?? 0, rawObjects: rawObjects[0]?.count ?? 0, activities: activities[0]?.count ?? 0, streams: streams[0]?.count ?? 0,
|
|
162
|
+
errors: errorDetails,
|
|
100
163
|
cursors: cursors.map((cursor) => ({ ...cursor, next_from_date: subtractDays(cursor.covered_through_date, Number(cursor.lookback_days)) })),
|
|
101
164
|
};
|
|
102
165
|
}
|
|
@@ -281,10 +344,6 @@ export class ReadOnlyCatenceDatabase {
|
|
|
281
344
|
}
|
|
282
345
|
}
|
|
283
346
|
}
|
|
284
|
-
/** Compose core read services with the DuckDB-backed, read-only adapter. */
|
|
285
|
-
export async function openReadOnlyRepository(paths) {
|
|
286
|
-
return new ReadOnlyRepository(await ReadOnlyCatenceDatabase.open(paths), paths);
|
|
287
|
-
}
|
|
288
347
|
export class ReadOnlyDatabaseError extends Error {
|
|
289
348
|
code;
|
|
290
349
|
constructor(code, message) {
|
|
@@ -293,4 +352,22 @@ export class ReadOnlyDatabaseError extends Error {
|
|
|
293
352
|
this.name = 'ReadOnlyDatabaseError';
|
|
294
353
|
}
|
|
295
354
|
}
|
|
355
|
+
/** Compose core read services with the DuckDB-backed, read-only adapter. */
|
|
356
|
+
export async function openReadOnlyRepository(paths) {
|
|
357
|
+
return new ReadOnlyRepository(await ReadOnlyCatenceDatabase.open(paths), paths);
|
|
358
|
+
}
|
|
359
|
+
function toSyncProgressState(row) {
|
|
360
|
+
return {
|
|
361
|
+
runId: row.run_id,
|
|
362
|
+
provider: row.provider,
|
|
363
|
+
stage: SYNC_STAGES.includes(row.stage) ? row.stage : 'starting',
|
|
364
|
+
currentStep: row.current_step,
|
|
365
|
+
completedUnits: Math.max(0, Number(row.completed_units) || 0),
|
|
366
|
+
totalUnits: row.total_units === null || row.total_units === undefined ? null : Math.max(0, Number(row.total_units) || 0),
|
|
367
|
+
percentComplete: Math.min(100, Math.max(0, Number(row.percent) || 0)),
|
|
368
|
+
elapsedSeconds: Math.max(0, Number(row.elapsed_seconds) || 0),
|
|
369
|
+
estimatedRemainingSeconds: row.estimated_remaining_seconds === null || row.estimated_remaining_seconds === undefined ? null : Math.max(0, Number(row.estimated_remaining_seconds) || 0),
|
|
370
|
+
heartbeatAt: row.heartbeat_at,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
296
373
|
//# sourceMappingURL=database.js.map
|
|
@@ -828,5 +828,29 @@ const swimFactsAndQualityMigration = {
|
|
|
828
828
|
UPDATE retrieval_index_state SET status = 'stale' WHERE index_name = 'context';
|
|
829
829
|
`,
|
|
830
830
|
};
|
|
831
|
-
|
|
831
|
+
const syncRunProgressMigration = {
|
|
832
|
+
version: 14,
|
|
833
|
+
name: 'sync_run_progress_heartbeats',
|
|
834
|
+
sql: `
|
|
835
|
+
-- Live progress heartbeats for in-flight sync runs. Written by the Node
|
|
836
|
+
-- side from progress records streamed by provider workers; read-only
|
|
837
|
+
-- surfaces (status, progress CLI, MCP) join against sync_runs.
|
|
838
|
+
CREATE TABLE IF NOT EXISTS sync_run_progress (
|
|
839
|
+
run_id VARCHAR PRIMARY KEY,
|
|
840
|
+
provider VARCHAR NOT NULL,
|
|
841
|
+
stage VARCHAR NOT NULL,
|
|
842
|
+
current_step VARCHAR,
|
|
843
|
+
completed_units INTEGER NOT NULL DEFAULT 0,
|
|
844
|
+
total_units INTEGER,
|
|
845
|
+
percent DOUBLE NOT NULL DEFAULT 0,
|
|
846
|
+
elapsed_seconds DOUBLE NOT NULL DEFAULT 0,
|
|
847
|
+
estimated_remaining_seconds DOUBLE,
|
|
848
|
+
heartbeat_at TIMESTAMPTZ NOT NULL,
|
|
849
|
+
detail_json JSON NOT NULL DEFAULT '{}'
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
UPDATE retrieval_index_state SET status = 'stale' WHERE index_name = 'context';
|
|
853
|
+
`,
|
|
854
|
+
};
|
|
855
|
+
export const migrations = [...baseMigrations, stravaAndIdentityMigration, garminPrimaryAndTrainingMetricsMigration, garminActivitySportAndFtpBackfillMigration, garminPrimaryDailyHealthMigration, garminHistoricalMetricsAndStreamsMigration, garminUtcActivityTimestampMigration, garminLactateThresholdMetricsMigration, swimFactsAndQualityMigration, syncRunProgressMigration];
|
|
832
856
|
//# sourceMappingURL=migrations.js.map
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
3
|
import { stdin as input, stdout as output } from 'node:process';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
-
import { addAthlete, athleteStorePaths, createDemoStore, dataStatus, defaultCatalogHome, disconnectStravaAccount, initializeCatalog, linkActivity, loadCatalog, rebuildRetrievalIndex, resolveAthlete, resolveCatalogPaths, retryDataSync, setAthleteSecret, syncData, connectStrava, connectStravaWithCallback, unlinkActivity, } from '../../runtime/index.js';
|
|
5
|
+
import { addAthlete, athleteStorePaths, CATENCE_RUNTIME_VERSION, channelForVersion, createDemoStore, dataStatus, defaultCatalogHome, disconnectStravaAccount, fetchNpmDistTags, fetchPypiReleases, hasCommand, initializeCatalog, isGlobalNpmInstall, linkActivity, loadCatalog, npmGlobalPrefix, planSelfUpdate, readInstalledConsoleVersion, rebuildRetrievalIndex, resolveAthlete, resolveCatalogPaths, retryDataSync, runSelfUpdateCommand, setAthleteSecret, syncData, syncProgress, connectStrava, connectStravaWithCallback, unlinkActivity, } from '../../runtime/index.js';
|
|
6
6
|
const program = new Command()
|
|
7
7
|
.name('catence-data')
|
|
8
8
|
.description('Athlete-scoped Catence data setup, extraction, and normalization.')
|
|
@@ -158,6 +158,96 @@ program.command('status').action(async () => {
|
|
|
158
158
|
const { paths } = await currentAthlete();
|
|
159
159
|
process.stdout.write(`${JSON.stringify(await dataStatus(paths), null, 2)}\n`);
|
|
160
160
|
});
|
|
161
|
+
function renderProgress(snapshot) {
|
|
162
|
+
const lines = snapshot.running.map((run) => {
|
|
163
|
+
const pct = run.percentComplete.toFixed(1).padStart(5);
|
|
164
|
+
const units = run.totalUnits !== null ? `${run.completedUnits}/${run.totalUnits}` : String(run.completedUnits);
|
|
165
|
+
const eta = run.estimatedRemainingSeconds !== null ? `${Math.round(run.estimatedRemainingSeconds)}s` : '?';
|
|
166
|
+
const step = run.currentStep ?? '';
|
|
167
|
+
return `${pct}% ${run.stage.padEnd(12)} ${units.padEnd(11)} ${eta.padEnd(6)} ${step}`;
|
|
168
|
+
});
|
|
169
|
+
return lines.length === 0 ? 'no active sync runs' : lines.join('\n');
|
|
170
|
+
}
|
|
171
|
+
program.command('progress')
|
|
172
|
+
.description('Show live progress for active sync runs.')
|
|
173
|
+
.option('--watch', 'refresh every few seconds until no sync runs are active')
|
|
174
|
+
.action(async (options) => {
|
|
175
|
+
const { paths } = await currentAthlete();
|
|
176
|
+
if (!options.watch) {
|
|
177
|
+
process.stdout.write(`${JSON.stringify(await syncProgress(paths), null, 2)}\n`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
while (true) {
|
|
181
|
+
const snapshot = await syncProgress(paths);
|
|
182
|
+
if (output.isTTY) {
|
|
183
|
+
output.write(`\x1b[2J\x1b[H${renderProgress(snapshot)}\n`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
process.stdout.write(`${JSON.stringify(snapshot, null, 2)}\n`);
|
|
187
|
+
}
|
|
188
|
+
if (snapshot.running.length === 0)
|
|
189
|
+
break;
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
program.command('update')
|
|
194
|
+
.description('Check for and apply the newest Catence release on the tracked channel (runtime and Console).')
|
|
195
|
+
.option('--check', 'report available releases without changing anything')
|
|
196
|
+
.option('--channel <stable|beta>', 'release channel to track; defaults to the installed runtime channel')
|
|
197
|
+
.option('--runtime', 'only check or update the Catence runtime package')
|
|
198
|
+
.option('--console', 'only check or update the Catence Console package')
|
|
199
|
+
.action(async (options) => {
|
|
200
|
+
if (options.channel !== undefined && options.channel !== 'stable' && options.channel !== 'beta') {
|
|
201
|
+
throw new Error('--channel must be stable or beta.');
|
|
202
|
+
}
|
|
203
|
+
const channel = options.channel ?? channelForVersion(CATENCE_RUNTIME_VERSION);
|
|
204
|
+
const runtimeOnly = options.runtime ?? false;
|
|
205
|
+
const consoleOnly = options.console ?? false;
|
|
206
|
+
const updateRuntime = runtimeOnly || !consoleOnly;
|
|
207
|
+
const updateConsole = consoleOnly || !runtimeOnly;
|
|
208
|
+
const [npmDistTags, pypiReleases, consoleInstalled] = await Promise.all([
|
|
209
|
+
fetchNpmDistTags('catence'),
|
|
210
|
+
fetchPypiReleases('catence-console'),
|
|
211
|
+
readInstalledConsoleVersion(),
|
|
212
|
+
]);
|
|
213
|
+
const plan = planSelfUpdate({ runtimeVersion: CATENCE_RUNTIME_VERSION, channel, npmDistTags, pypiReleases, consoleInstalled });
|
|
214
|
+
const report = { channel, runtime: plan.runtime, console: plan.console };
|
|
215
|
+
if (options.check) {
|
|
216
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
217
|
+
process.exit(plan.ok ? 0 : 1);
|
|
218
|
+
}
|
|
219
|
+
const applied = [];
|
|
220
|
+
if (plan.runtime.updateAvailable && updateRuntime) {
|
|
221
|
+
const runtimeTarget = plan.runtime.target;
|
|
222
|
+
if (runtimeTarget === undefined)
|
|
223
|
+
throw new Error('No runtime update target is available.');
|
|
224
|
+
const npmPrefix = await npmGlobalPrefix();
|
|
225
|
+
if (isGlobalNpmInstall(import.meta.url, npmPrefix)) {
|
|
226
|
+
const exitCode = await runSelfUpdateCommand('npm', ['install', '--global', `catence@${runtimeTarget}`]);
|
|
227
|
+
if (exitCode !== 0)
|
|
228
|
+
throw new Error(`npm failed to install catence@${runtimeTarget} (exit ${exitCode}).`);
|
|
229
|
+
applied.push(`runtime: catence@${runtimeTarget}`);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
process.stderr.write(`Runtime is not a global npm install; run: npm install --global catence@${runtimeTarget}\n`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (plan.console.updateAvailable && updateConsole) {
|
|
236
|
+
const consoleTarget = plan.console.target;
|
|
237
|
+
if (consoleTarget === undefined)
|
|
238
|
+
throw new Error('No Console update target is available.');
|
|
239
|
+
if (await hasCommand('uv')) {
|
|
240
|
+
const exitCode = await runSelfUpdateCommand('uv', ['tool', 'install', '--upgrade', `catence-console==${consoleTarget}`]);
|
|
241
|
+
if (exitCode !== 0)
|
|
242
|
+
throw new Error(`uv failed to install catence-console==${consoleTarget} (exit ${exitCode}).`);
|
|
243
|
+
applied.push(`console: catence-console==${consoleTarget}`);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
process.stderr.write(`uv is not installed; run: pip install --upgrade 'catence-console==${consoleTarget}'\n`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
process.stdout.write(`${JSON.stringify({ ...report, applied }, null, 2)}\n`);
|
|
250
|
+
});
|
|
161
251
|
program.command('build-retrieval-index').description('Build derived local retrieval context for one athlete.').action(async () => {
|
|
162
252
|
const { paths } = await currentAthlete();
|
|
163
253
|
process.stdout.write(`${JSON.stringify(await rebuildRetrievalIndex(paths), null, 2)}\n`);
|
|
@@ -231,6 +231,13 @@ export function createCatenceMcpServer(paths = resolveCatalogPaths(), dependenci
|
|
|
231
231
|
data: { ...(await repository.status()), ...(await repository.coverage()) },
|
|
232
232
|
provenance: { database: 'read-only DuckDB snapshot' }, query: {}, caveats: [],
|
|
233
233
|
}))));
|
|
234
|
+
server.registerTool('catence_sync_progress', {
|
|
235
|
+
title: 'Catence sync progress',
|
|
236
|
+
description: 'Read live progress heartbeats for active sync runs, plus the most recent completed or interrupted runs. Read-only.',
|
|
237
|
+
}, tool('catence_sync_progress', async () => useRepository(activePaths(), async (repository) => ({
|
|
238
|
+
data: await repository.progress(),
|
|
239
|
+
provenance: { database: 'read-only DuckDB snapshot' }, query: {}, caveats: [],
|
|
240
|
+
}))));
|
|
234
241
|
server.registerTool('review_daily_recovery_load', {
|
|
235
242
|
title: 'Review daily recovery and training load',
|
|
236
243
|
description: 'Return source-cited daily health, training, and nutrition facts for a recovery/load review. It does not prescribe a training plan.',
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* without changing the public product adapters.
|
|
8
8
|
*/
|
|
9
9
|
export type { CatencePaths } from '../contracts/runtime.js';
|
|
10
|
+
export { channelForVersion, compareReleaseVersions, fetchNpmDistTags, fetchPypiReleases, hasCommand, isGlobalNpmInstall, latestRelease, npmGlobalPrefix, parseReleaseVersion, planSelfUpdate, readInstalledConsoleVersion, runSelfUpdateCommand, } from '../core/runtime/self-update.js';
|
|
11
|
+
export type { SelfUpdatePlan, UpdateChannel } from '../core/runtime/self-update.js';
|
|
10
12
|
export { CATENCE_PROTOCOL_VERSION, CATENCE_RUNTIME_VERSION, catenceRuntimeHealth } from '../contracts/release.js';
|
|
11
13
|
export { configuredMcpRateLimit, loadCatenceConfig, resolvePaths } from '../core/runtime/configuration.js';
|
|
12
14
|
export { addAthlete, athleteStorePaths, defaultAthlete, defaultCatalogHome, initializeCatalog, loadCatalog, resolveAthlete, resolveCatalogPaths, } from '../core/runtime/catalog.js';
|
|
@@ -26,8 +28,9 @@ export { WELLNESS_METRICS, WellnessService } from '../core/query/wellness.js';
|
|
|
26
28
|
export { jsonSafe, ReadOnlyRepository } from '../core/query/repository.js';
|
|
27
29
|
export { queryReadOnlyData } from '../core/query/sql-guard.js';
|
|
28
30
|
export { createDemoStore, demoStoreMetadata } from '../elt/application/demo.js';
|
|
29
|
-
export { connectStrava, connectStravaWithCallback, dataStatus, disconnectStravaAccount, initializeDataStore, linkActivity, rebuildRetrievalIndex, retryDataSync, syncData, unlinkActivity, } from '../elt/application/management.js';
|
|
31
|
+
export { connectStrava, connectStravaWithCallback, dataStatus, disconnectStravaAccount, initializeDataStore, linkActivity, rebuildRetrievalIndex, retryDataSync, syncData, syncProgress, unlinkActivity, } from '../elt/application/management.js';
|
|
30
32
|
export type { ProviderChoice } from '../elt/application/management.js';
|
|
33
|
+
export type { InterruptedRuns, SyncProgressSnapshot, SyncProgressState } from '../contracts/progress.js';
|
|
31
34
|
export { openReadOnlyRepository, ReadOnlyDatabaseError } from '../elt/storage/database.js';
|
|
32
35
|
export { DataWriteBusyError } from '../elt/storage/write-lock.js';
|
|
33
36
|
export { hydrateStravaActivity, hydrateStravaSegmentHistory, StravaEnrichmentError, StravaRateLimitError, } from '../elt/ingestion/providers/strava/service.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { channelForVersion, compareReleaseVersions, fetchNpmDistTags, fetchPypiReleases, hasCommand, isGlobalNpmInstall, latestRelease, npmGlobalPrefix, parseReleaseVersion, planSelfUpdate, readInstalledConsoleVersion, runSelfUpdateCommand, } from '../core/runtime/self-update.js';
|
|
1
2
|
export { CATENCE_PROTOCOL_VERSION, CATENCE_RUNTIME_VERSION, catenceRuntimeHealth } from '../contracts/release.js';
|
|
2
3
|
export { configuredMcpRateLimit, loadCatenceConfig, resolvePaths } from '../core/runtime/configuration.js';
|
|
3
4
|
export { addAthlete, athleteStorePaths, defaultAthlete, defaultCatalogHome, initializeCatalog, loadCatalog, resolveAthlete, resolveCatalogPaths, } from '../core/runtime/catalog.js';
|
|
@@ -14,7 +15,7 @@ export { WELLNESS_METRICS, WellnessService } from '../core/query/wellness.js';
|
|
|
14
15
|
export { jsonSafe, ReadOnlyRepository } from '../core/query/repository.js';
|
|
15
16
|
export { queryReadOnlyData } from '../core/query/sql-guard.js';
|
|
16
17
|
export { createDemoStore, demoStoreMetadata } from '../elt/application/demo.js';
|
|
17
|
-
export { connectStrava, connectStravaWithCallback, dataStatus, disconnectStravaAccount, initializeDataStore, linkActivity, rebuildRetrievalIndex, retryDataSync, syncData, unlinkActivity, } from '../elt/application/management.js';
|
|
18
|
+
export { connectStrava, connectStravaWithCallback, dataStatus, disconnectStravaAccount, initializeDataStore, linkActivity, rebuildRetrievalIndex, retryDataSync, syncData, syncProgress, unlinkActivity, } from '../elt/application/management.js';
|
|
18
19
|
export { openReadOnlyRepository, ReadOnlyDatabaseError } from '../elt/storage/database.js';
|
|
19
20
|
export { DataWriteBusyError } from '../elt/storage/write-lock.js';
|
|
20
21
|
export { hydrateStravaActivity, hydrateStravaSegmentHistory, StravaEnrichmentError, StravaRateLimitError, } from '../elt/ingestion/providers/strava/service.js';
|
|
@@ -0,0 +1,338 @@
|
|
|
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.
|