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
|
@@ -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, importStagedFile, initializeCatalog, isGlobalNpmInstall, linkActivity, loadCatalog, npmGlobalPrefix, planSelfUpdate, readInstalledConsoleVersion, rebuildRetrievalIndex, reimportNutrition, resolveExtractionErrors, 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,9 +158,124 @@ 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`);
|
|
164
254
|
});
|
|
255
|
+
program.command('reimport-nutrition').description('Re-run normalization over captured nutrition entities without contacting providers.').action(async () => {
|
|
256
|
+
const { paths } = await currentAthlete();
|
|
257
|
+
process.stdout.write(`${JSON.stringify(await reimportNutrition(paths), null, 2)}\n`);
|
|
258
|
+
});
|
|
259
|
+
program
|
|
260
|
+
.command('import')
|
|
261
|
+
.description('Import a staged JSONL file (for example a swim-length backfill) into one athlete’s data store.')
|
|
262
|
+
.requiredOption('--provider <provider>', 'garmin, intervals, or strava')
|
|
263
|
+
.argument('<file>', 'path to the staged JSONL file')
|
|
264
|
+
.action(async (file, options) => {
|
|
265
|
+
const { paths } = await currentAthlete();
|
|
266
|
+
process.stdout.write(`${JSON.stringify(await importStagedFile(paths, options.provider, file), null, 2)}\n`);
|
|
267
|
+
});
|
|
268
|
+
program
|
|
269
|
+
.command('resolve-errors')
|
|
270
|
+
.description('Mark unresolved extraction errors as resolved (for example stale bootstrap failures from an environment that has since recovered).')
|
|
271
|
+
.option('--run <run-id>', 'resolve errors from a specific run only (repeatable)')
|
|
272
|
+
.option('--provider <provider>', 'resolve errors for garmin, intervals, or strava only')
|
|
273
|
+
.option('--before <timestamp>', 'resolve errors created before this ISO timestamp')
|
|
274
|
+
.option('--all', 'resolve every unresolved error')
|
|
275
|
+
.action(async (options) => {
|
|
276
|
+
const { paths } = await currentAthlete();
|
|
277
|
+
const runIds = Array.isArray(options.run) ? options.run : options.run ? [options.run] : undefined;
|
|
278
|
+
process.stdout.write(`${JSON.stringify(await resolveExtractionErrors(paths, { runIds, provider: options.provider, before: options.before, all: options.all }), null, 2)}\n`);
|
|
279
|
+
});
|
|
165
280
|
await program.parseAsync();
|
|
166
281
|
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read + classify the generation progress sidecar written by the Catence
|
|
3
|
+
* Console agent turn (see `catence_console/generation_sidecar.py`). Mirrors the
|
|
4
|
+
* detached-sync sidecar logic: a run is "running" until it reaches a terminal
|
|
5
|
+
* stage, and "stale" (i.e. likely stuck) when its heartbeat is older than the
|
|
6
|
+
* threshold. This powers the chat UI's "still thinking vs stalled" signal.
|
|
7
|
+
*/
|
|
8
|
+
export interface GenerationStatus {
|
|
9
|
+
threadId: string;
|
|
10
|
+
stage?: string;
|
|
11
|
+
running: boolean;
|
|
12
|
+
stale: boolean;
|
|
13
|
+
heartbeatAt?: string;
|
|
14
|
+
startedAt?: string;
|
|
15
|
+
updatedAt?: string;
|
|
16
|
+
toolCallCount: number;
|
|
17
|
+
lastTool?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function generationStatus(homeRoot: string, threadId: string): Promise<GenerationStatus>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const TERMINAL_STAGES = new Set([
|
|
4
|
+
'completed',
|
|
5
|
+
'failed',
|
|
6
|
+
'interrupted',
|
|
7
|
+
'timed_out'
|
|
8
|
+
]);
|
|
9
|
+
const STALE_MS = 5 * 60 * 1000; // 5 minutes
|
|
10
|
+
function emptyStatus(threadId) {
|
|
11
|
+
return {
|
|
12
|
+
threadId,
|
|
13
|
+
running: false,
|
|
14
|
+
stale: false,
|
|
15
|
+
toolCallCount: 0
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function generationStatus(homeRoot, threadId) {
|
|
19
|
+
const safe = threadId.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
20
|
+
if (!safe)
|
|
21
|
+
return emptyStatus(threadId);
|
|
22
|
+
const file = path.join(homeRoot, 'generation', `${safe}.generation.json`);
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = await readFile(file, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return emptyStatus(threadId);
|
|
29
|
+
}
|
|
30
|
+
let data;
|
|
31
|
+
try {
|
|
32
|
+
data = JSON.parse(raw);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return emptyStatus(threadId);
|
|
36
|
+
}
|
|
37
|
+
const base = {
|
|
38
|
+
threadId,
|
|
39
|
+
stage: typeof data.stage === 'string' ? data.stage : undefined,
|
|
40
|
+
heartbeatAt: typeof data.heartbeatAt === 'string' ? data.heartbeatAt : undefined,
|
|
41
|
+
startedAt: typeof data.startedAt === 'string' ? data.startedAt : undefined,
|
|
42
|
+
updatedAt: typeof data.updatedAt === 'string' ? data.updatedAt : undefined,
|
|
43
|
+
toolCallCount: typeof data.toolCallCount === 'number' ? data.toolCallCount : 0,
|
|
44
|
+
lastTool: data.lastTool === null || typeof data.lastTool === 'string'
|
|
45
|
+
? data.lastTool
|
|
46
|
+
: undefined
|
|
47
|
+
};
|
|
48
|
+
if (base.stage && TERMINAL_STAGES.has(base.stage)) {
|
|
49
|
+
return { ...base, running: false, stale: false };
|
|
50
|
+
}
|
|
51
|
+
const hb = base.heartbeatAt ? Date.parse(base.heartbeatAt) : NaN;
|
|
52
|
+
const stale = !Number.isFinite(hb) || Date.now() - hb > STALE_MS;
|
|
53
|
+
return { ...base, running: true, stale };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=generation-status.js.map
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { type Server } from 'node:http';
|
|
2
|
-
import { type CatalogPaths, type CatencePaths } from '../../runtime/index.js';
|
|
2
|
+
import { type DetachedSyncSpawner, mergeOpenCodeGoConsoleProfiles, startDetachedSync, type CatalogPaths, type CatencePaths } from '../../runtime/index.js';
|
|
3
3
|
export type CatenceHttpServerOptions = {
|
|
4
4
|
/** A shared catalog for public servers. All data APIs then require athleteId. */
|
|
5
5
|
catalogPaths?: CatalogPaths;
|
|
6
6
|
/** Internal compatibility hook for tests and embedded single-store callers. */
|
|
7
7
|
paths?: CatencePaths;
|
|
8
8
|
allowedOrigins?: readonly string[];
|
|
9
|
+
/** Internal compatibility hook for tests: override the detached sync spawner. */
|
|
10
|
+
startSync?: typeof startDetachedSync;
|
|
11
|
+
/** Internal compatibility hook for tests: passed into the default startDetachedSync. */
|
|
12
|
+
syncSpawnProcess?: DetachedSyncSpawner;
|
|
13
|
+
/** Internal compatibility hook for tests: override OpenCode Go model discovery. */
|
|
14
|
+
mergeModels?: typeof mergeOpenCodeGoConsoleProfiles;
|
|
9
15
|
};
|
|
10
16
|
/**
|
|
11
17
|
* Create Catence's local Streamable HTTP server.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
-
import { catenceRuntimeHealth, DashboardSnapshotService, jsonSafe, loadCatalog, openReadOnlyRepository, resolveAthlete, resolveCatalogPaths } from '../../runtime/index.js';
|
|
4
|
+
import { catenceRuntimeHealth, DashboardSnapshotService, DetachedSyncBusyError, DETACHED_SYNC_PROVIDERS, jsonSafe, loadCatalog, mergeOpenCodeGoConsoleProfiles, openReadOnlyRepository, resolveAthlete, resolveCatalogPaths, startDetachedSync, syncProgress, } from '../../runtime/index.js';
|
|
4
5
|
import { createCatenceMcpServer } from '../mcp/server.js';
|
|
6
|
+
import { generationStatus } from './generation-status.js';
|
|
5
7
|
const MAX_REQUEST_BYTES = 1_000_000;
|
|
6
8
|
function json(response, statusCode, value) {
|
|
7
9
|
const body = JSON.stringify(jsonSafe(value));
|
|
@@ -85,6 +87,84 @@ async function dashboardPaths(catalogPaths, staticPaths, url) {
|
|
|
85
87
|
return staticPaths;
|
|
86
88
|
throw new Error('No Catence data store is configured.');
|
|
87
89
|
}
|
|
90
|
+
function parseSyncBody(body) {
|
|
91
|
+
if (body === undefined)
|
|
92
|
+
return {};
|
|
93
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body))
|
|
94
|
+
throw new Error('Request body must be a JSON object.');
|
|
95
|
+
const raw = body;
|
|
96
|
+
for (const key of Object.keys(raw)) {
|
|
97
|
+
if (!['athleteId', 'provider', 'from', 'refresh', 'refreshModels'].includes(key)) {
|
|
98
|
+
throw new Error(`Unknown sync field: ${key}.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (raw.athleteId !== undefined && typeof raw.athleteId !== 'string')
|
|
102
|
+
throw new Error('athleteId must be a string.');
|
|
103
|
+
if (raw.provider !== undefined && (typeof raw.provider !== 'string' || !DETACHED_SYNC_PROVIDERS.includes(raw.provider))) {
|
|
104
|
+
throw new Error('provider must be intervals, garmin, strava, or all.');
|
|
105
|
+
}
|
|
106
|
+
if (raw.from !== undefined && (typeof raw.from !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(raw.from))) {
|
|
107
|
+
throw new Error('from must use YYYY-MM-DD.');
|
|
108
|
+
}
|
|
109
|
+
for (const flag of ['refresh', 'refreshModels']) {
|
|
110
|
+
if (raw[flag] !== undefined && typeof raw[flag] !== 'boolean')
|
|
111
|
+
throw new Error(`${flag} must be a boolean.`);
|
|
112
|
+
}
|
|
113
|
+
return raw;
|
|
114
|
+
}
|
|
115
|
+
async function resolveSyncPaths(catalogPaths, staticPaths, body) {
|
|
116
|
+
if (catalogPaths) {
|
|
117
|
+
if (!body.athleteId)
|
|
118
|
+
throw new Error('athleteId is required. Call /api/v1/athletes to inspect the configured catalog.');
|
|
119
|
+
return (await resolveAthlete(catalogPaths, body.athleteId)).paths;
|
|
120
|
+
}
|
|
121
|
+
if (staticPaths)
|
|
122
|
+
return staticPaths;
|
|
123
|
+
throw new Error('No Catence data store is configured.');
|
|
124
|
+
}
|
|
125
|
+
/** Best-effort last-success timestamps; null while a run holds the write lock. */
|
|
126
|
+
async function lastSyncSummary(paths) {
|
|
127
|
+
const completedAtIso = (value) => {
|
|
128
|
+
if (value instanceof Date)
|
|
129
|
+
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
130
|
+
if (typeof value === 'string' && value.trim())
|
|
131
|
+
return value;
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
// Read-only repository on purpose: it exposes per-run history (`syncRuns`)
|
|
136
|
+
// and degrades through the typed lock-conflict error while a detached
|
|
137
|
+
// sync holds DuckDB's single-writer lock. The write-capable dataStatus()
|
|
138
|
+
// would both block here and has no run history at all.
|
|
139
|
+
const repository = await openReadOnlyRepository(paths);
|
|
140
|
+
try {
|
|
141
|
+
const status = await repository.status();
|
|
142
|
+
const runs = Array.isArray(status.syncRuns) ? status.syncRuns : [];
|
|
143
|
+
const completed = runs.filter((run) => String(run.status ?? '').startsWith('completed') && completedAtIso(run.completed_at));
|
|
144
|
+
const providers = {};
|
|
145
|
+
let lastCompletedAt = null;
|
|
146
|
+
for (const run of completed) {
|
|
147
|
+
const completedAt = completedAtIso(run.completed_at);
|
|
148
|
+
const provider = String(run.provider ?? 'unknown');
|
|
149
|
+
if (!providers[provider] || providers[provider] < completedAt)
|
|
150
|
+
providers[provider] = completedAt;
|
|
151
|
+
if (!lastCompletedAt || lastCompletedAt < completedAt)
|
|
152
|
+
lastCompletedAt = completedAt;
|
|
153
|
+
}
|
|
154
|
+
return { lastCompletedAt, providers, recentRuns: runs.slice(0, 5) };
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await repository.close();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Best-effort by contract: while a detached sync holds the single-writer
|
|
162
|
+
// DuckDB lock, even opening a snapshot fails cross-process. Progress (from
|
|
163
|
+
// sidecar heartbeats) must still reach the dashboard, so any failure here
|
|
164
|
+
// degrades to "no last-sync summary" instead of failing /sync/status.
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
88
168
|
async function handleMcpRequest(request, response, paths) {
|
|
89
169
|
if (request.method !== 'POST') {
|
|
90
170
|
methodNotAllowed(response);
|
|
@@ -143,6 +223,8 @@ export function createCatenceHttpServer(options = {}) {
|
|
|
143
223
|
const catalogPaths = options.catalogPaths ?? (options.paths ? null : resolveCatalogPaths());
|
|
144
224
|
const staticPaths = options.paths ?? null;
|
|
145
225
|
const allowedOrigins = options.allowedOrigins ?? [];
|
|
226
|
+
const startSync = options.startSync ?? startDetachedSync;
|
|
227
|
+
const mergeModels = options.mergeModels ?? mergeOpenCodeGoConsoleProfiles;
|
|
146
228
|
return createServer((request, response) => {
|
|
147
229
|
void (async () => {
|
|
148
230
|
if (!addCorsHeaders(request, response, allowedOrigins)) {
|
|
@@ -158,6 +240,10 @@ export function createCatenceHttpServer(options = {}) {
|
|
|
158
240
|
json(response, 200, catenceRuntimeHealth());
|
|
159
241
|
return;
|
|
160
242
|
}
|
|
243
|
+
if (pathname === '/api/v1/health' && request.method === 'GET') {
|
|
244
|
+
json(response, 200, catenceRuntimeHealth());
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
161
247
|
if (pathname === '/api/v1/athletes' && request.method === 'GET') {
|
|
162
248
|
if (!catalogPaths) {
|
|
163
249
|
json(response, 200, { defaultAthleteId: 'local', athletes: [{ id: 'local', label: 'Local athlete' }] });
|
|
@@ -182,6 +268,103 @@ export function createCatenceHttpServer(options = {}) {
|
|
|
182
268
|
}
|
|
183
269
|
return;
|
|
184
270
|
}
|
|
271
|
+
if (pathname === '/api/v1/sync' && request.method === 'POST') {
|
|
272
|
+
let body;
|
|
273
|
+
try {
|
|
274
|
+
body = parseSyncBody(await readJsonBody(request));
|
|
275
|
+
const paths = await resolveSyncPaths(catalogPaths, staticPaths, body);
|
|
276
|
+
// Optional model self-discovery runs before the data sync so new
|
|
277
|
+
// OpenCode Go models appear without a manual script run. Failure is
|
|
278
|
+
// non-fatal: it must never block or fail a data sync. The Console
|
|
279
|
+
// reads <CATENCE_HOME>/config.json, so catalog deployments merge
|
|
280
|
+
// into the catalog root's config instead of the athlete store.
|
|
281
|
+
const discoveryConfigPath = catalogPaths ? path.join(catalogPaths.root, 'config.json') : paths.config;
|
|
282
|
+
let discoveryWarning = null;
|
|
283
|
+
if (body.refreshModels) {
|
|
284
|
+
try {
|
|
285
|
+
await mergeModels({ configPath: discoveryConfigPath, setDefault: false });
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
discoveryWarning = `OpenCode Go model discovery failed; profiles were left unchanged (${error instanceof Error ? error.message : String(error)}).`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const handle = await startSync({
|
|
292
|
+
paths,
|
|
293
|
+
athleteId: body.athleteId ?? 'local',
|
|
294
|
+
provider: body.provider,
|
|
295
|
+
from: body.from,
|
|
296
|
+
refresh: body.refresh,
|
|
297
|
+
// The child resolves the athlete within the catalog, not within
|
|
298
|
+
// the athlete store this route resolved for locks and logs.
|
|
299
|
+
cliHome: catalogPaths ? catalogPaths.root : undefined,
|
|
300
|
+
}, { spawnProcess: options.syncSpawnProcess });
|
|
301
|
+
json(response, 202, { ...handle, ...(discoveryWarning ? { warning: discoveryWarning } : {}), progressEndpoint: '/api/v1/sync/status' });
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
json(response, error instanceof DetachedSyncBusyError ? 409 : 400, {
|
|
305
|
+
error: {
|
|
306
|
+
code: error instanceof DetachedSyncBusyError ? 'sync_in_progress' : 'sync_start_failed',
|
|
307
|
+
message: error instanceof Error ? error.message : String(error),
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (pathname === '/api/v1/models/discover' && request.method === 'POST') {
|
|
314
|
+
// Refresh the OpenCode Go profiles without starting a data sync. The
|
|
315
|
+
// Console's Models page calls this behind its authenticated proxy.
|
|
316
|
+
try {
|
|
317
|
+
const body = await readJsonBody(request).catch(() => ({}));
|
|
318
|
+
const raw = (body ?? {});
|
|
319
|
+
if (raw.setDefault !== undefined && typeof raw.setDefault !== 'boolean')
|
|
320
|
+
throw new Error('setDefault must be a boolean.');
|
|
321
|
+
const configPath = catalogPaths ? path.join(catalogPaths.root, 'config.json') : staticPaths?.config;
|
|
322
|
+
if (!configPath)
|
|
323
|
+
throw new Error('No Catence data store is configured.');
|
|
324
|
+
json(response, 200, jsonSafe(await mergeModels({ configPath, setDefault: raw.setDefault === true })));
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
json(response, 400, { error: { code: 'model_discovery_failed', message: error instanceof Error ? error.message : String(error) } });
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (pathname === '/api/v1/sync/status' && request.method === 'GET') {
|
|
332
|
+
try {
|
|
333
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
334
|
+
const paths = await dashboardPaths(catalogPaths, staticPaths, url);
|
|
335
|
+
const athleteId = catalogPaths ? (await resolveAthlete(catalogPaths, url.searchParams.get('athleteId') ?? '')).athlete.id : 'local';
|
|
336
|
+
json(response, 200, { athleteId, progress: await syncProgress(paths), lastSync: await lastSyncSummary(paths) });
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
json(response, 400, { error: { code: 'sync_status_unavailable', message: error instanceof Error ? error.message : String(error) } });
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (pathname.startsWith('/api/v1/threads/') &&
|
|
344
|
+
pathname.endsWith('/generation') &&
|
|
345
|
+
request.method === 'GET') {
|
|
346
|
+
try {
|
|
347
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
348
|
+
const threadId = segments[segments.length - 2];
|
|
349
|
+
if (!threadId)
|
|
350
|
+
throw new Error('Missing threadId in request path.');
|
|
351
|
+
const base = (catalogPaths ?? staticPaths ?? resolveCatalogPaths());
|
|
352
|
+
const root = base?.root;
|
|
353
|
+
if (!root || typeof root !== 'string') {
|
|
354
|
+
throw new Error('No Catence data store is configured.');
|
|
355
|
+
}
|
|
356
|
+
json(response, 200, await generationStatus(root, threadId));
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
json(response, 400, {
|
|
360
|
+
error: {
|
|
361
|
+
code: 'generation_status_unavailable',
|
|
362
|
+
message: error instanceof Error ? error.message : String(error)
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
185
368
|
if (pathname === '/mcp') {
|
|
186
369
|
await handleMcpRequest(request, response, catalogPaths ?? staticPaths ?? resolveCatalogPaths());
|
|
187
370
|
return;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { type CatalogPaths, hydrateStravaActivity, type CatencePaths } from '../../runtime/index.js';
|
|
2
|
+
import { type CatalogPaths, hydrateStravaActivity, startDetachedSync, type CatencePaths } from '../../runtime/index.js';
|
|
3
3
|
type StravaActivityHydrator = typeof hydrateStravaActivity;
|
|
4
4
|
type McpDependencies = {
|
|
5
5
|
hydrateStravaActivity?: StravaActivityHydrator;
|
|
6
|
+
startDetachedSync?: typeof startDetachedSync;
|
|
6
7
|
};
|
|
7
8
|
export declare function createCatenceMcpServer(paths?: CatencePaths | CatalogPaths, dependencies?: McpDependencies): McpServer;
|
|
8
9
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { ActivityDiscoveryService, AnalyticsService, loadCatalog, configuredMcpRateLimit, DataWriteBusyError, DATASET_CATALOG, demoStoreMetadata, FitnessService, getDataset, hydrateStravaActivity, hydrateStravaSegmentHistory, jsonSafe, loadCatenceConfig, openReadOnlyRepository, queryReadOnlyData, QueryValidationError, ReadOnlyDatabaseError, resolveAthlete, resolveCatalogPaths, searchContext, SlidingWindowLimiter, StravaEnrichmentError, StravaRateLimitError, SwimmingService, WELLNESS_METRICS, WellnessService, } from '../../runtime/index.js';
|
|
4
|
+
import { ActivityDiscoveryService, AnalyticsService, loadCatalog, configuredMcpRateLimit, DataWriteBusyError, DATASET_CATALOG, demoStoreMetadata, DetachedSyncBusyError, FitnessService, getDataset, hydrateStravaActivity, hydrateStravaSegmentHistory, jsonSafe, loadCatenceConfig, openReadOnlyRepository, queryReadOnlyData, QueryValidationError, ReadOnlyDatabaseError, resolveAthlete, resolveCatalogPaths, searchContext, SlidingWindowLimiter, startDetachedSync, StravaEnrichmentError, StravaRateLimitError, SwimmingService, WELLNESS_METRICS, WellnessService, } from '../../runtime/index.js';
|
|
5
5
|
const filterSchema = z.object({
|
|
6
6
|
column: z.string().min(1),
|
|
7
7
|
op: z.enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'between', 'contains']),
|
|
@@ -32,17 +32,19 @@ function textResult(value) {
|
|
|
32
32
|
return { content: [{ type: 'text', text: JSON.stringify(jsonSafe(value), null, 2) }] };
|
|
33
33
|
}
|
|
34
34
|
function errorResult(error) {
|
|
35
|
-
const classified = error instanceof
|
|
36
|
-
? { code: '
|
|
37
|
-
: error instanceof
|
|
38
|
-
? { code: '
|
|
39
|
-
: error instanceof
|
|
40
|
-
? { code: '
|
|
41
|
-
: error instanceof
|
|
42
|
-
? { code:
|
|
43
|
-
: error instanceof
|
|
44
|
-
? { code:
|
|
45
|
-
:
|
|
35
|
+
const classified = error instanceof DetachedSyncBusyError
|
|
36
|
+
? { code: 'sync_in_progress', message: error.message, retryable: true, runId: error.runId }
|
|
37
|
+
: error instanceof DataWriteBusyError
|
|
38
|
+
? { code: 'data_sync_in_progress', message: error.message, retryable: true }
|
|
39
|
+
: error instanceof StravaRateLimitError
|
|
40
|
+
? { code: 'rate_limited', message: error.message, retryable: true, retryAfterSeconds: error.retryAfterSeconds ?? null }
|
|
41
|
+
: error instanceof StravaEnrichmentError
|
|
42
|
+
? { code: 'strava_connection_error', message: error.message }
|
|
43
|
+
: error instanceof ReadOnlyDatabaseError
|
|
44
|
+
? { code: error.code, message: error.message }
|
|
45
|
+
: error instanceof QueryValidationError
|
|
46
|
+
? { code: 'invalid_request', message: error.message }
|
|
47
|
+
: { code: 'data_unavailable', message: error instanceof Error ? error.message : String(error) };
|
|
46
48
|
return { ...textResult({ data: null, provenance: {}, query: {}, caveats: [classified.message], error: classified }), isError: true };
|
|
47
49
|
}
|
|
48
50
|
function hydrationFailure(error) {
|
|
@@ -123,6 +125,7 @@ async function activitySegments(repository, activityId, hydratedStravaActivityId
|
|
|
123
125
|
export function createCatenceMcpServer(paths = resolveCatalogPaths(), dependencies = {}) {
|
|
124
126
|
const server = new McpServer({ name: 'catence', version: '0.1.0' }, { instructions: MCP_INSTRUCTIONS });
|
|
125
127
|
const hydrateActivity = dependencies.hydrateStravaActivity ?? hydrateStravaActivity;
|
|
128
|
+
const startSync = dependencies.startDetachedSync ?? startDetachedSync;
|
|
126
129
|
const catalogPaths = isCatalogPaths(paths) ? paths : null;
|
|
127
130
|
const staticPaths = isCatalogPaths(paths) ? null : paths;
|
|
128
131
|
const scope = new AsyncLocalStorage();
|
|
@@ -176,7 +179,12 @@ export function createCatenceMcpServer(paths = resolveCatalogPaths(), dependenci
|
|
|
176
179
|
...(demoStore ? { demoStore: { generated: true, seed: demoStore.seed, days: demoStore.days, startDate: demoStore.startDate, endDate: demoStore.endDate }, caveats: [...caveats, 'This response uses Catence generated demo data, not personal measurements.'] } : {}),
|
|
177
180
|
});
|
|
178
181
|
};
|
|
179
|
-
return
|
|
182
|
+
// `return await` matters here: a bare `return run()` would let a
|
|
183
|
+
// rejected promise escape the try block, bypassing the classified
|
|
184
|
+
// errorResult payload entirely.
|
|
185
|
+
if (selected)
|
|
186
|
+
return await scope.run(selected, run);
|
|
187
|
+
return await run();
|
|
180
188
|
}
|
|
181
189
|
catch (error) {
|
|
182
190
|
return errorResult(error);
|
|
@@ -231,6 +239,35 @@ export function createCatenceMcpServer(paths = resolveCatalogPaths(), dependenci
|
|
|
231
239
|
data: { ...(await repository.status()), ...(await repository.coverage()) },
|
|
232
240
|
provenance: { database: 'read-only DuckDB snapshot' }, query: {}, caveats: [],
|
|
233
241
|
}))));
|
|
242
|
+
server.registerTool('catence_sync_progress', {
|
|
243
|
+
title: 'Catence sync progress',
|
|
244
|
+
description: 'Read live progress heartbeats for active sync runs, plus the most recent completed or interrupted runs. Read-only.',
|
|
245
|
+
}, tool('catence_sync_progress', async () => useRepository(activePaths(), async (repository) => ({
|
|
246
|
+
data: await repository.progress(),
|
|
247
|
+
provenance: { database: 'read-only DuckDB snapshot' }, query: {}, caveats: [],
|
|
248
|
+
}))));
|
|
249
|
+
server.registerTool('start_detached_sync', {
|
|
250
|
+
title: 'Start a detached data sync',
|
|
251
|
+
description: 'Write-only sync trigger, named like the other explicit lock-guarded write tools. Spawns one detached catence-data sync process for this athlete and returns immediately with the run handle and log file; it refuses while another sync run is active. Track the run with catence_sync_progress; this tool never waits for completion.',
|
|
252
|
+
inputSchema: {
|
|
253
|
+
provider: z.enum(['intervals', 'garmin', 'strava', 'all']).optional(),
|
|
254
|
+
from: z.string().date().optional(),
|
|
255
|
+
refresh: z.boolean().optional(),
|
|
256
|
+
},
|
|
257
|
+
}, tool('start_detached_sync', async (input) => {
|
|
258
|
+
const paths = activePaths();
|
|
259
|
+
const athleteId = activeAthlete()?.id ?? 'local';
|
|
260
|
+
const handle = await startSync({ paths, athleteId, provider: input.provider, from: input.from, refresh: input.refresh });
|
|
261
|
+
return {
|
|
262
|
+
data: { ...handle, progressTool: 'catence_sync_progress' },
|
|
263
|
+
provenance: { operation: 'detached_sync_spawn' },
|
|
264
|
+
query: { ...input },
|
|
265
|
+
caveats: [
|
|
266
|
+
'The sync runs in its own process and keeps running if this session ends; its log file records the full worker output.',
|
|
267
|
+
'Only one sync run may be active per athlete store.',
|
|
268
|
+
],
|
|
269
|
+
};
|
|
270
|
+
}));
|
|
234
271
|
server.registerTool('review_daily_recovery_load', {
|
|
235
272
|
title: 'Review daily recovery and training load',
|
|
236
273
|
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,13 @@ 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, importStagedFile, linkActivity, rebuildRetrievalIndex, reimportNutrition, resolveExtractionErrors, retryDataSync, syncData, syncProgress, unlinkActivity, } from '../elt/application/management.js';
|
|
30
32
|
export type { ProviderChoice } from '../elt/application/management.js';
|
|
33
|
+
export { DETACHED_SYNC_PROVIDERS, DetachedSyncBusyError, startDetachedSync, } from '../elt/application/detached-sync.js';
|
|
34
|
+
export type { DetachedSyncHandle, DetachedSyncProvider, DetachedSyncRequest, DetachedSyncSpawner } from '../elt/application/detached-sync.js';
|
|
35
|
+
export { buildOpenCodeGoConsoleProfiles, classifyOpenCodeGoModel, fetchOpenCodeGoModelIds, mergeOpenCodeGoConsoleProfiles, OPENCODE_GO_API_BASE_ENV, OPENCODE_GO_API_KEY_ENV, OPENCODE_GO_DEFAULT_BASE_URL, OPENCODE_GO_MESSAGES_API_BASE_ENV, } from '../core/runtime/opencode-go.js';
|
|
36
|
+
export type { OpenCodeGoConsoleProfiles, OpenCodeGoMergeResult, OpenCodeGoRoute } from '../core/runtime/opencode-go.js';
|
|
37
|
+
export type { InterruptedRuns, SyncProgressSnapshot, SyncProgressState } from '../contracts/progress.js';
|
|
31
38
|
export { openReadOnlyRepository, ReadOnlyDatabaseError } from '../elt/storage/database.js';
|
|
32
39
|
export { DataWriteBusyError } from '../elt/storage/write-lock.js';
|
|
33
40
|
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,9 @@ 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, importStagedFile, linkActivity, rebuildRetrievalIndex, reimportNutrition, resolveExtractionErrors, retryDataSync, syncData, syncProgress, unlinkActivity, } from '../elt/application/management.js';
|
|
19
|
+
export { DETACHED_SYNC_PROVIDERS, DetachedSyncBusyError, startDetachedSync, } from '../elt/application/detached-sync.js';
|
|
20
|
+
export { buildOpenCodeGoConsoleProfiles, classifyOpenCodeGoModel, fetchOpenCodeGoModelIds, mergeOpenCodeGoConsoleProfiles, OPENCODE_GO_API_BASE_ENV, OPENCODE_GO_API_KEY_ENV, OPENCODE_GO_DEFAULT_BASE_URL, OPENCODE_GO_MESSAGES_API_BASE_ENV, } from '../core/runtime/opencode-go.js';
|
|
18
21
|
export { openReadOnlyRepository, ReadOnlyDatabaseError } from '../elt/storage/database.js';
|
|
19
22
|
export { DataWriteBusyError } from '../elt/storage/write-lock.js';
|
|
20
23
|
export { hydrateStravaActivity, hydrateStravaSegmentHistory, StravaEnrichmentError, StravaRateLimitError, } from '../elt/ingestion/providers/strava/service.js';
|
package/docs/architecture.md
CHANGED
|
@@ -87,7 +87,17 @@ The query layer has four shared pieces:
|
|
|
87
87
|
|
|
88
88
|
### 4. Serving and retrieval
|
|
89
89
|
|
|
90
|
-
The server exposes
|
|
90
|
+
The server exposes these tools, grouped by purpose:
|
|
91
|
+
|
|
92
|
+
- Catalog and status: `list_athletes`, `catence_status`, `catence_sync_progress`.
|
|
93
|
+
- Review workflows: `review_daily_recovery_load`, `review_weekly_training`, `review_activity_deep_dive`, plus the matching `daily_recovery_load_review`, `weekly_training_review`, and `activity_deep_dive` prompts.
|
|
94
|
+
- General data and analytics: `describe_data`, `describe_dataset`, `read_series`, `aggregate_data`, `analyze_series`, `fit_series_model`, `query_read_only_data`, `search_context`.
|
|
95
|
+
- Wellness: `wellness_correlate`, `wellness_baselines`, `wellness_anomalies`, `wellness_coverage`.
|
|
96
|
+
- Fitness and progress: `get_ftp_history`, `get_vo2max_history`, `power_curve_trend`, `power_coverage_report`, `latest_cycling_activities`, `cycling_progress_report`, `get_swim_laps`, `swim_progress_report`.
|
|
97
|
+
- Activity detail and segments: `find_activities`, `get_activity_segments`.
|
|
98
|
+
- Strava hydration (the only writers): `hydrate_strava_activity`, `hydrate_recent_strava_activities`, `hydrate_strava_segment_history`.
|
|
99
|
+
|
|
100
|
+
Resources are mode-dependent. In catalog mode the server registers only `athletes` (`catence://athletes`), the machine-readable roster of configured athlete IDs and labels; in a single-store (non-catalog) deployment it registers resources for status, catalog, a single activity template (`catence://activity/{activityId}`), and a date-range summary template (`catence://summary/{startDate}/{endDate}`).
|
|
91
101
|
|
|
92
102
|
`catence serve` is a loopback-only-by-default Streamable HTTP adapter. It creates a stateless MCP server and transport for each `/mcp` request, so the HTTP and stdio surfaces use the same tool implementation. `GET /health` is a versioned client handshake with runtime version, protocol version, and capabilities; distributed Console builds reject an incompatible protocol before starting a chat. The same listener serves a small read-only `/api/v1/dashboard` snapshot for the local Console: canonical daily-health rows, weekly canonical activity aggregates, recent canonical activities, status, coverage, and explicit missing-data caveats.
|
|
93
103
|
|