catence 0.2.1-beta.2 → 0.2.1-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 +4 -4
- package/config.example.json +1 -0
- package/dist/contracts/release.d.ts +1 -1
- package/dist/contracts/release.js +1 -1
- package/dist/contracts/runtime.d.ts +4 -0
- package/dist/core/query/catalog.d.ts +1 -1
- package/dist/core/query/catalog.js +16 -10
- package/dist/core/query/repository.d.ts +1 -1
- package/dist/core/query/repository.js +31 -2
- package/dist/core/retrieval/index.js +23 -5
- package/dist/core/runtime/athlete-file.d.ts +49 -0
- package/dist/core/runtime/athlete-file.js +177 -0
- package/dist/core/runtime/configuration.js +2 -0
- package/dist/core/runtime/console-accounts.d.ts +52 -0
- package/dist/core/runtime/console-accounts.js +103 -0
- package/dist/core/runtime/mcp-keys.d.ts +65 -0
- package/dist/core/runtime/mcp-keys.js +171 -0
- package/dist/core/runtime/oauth/model.d.ts +44 -0
- package/dist/core/runtime/oauth/model.js +345 -0
- package/dist/core/runtime/oauth/public-client.d.ts +28 -0
- package/dist/core/runtime/oauth/public-client.js +35 -0
- package/dist/core/runtime/oauth/settings.d.ts +33 -0
- package/dist/core/runtime/oauth/settings.js +96 -0
- package/dist/core/runtime/secrets.d.ts +15 -0
- package/dist/core/runtime/secrets.js +48 -7
- package/dist/elt/application/autosync.d.ts +71 -0
- package/dist/elt/application/autosync.js +173 -0
- package/dist/elt/application/detached-sync.d.ts +7 -2
- package/dist/elt/application/detached-sync.js +10 -4
- package/dist/elt/application/management.d.ts +7 -0
- package/dist/elt/application/management.js +103 -7
- package/dist/elt/ingestion/importer.d.ts +11 -1
- package/dist/elt/ingestion/importer.js +63 -14
- package/dist/elt/ingestion/providers/intervals.d.ts +7 -0
- package/dist/elt/ingestion/providers/intervals.js +21 -2
- package/dist/elt/normalization/normalizers.js +122 -0
- package/dist/elt/storage/database.d.ts +6 -1
- package/dist/elt/storage/database.js +7 -1
- package/dist/elt/storage/migrations.js +45 -1
- package/dist/elt/storage/progress-sidecar.d.ts +9 -0
- package/dist/elt/storage/progress-sidecar.js +31 -16
- package/dist/interfaces/cli/commands.js +117 -2
- package/dist/interfaces/http/cli-options.d.ts +64 -1
- package/dist/interfaces/http/cli-options.js +214 -2
- package/dist/interfaces/http/oauth.d.ts +16 -0
- package/dist/interfaces/http/oauth.js +228 -0
- package/dist/interfaces/http/server.d.ts +60 -1
- package/dist/interfaces/http/server.js +810 -16
- package/dist/interfaces/mcp/main.js +35 -3
- package/dist/interfaces/mcp/server.d.ts +21 -1
- package/dist/interfaces/mcp/server.js +107 -21
- package/dist/runtime/index.d.ts +15 -3
- package/dist/runtime/index.js +9 -2
- package/docs/architecture.md +1 -1
- package/docs/configuration.md +76 -6
- package/docs/deployment/docker.md +173 -15
- package/docs/deployment/local-console.md +132 -21
- package/docs/deployment/local-mcp.md +190 -11
- package/docs/development.md +5 -3
- package/docs/llm-providers.md +4 -4
- package/package.json +11 -4
- package/pyproject.toml +1 -1
- package/python/catence/providers/garmin/cli.py +55 -7
- package/python/catence/providers/garmin/progress.py +71 -35
- package/python/catence/providers/garmin/staging.py +11 -4
- package/python/catence/providers/garmin/worker.py +420 -206
- package/uv.lock +3 -3
|
@@ -243,14 +243,33 @@ export class IntervalsExtractor {
|
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Intervals.icu enforces 2500 requests per rolling 15 minutes and 5000 per
|
|
248
|
+
* day; over-limit calls answer 429 with a Retry-After header. Back off
|
|
249
|
+
* cooperatively and retry a bounded number of times so one auto-sync run
|
|
250
|
+
* degrades instead of recording a wall of retryable errors.
|
|
251
|
+
*/
|
|
252
|
+
async request(route, accept) {
|
|
253
|
+
for (let attempt = 0;; attempt++) {
|
|
254
|
+
const headers = { Authorization: basicAuth(this.apiKey) };
|
|
255
|
+
if (accept)
|
|
256
|
+
headers.Accept = accept;
|
|
257
|
+
const response = await fetch(`${apiBaseUrl}${route}`, { headers });
|
|
258
|
+
if (response.status !== 429 || attempt >= 2)
|
|
259
|
+
return response;
|
|
260
|
+
const retryAfterSeconds = Math.min(Math.max(Number(response.headers.get('retry-after')) || 30, 1), 300);
|
|
261
|
+
this.log.warn('Intervals rate limit hit; backing off', { route, retryAfterSeconds, attempt: attempt + 1 });
|
|
262
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfterSeconds * 1_000));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
246
265
|
async getJson(route) {
|
|
247
|
-
const response = await
|
|
266
|
+
const response = await this.request(route, 'application/json');
|
|
248
267
|
if (!response.ok)
|
|
249
268
|
throw new Error(`Intervals ${route} failed with HTTP ${response.status}`);
|
|
250
269
|
return response.json();
|
|
251
270
|
}
|
|
252
271
|
async getBinary(route) {
|
|
253
|
-
const response = await
|
|
272
|
+
const response = await this.request(route, null);
|
|
254
273
|
if (!response.ok)
|
|
255
274
|
throw new Error(`Intervals ${route} failed with HTTP ${response.status}`);
|
|
256
275
|
return new Uint8Array(await response.arrayBuffer());
|
|
@@ -254,6 +254,7 @@ export async function importSourceEntity(database, entity) {
|
|
|
254
254
|
if (entity.entityType === 'activity') {
|
|
255
255
|
await importActivity(database, entity.provider, entity.remoteId, payload, rawHash);
|
|
256
256
|
await importActivityDecoupling(database, entity.provider, entity.remoteId, payload, rawHash);
|
|
257
|
+
await importActivityRunDynamics(database, entity.provider, entity.remoteId, payload, rawHash);
|
|
257
258
|
if (entity.provider === 'intervals')
|
|
258
259
|
await importIntervalsAutoSwimSets(database, entity.remoteId, payload, rawHash);
|
|
259
260
|
if (entity.provider === 'garmin') {
|
|
@@ -271,6 +272,8 @@ export async function importSourceEntity(database, entity) {
|
|
|
271
272
|
}
|
|
272
273
|
if (entity.entityType === 'wellness' || entity.entityType === 'daily_health')
|
|
273
274
|
await importDailyMetrics(database, entity.provider, entity.remoteId, entity.occurredOn, payload, rawHash);
|
|
275
|
+
if (entity.provider === 'garmin' && entity.entityType === 'body_composition')
|
|
276
|
+
await importGarminWeight(database, entity.occurredOn, payload, rawHash);
|
|
274
277
|
if (entity.entityType === 'nutrition_day' || entity.entityType === 'nutrition_log')
|
|
275
278
|
await importNutrition(database, entity.provider, entity.occurredOn, entity.remoteId, payload, rawHash);
|
|
276
279
|
if (entity.provider === 'garmin' && entity.entityType === 'training_metric')
|
|
@@ -567,6 +570,67 @@ async function importActivityDecoupling(database, provider, remoteId, payload, r
|
|
|
567
570
|
unit = excluded.unit, source_type = excluded.source_type, caveats_json = excluded.caveats_json, raw_object_hash = excluded.raw_object_hash`, { provider, activitySourceId, sport: fields.sport, startedAtUtc: fields.startAtUtc, metric: spec.metric, value, unit: spec.unit, caveats: json({}), rawHash });
|
|
568
571
|
}
|
|
569
572
|
}
|
|
573
|
+
/** Containers searched for provider-supplied run dynamics: the payload itself,
|
|
574
|
+
* the multisport summary object, common nested analysis/metrics objects, and
|
|
575
|
+
* the stored metrics_json. Single-activity Garmin payloads carry flat keys;
|
|
576
|
+
* multisport legs carry the same fields under summaryDTO. */
|
|
577
|
+
function runDynamicsContainers(payload) {
|
|
578
|
+
const containers = [payload];
|
|
579
|
+
for (const key of ['summaryDTO', 'analysis', 'data', 'metrics', 'icuAnalysis', 'summaries']) {
|
|
580
|
+
const nested = payload[key];
|
|
581
|
+
if (nested !== null && typeof nested === 'object' && !Array.isArray(nested))
|
|
582
|
+
containers.push(nested);
|
|
583
|
+
}
|
|
584
|
+
const metricsJson = payload.metrics_json;
|
|
585
|
+
if (metricsJson !== undefined && metricsJson !== null)
|
|
586
|
+
containers.push(objectFromJson(metricsJson));
|
|
587
|
+
return containers;
|
|
588
|
+
}
|
|
589
|
+
const RUN_DYNAMICS_METRICS = [
|
|
590
|
+
{ column: 'vertical_oscillation_cm', keys: ['avgVerticalOscillation', 'verticalOscillation'] },
|
|
591
|
+
{ column: 'ground_contact_time_ms', keys: ['avgGctTime', 'groundContactTime', 'avgGroundContactTime'] },
|
|
592
|
+
{ column: 'flight_time_ms', keys: ['avgFlightTime', 'flightTime'] },
|
|
593
|
+
{ column: 'stride_length_cm', keys: ['avgStrideLength', 'strideLength'] },
|
|
594
|
+
{ column: 'vertical_ratio_pct', keys: ['avgVerticalRatio', 'verticalRatio'] },
|
|
595
|
+
{ column: 'cadence_spm', keys: ['averageRunningCadenceInStepsPerMinute', 'avgRunCadence', 'averageCadence'] },
|
|
596
|
+
];
|
|
597
|
+
function firstReportedNumber(containers, keys) {
|
|
598
|
+
for (const container of containers) {
|
|
599
|
+
const found = firstNumber(container, keys);
|
|
600
|
+
if (found !== null)
|
|
601
|
+
return found;
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
/** A5 (persistence): persist provider-supplied running form metrics (vertical
|
|
606
|
+
* oscillation, ground contact time, flight time, stride length, vertical ratio,
|
|
607
|
+
* running cadence) so they are queryable across activities instead of only
|
|
608
|
+
* inside one activity's raw metrics_json. A row is stored only when the payload
|
|
609
|
+
* reported at least one dynamics value; null columns in a stored row mean the
|
|
610
|
+
* provider did not report that metric. */
|
|
611
|
+
async function importActivityRunDynamics(database, provider, remoteId, payload, rawHash) {
|
|
612
|
+
const containers = runDynamicsContainers(payload);
|
|
613
|
+
const reported = RUN_DYNAMICS_METRICS.map((spec) => firstReportedNumber(containers, spec.keys));
|
|
614
|
+
if (reported.every((value) => value === null))
|
|
615
|
+
return;
|
|
616
|
+
const values = Object.fromEntries(RUN_DYNAMICS_METRICS.map((spec, index) => [spec.column, reported[index]]));
|
|
617
|
+
const fields = activityFields(provider, payload);
|
|
618
|
+
await database.run(`INSERT INTO run_dynamics_facts
|
|
619
|
+
(provider, activity_source_id, sport, started_at_utc, vertical_oscillation_cm, ground_contact_time_ms, flight_time_ms, stride_length_cm, vertical_ratio_pct, cadence_spm, source_type, caveats_json, raw_object_hash)
|
|
620
|
+
VALUES ($provider, $activitySourceId, $sport, try_cast($startedAtUtc AS TIMESTAMPTZ), $verticalOscillation, $groundContactTime, $flightTime, $strideLength, $verticalRatio, $cadence, 'provider', $caveats, $rawHash)
|
|
621
|
+
ON CONFLICT (provider, activity_source_id) DO UPDATE SET
|
|
622
|
+
sport = excluded.sport, started_at_utc = excluded.started_at_utc,
|
|
623
|
+
vertical_oscillation_cm = excluded.vertical_oscillation_cm, ground_contact_time_ms = excluded.ground_contact_time_ms,
|
|
624
|
+
flight_time_ms = excluded.flight_time_ms, stride_length_cm = excluded.stride_length_cm,
|
|
625
|
+
vertical_ratio_pct = excluded.vertical_ratio_pct, cadence_spm = excluded.cadence_spm,
|
|
626
|
+
source_type = excluded.source_type, caveats_json = excluded.caveats_json, raw_object_hash = excluded.raw_object_hash`, {
|
|
627
|
+
provider, activitySourceId: providerActivityId(provider, remoteId), sport: fields.sport, startedAtUtc: fields.startAtUtc,
|
|
628
|
+
verticalOscillation: values.vertical_oscillation_cm, groundContactTime: values.ground_contact_time_ms,
|
|
629
|
+
flightTime: values.flight_time_ms, strideLength: values.stride_length_cm,
|
|
630
|
+
verticalRatio: values.vertical_ratio_pct, cadence: values.cadence_spm,
|
|
631
|
+
caveats: json({}), rawHash,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
570
634
|
/** Pull a course's ordered geometry samples, tolerating the several shapes a
|
|
571
635
|
* Garmin course payload may use (a flat ``geoPoints`` array on the live course
|
|
572
636
|
* endpoint, legacy ``coursePoints``, or points nested under a geo/route object).
|
|
@@ -968,6 +1032,64 @@ async function importDailyMetrics(database, provider, remoteId, date, payload, r
|
|
|
968
1032
|
if (provider === 'garmin')
|
|
969
1033
|
await importGarminDailyDetails(database, remoteId, metricDate, payload, rawHash);
|
|
970
1034
|
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Weight-only projection for Garmin weigh-ins. The range endpoint returns
|
|
1037
|
+
* `{dailyWeightSummaries: [{summaryDate, allWeightMetrics: [{weight, ...}]}]}`
|
|
1038
|
+
* with weight in grams, while dayview returns `{dateWeightList: [...]}`.
|
|
1039
|
+
* The worker stages a whole range window as one `body_composition` entity,
|
|
1040
|
+
* so dates are derived per entry here. Multiple weigh-ins on one day resolve
|
|
1041
|
+
* to the latest timestamp. Body-composition fields (fat, muscle, ...) are
|
|
1042
|
+
* deliberately ignored.
|
|
1043
|
+
*/
|
|
1044
|
+
function weightKgFromValue(value) {
|
|
1045
|
+
if (value === null)
|
|
1046
|
+
return null;
|
|
1047
|
+
return value > 1000 ? value / 1000 : value;
|
|
1048
|
+
}
|
|
1049
|
+
function weightEntryDate(entry, fallbackDay) {
|
|
1050
|
+
// Prefer the parent summaryDate (Garmin's local day) over the entry's UTC
|
|
1051
|
+
// timestampGMT, which can fall on a different UTC date.
|
|
1052
|
+
return fallbackDay
|
|
1053
|
+
?? datePart(firstString(entry, ['summaryDate', 'calendarDate', 'date']))
|
|
1054
|
+
?? datePart(timestamp(entry.timestampGMT));
|
|
1055
|
+
}
|
|
1056
|
+
function collectGarminWeightEntries(payload, fallbackDay) {
|
|
1057
|
+
const entries = [];
|
|
1058
|
+
const pushEntry = (entry, fallback) => {
|
|
1059
|
+
const kg = weightKgFromValue(firstNumber(entry, ['weight', 'weightValue', 'weightInKg']));
|
|
1060
|
+
if (kg === null)
|
|
1061
|
+
return;
|
|
1062
|
+
const day = weightEntryDate(entry, fallback ?? fallbackDay);
|
|
1063
|
+
if (!day)
|
|
1064
|
+
return;
|
|
1065
|
+
const ts = typeof entry.timestampGMT === 'number' && Number.isFinite(entry.timestampGMT) ? entry.timestampGMT : 0;
|
|
1066
|
+
entries.push({ date: day, kg, ts });
|
|
1067
|
+
};
|
|
1068
|
+
for (const summary of objectArray(payload.dailyWeightSummaries)) {
|
|
1069
|
+
const day = datePart(firstString(summary, ['summaryDate', 'calendarDate', 'date'])) ?? fallbackDay;
|
|
1070
|
+
for (const metric of objectArray(summary.allWeightMetrics))
|
|
1071
|
+
pushEntry(metric, day);
|
|
1072
|
+
}
|
|
1073
|
+
for (const key of ['dateWeightList', 'allWeightMetrics']) {
|
|
1074
|
+
for (const entry of objectArray(payload[key]))
|
|
1075
|
+
pushEntry(entry, fallbackDay);
|
|
1076
|
+
}
|
|
1077
|
+
if (firstNumber(payload, ['weight', 'weightValue', 'weightInKg']) !== null)
|
|
1078
|
+
pushEntry(payload, fallbackDay);
|
|
1079
|
+
return entries;
|
|
1080
|
+
}
|
|
1081
|
+
async function importGarminWeight(database, date, payload, rawHash) {
|
|
1082
|
+
const fallbackDay = datePart(date);
|
|
1083
|
+
const latestByDay = new Map();
|
|
1084
|
+
for (const entry of collectGarminWeightEntries(payload, fallbackDay)) {
|
|
1085
|
+
const current = latestByDay.get(entry.date);
|
|
1086
|
+
if (!current || entry.ts >= current.ts)
|
|
1087
|
+
latestByDay.set(entry.date, { kg: entry.kg, ts: entry.ts });
|
|
1088
|
+
}
|
|
1089
|
+
for (const [metricDate, { kg }] of latestByDay) {
|
|
1090
|
+
await upsertDailyMetric(database, 'garmin', metricDate, 'weight_kg', kg, null, 'kg', rawHash);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
971
1093
|
async function importNutrition(database, provider, date, remoteId, payload, rawHash) {
|
|
972
1094
|
const nutritionDate = datePart(date) ?? datePart(firstString(payload, ['id', 'date', 'calendarDate'])) ?? null;
|
|
973
1095
|
if (!nutritionDate)
|
|
@@ -60,7 +60,12 @@ export declare class CatenceDatabase implements WriteDataStore {
|
|
|
60
60
|
knownActivitySummaryHashes(provider: Provider): Promise<Record<string, string>>;
|
|
61
61
|
hasGarminLactateThresholdHistory(): Promise<boolean>;
|
|
62
62
|
earliestActivityStartDate(provider: Provider): Promise<string | null>;
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Swim activities that still lack per-length rows: the FIT backfill only
|
|
65
|
+
* has work to do for these, so syncs skip re-parsing already-derived
|
|
66
|
+
* archives entirely.
|
|
67
|
+
*/
|
|
68
|
+
garminSwimActivitySourcesWithoutLengths(): Promise<Array<{
|
|
64
69
|
remote_activity_id: string;
|
|
65
70
|
started_on: string | null;
|
|
66
71
|
}>>;
|
|
@@ -319,13 +319,19 @@ export class CatenceDatabase {
|
|
|
319
319
|
JOIN activity_sources source USING (activity_id) WHERE source.provider = $provider`, { provider });
|
|
320
320
|
return toIsoDate(rows[0]?.earliest ?? null);
|
|
321
321
|
}
|
|
322
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Swim activities that still lack per-length rows: the FIT backfill only
|
|
324
|
+
* has work to do for these, so syncs skip re-parsing already-derived
|
|
325
|
+
* archives entirely.
|
|
326
|
+
*/
|
|
327
|
+
async garminSwimActivitySourcesWithoutLengths() {
|
|
323
328
|
const sports = ['lap_swimming', 'swimming', 'open_water_swimming', 'openwaterswimming'];
|
|
324
329
|
return this.rows(`SELECT source.remote_activity_id, cast(activity.started_at_utc AS VARCHAR) AS started_on
|
|
325
330
|
FROM activity_sources AS source
|
|
326
331
|
JOIN activities AS activity USING (activity_id)
|
|
327
332
|
WHERE source.provider = 'garmin'
|
|
328
333
|
AND lower(coalesce(activity.sport, '')) IN (${sports.map((sport) => `'${sport}'`).join(', ')})
|
|
334
|
+
AND NOT EXISTS (SELECT 1 FROM swim_lengths WHERE swim_lengths.activity_source_id = source.activity_source_id)
|
|
329
335
|
ORDER BY activity.started_at_utc`);
|
|
330
336
|
}
|
|
331
337
|
async latestSourceDate(provider, cursorName) {
|
|
@@ -972,5 +972,49 @@ const activityDecouplingFactsMigration = {
|
|
|
972
972
|
UPDATE retrieval_index_state SET status = 'stale' WHERE index_name = 'context';
|
|
973
973
|
`,
|
|
974
974
|
};
|
|
975
|
-
|
|
975
|
+
const runDynamicsFactsMigration = {
|
|
976
|
+
version: 18,
|
|
977
|
+
name: 'run_dynamics_facts',
|
|
978
|
+
sql: `
|
|
979
|
+
-- A5 (persistence): make provider-supplied running dynamics queryable
|
|
980
|
+
-- historically instead of only inside one activity's raw metrics_json.
|
|
981
|
+
-- Values are provider-normalized running form metrics; a null column means
|
|
982
|
+
-- the provider did not report that metric for the activity. Only activities
|
|
983
|
+
-- with at least one reported value get a row. Single activities carry the
|
|
984
|
+
-- fields flat; multisport legs carry them nested under summaryDTO, and the
|
|
985
|
+
-- normalizer searches both shapes. Rows are written by the activity
|
|
986
|
+
-- normalizer on import (matching activity_decoupling, with no SQL backfill),
|
|
987
|
+
-- so previously imported activities gain rows when their source entity is
|
|
988
|
+
-- refreshed or re-imported; the upsert is idempotent.
|
|
989
|
+
CREATE TABLE IF NOT EXISTS run_dynamics_facts (
|
|
990
|
+
provider VARCHAR NOT NULL,
|
|
991
|
+
activity_source_id VARCHAR NOT NULL,
|
|
992
|
+
sport VARCHAR,
|
|
993
|
+
started_at_utc TIMESTAMPTZ,
|
|
994
|
+
vertical_oscillation_cm DOUBLE,
|
|
995
|
+
ground_contact_time_ms DOUBLE,
|
|
996
|
+
flight_time_ms DOUBLE,
|
|
997
|
+
stride_length_cm DOUBLE,
|
|
998
|
+
vertical_ratio_pct DOUBLE,
|
|
999
|
+
cadence_spm DOUBLE,
|
|
1000
|
+
source_type VARCHAR NOT NULL,
|
|
1001
|
+
caveats_json JSON,
|
|
1002
|
+
raw_object_hash VARCHAR,
|
|
1003
|
+
PRIMARY KEY (provider, activity_source_id)
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
-- Join view so callers can filter or group by logical activity without
|
|
1007
|
+
-- hand-joining activity_sources/activities. Every facts row is current by
|
|
1008
|
+
-- construction (the primary key is the source row), so no ranking filter is
|
|
1009
|
+
-- applied and provider rows are never hidden.
|
|
1010
|
+
CREATE OR REPLACE VIEW run_dynamics AS
|
|
1011
|
+
SELECT facts.*, source.activity_id, source.remote_activity_id, activity.name
|
|
1012
|
+
FROM run_dynamics_facts AS facts
|
|
1013
|
+
JOIN activity_sources AS source USING (activity_source_id)
|
|
1014
|
+
JOIN activities AS activity USING (activity_id);
|
|
1015
|
+
|
|
1016
|
+
UPDATE retrieval_index_state SET status = 'stale' WHERE index_name = 'context';
|
|
1017
|
+
`,
|
|
1018
|
+
};
|
|
1019
|
+
export const migrations = [...baseMigrations, stravaAndIdentityMigration, garminPrimaryAndTrainingMetricsMigration, garminActivitySportAndFtpBackfillMigration, garminPrimaryDailyHealthMigration, garminHistoricalMetricsAndStreamsMigration, garminUtcActivityTimestampMigration, garminLactateThresholdMetricsMigration, swimFactsAndQualityMigration, syncRunProgressMigration, canonicalTrainingDerivedAvgPowerMigration, courseGeometryMigration, activityDecouplingFactsMigration, runDynamicsFactsMigration];
|
|
976
1020
|
//# sourceMappingURL=migrations.js.map
|
|
@@ -48,7 +48,16 @@ export declare class ProgressPump {
|
|
|
48
48
|
private lastHeartbeatAt;
|
|
49
49
|
private interval;
|
|
50
50
|
private stageOverride;
|
|
51
|
+
/**
|
|
52
|
+
* Serializes heartbeat writes in call order. publish() used to fire its
|
|
53
|
+
* database and sidecar writes without awaiting them, so a throttled write
|
|
54
|
+
* could win the sidecar rename race against a later publishFinal() and
|
|
55
|
+
* leave a stale non-terminal stage behind.
|
|
56
|
+
*/
|
|
57
|
+
private writeTail;
|
|
51
58
|
constructor(runId: string, provider: string, options: ProgressPumpOptions);
|
|
59
|
+
private enqueueHeartbeat;
|
|
60
|
+
private writeHeartbeat;
|
|
52
61
|
start(): void;
|
|
53
62
|
publish(state: SyncProgressState): void;
|
|
54
63
|
/** Force subsequent keep-alive heartbeats to report the given stage. */
|
|
@@ -133,11 +133,36 @@ export class ProgressPump {
|
|
|
133
133
|
lastHeartbeatAt = 0;
|
|
134
134
|
interval = null;
|
|
135
135
|
stageOverride = null;
|
|
136
|
+
/**
|
|
137
|
+
* Serializes heartbeat writes in call order. publish() used to fire its
|
|
138
|
+
* database and sidecar writes without awaiting them, so a throttled write
|
|
139
|
+
* could win the sidecar rename race against a later publishFinal() and
|
|
140
|
+
* leave a stale non-terminal stage behind.
|
|
141
|
+
*/
|
|
142
|
+
writeTail = Promise.resolve();
|
|
136
143
|
constructor(runId, provider, options) {
|
|
137
144
|
this.runId = runId;
|
|
138
145
|
this.provider = provider;
|
|
139
146
|
this.options = options;
|
|
140
147
|
}
|
|
148
|
+
enqueueHeartbeat(state) {
|
|
149
|
+
const run = this.writeTail.then(() => this.writeHeartbeat(state));
|
|
150
|
+
// A write failure is already logged inside writeHeartbeat; never let it
|
|
151
|
+
// break the chain (or surface as an unhandled rejection) for later beats.
|
|
152
|
+
this.writeTail = run.then(() => undefined, () => undefined);
|
|
153
|
+
return run;
|
|
154
|
+
}
|
|
155
|
+
async writeHeartbeat(state) {
|
|
156
|
+
const { database, paths, log } = this.options;
|
|
157
|
+
await Promise.all([
|
|
158
|
+
database.heartbeatRun(this.runId, state).catch((error) => {
|
|
159
|
+
log.debug('Progress heartbeat failed', { runId: this.runId, error: error instanceof Error ? error.message : String(error) });
|
|
160
|
+
}),
|
|
161
|
+
writeProgressSidecar(paths, this.runId, state).catch((error) => {
|
|
162
|
+
log.debug('Progress sidecar write failed', { runId: this.runId, error: error instanceof Error ? error.message : String(error) });
|
|
163
|
+
}),
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
141
166
|
start() {
|
|
142
167
|
if (this.interval)
|
|
143
168
|
return;
|
|
@@ -156,13 +181,7 @@ export class ProgressPump {
|
|
|
156
181
|
}
|
|
157
182
|
this.lastHeartbeatAt = now;
|
|
158
183
|
this.lastProgress = state;
|
|
159
|
-
|
|
160
|
-
void database.heartbeatRun(this.runId, state).catch((error) => {
|
|
161
|
-
log.debug('Progress heartbeat failed', { runId: this.runId, error: error instanceof Error ? error.message : String(error) });
|
|
162
|
-
});
|
|
163
|
-
void writeProgressSidecar(paths, this.runId, state).catch((error) => {
|
|
164
|
-
log.debug('Progress sidecar write failed', { runId: this.runId, error: error instanceof Error ? error.message : String(error) });
|
|
165
|
-
});
|
|
184
|
+
void this.enqueueHeartbeat(state).catch(() => undefined);
|
|
166
185
|
}
|
|
167
186
|
/** Force subsequent keep-alive heartbeats to report the given stage. */
|
|
168
187
|
setStage(stage) {
|
|
@@ -180,15 +199,11 @@ export class ProgressPump {
|
|
|
180
199
|
async publishFinal(state) {
|
|
181
200
|
this.lastHeartbeatAt = Date.now();
|
|
182
201
|
this.lastProgress = state;
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
});
|
|
189
|
-
await writeProgressSidecar(paths, this.runId, state).catch((error) => {
|
|
190
|
-
log.debug('Progress sidecar write failed', { runId: this.runId, error: error instanceof Error ? error.message : String(error) });
|
|
191
|
-
});
|
|
202
|
+
// Awaiting the chained write guarantees every earlier heartbeat (in
|
|
203
|
+
// particular a throttled publish still in flight) lands first, so the
|
|
204
|
+
// terminal stage is always the last one persisted before the caller
|
|
205
|
+
// removes the sidecar.
|
|
206
|
+
await this.enqueueHeartbeat(state);
|
|
192
207
|
}
|
|
193
208
|
stop() {
|
|
194
209
|
if (this.interval) {
|
|
@@ -2,7 +2,8 @@
|
|
|
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 {
|
|
5
|
+
import { defaultBearerTokenPath, defaultMcpKeysPath, defaultOauthSettingsPath, disableBearerToken, ensureBearerToken, mcpExposureStatus, oauthStatusFromSettings, readBearerToken, } from '../http/cli-options.js';
|
|
6
|
+
import { addAthlete, addMcpKey, athleteStorePaths, CATENCE_RUNTIME_VERSION, CatenceDatabase, channelForVersion, createDemoStore, dataStatus, deleteAthleteSecret, defaultCatalogHome, disconnectStravaAccount, fetchNpmDistTags, fetchPypiReleases, hasCommand, importStagedFile, initializeCatalog, isGlobalNpmInstall, linkActivity, listMcpKeys, loadCatalog, npmGlobalPrefix, parseAutoSyncInterval, planSelfUpdate, readAutoSyncSchedule, readInstalledConsoleVersion, rebuildRetrievalIndex, reimportNutrition, reimportWeight, resolveExtractionErrors, resolveAthlete, resolveCatalogPaths, retryDataSync, revokeMcpKey, rollMcpKey, runSelfUpdateCommand, setAthleteSecret, syncData, syncProgress, connectStrava, connectStravaWithCallback, unlinkActivity, updateAutoSyncSchedule, } from '../../runtime/index.js';
|
|
6
7
|
const program = new Command()
|
|
7
8
|
.name('catence-data')
|
|
8
9
|
.description('Athlete-scoped Catence data setup, extraction, and normalization.')
|
|
@@ -37,6 +38,85 @@ async function stdinSecret() {
|
|
|
37
38
|
throw new Error('--value-stdin received an empty secret.');
|
|
38
39
|
return value;
|
|
39
40
|
}
|
|
41
|
+
const mcpCommand = program.command('mcp').description('Manage Streamable HTTP MCP exposure: the bearer-token gate, its token, per-account keys, and endpoint URLs.');
|
|
42
|
+
const mcpLocalEndpoint = () => `http://127.0.0.1:${process.env.CATENCE_HTTP_PORT ?? '8787'}`;
|
|
43
|
+
// The keys registry resolves like the serve option: --file, then the
|
|
44
|
+
// environment, then the catalog home default.
|
|
45
|
+
const mcpKeysFile = (file) => file ?? process.env.CATENCE_MCP_KEYS_FILE ?? defaultMcpKeysPath(currentCatalog().root);
|
|
46
|
+
// The OAuth settings file resolves like the serve option: the environment,
|
|
47
|
+
// then the catalog home default.
|
|
48
|
+
const mcpOauthSettingsFile = () => process.env.CATENCE_OAUTH_SETTINGS_FILE ?? defaultOauthSettingsPath(currentCatalog().root);
|
|
49
|
+
mcpCommand.command('status')
|
|
50
|
+
.description('Report whether the bearer-token gate is enabled, the token, the per-account key counts, and the endpoint URLs.')
|
|
51
|
+
.option('--file <path>', 'non-default token file path')
|
|
52
|
+
.action(async (options) => {
|
|
53
|
+
const tokenFile = options.file ?? defaultBearerTokenPath(currentCatalog().root);
|
|
54
|
+
const keys = await listMcpKeys(mcpKeysFile());
|
|
55
|
+
const status = {
|
|
56
|
+
...mcpExposureStatus(tokenFile, mcpLocalEndpoint()),
|
|
57
|
+
keys: { total: keys.length, active: keys.filter((key) => key.revokedAt === undefined).length },
|
|
58
|
+
oauth: oauthStatusFromSettings(mcpOauthSettingsFile()),
|
|
59
|
+
};
|
|
60
|
+
process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
|
|
61
|
+
});
|
|
62
|
+
mcpCommand.command('enable')
|
|
63
|
+
.description('Enable the bearer-token gate: generate the token file (mode 0600) if it does not exist.')
|
|
64
|
+
.option('--file <path>', 'non-default token file path')
|
|
65
|
+
.action(async (options) => {
|
|
66
|
+
const tokenFile = options.file ?? defaultBearerTokenPath(currentCatalog().root);
|
|
67
|
+
ensureBearerToken(tokenFile);
|
|
68
|
+
process.stdout.write(`${JSON.stringify(mcpExposureStatus(tokenFile, mcpLocalEndpoint()), null, 2)}\n`);
|
|
69
|
+
});
|
|
70
|
+
mcpCommand.command('disable')
|
|
71
|
+
.description('Disable the bearer-token gate by removing the token file. Remote access is refused again.')
|
|
72
|
+
.option('--file <path>', 'non-default token file path')
|
|
73
|
+
.action(async (options) => {
|
|
74
|
+
const tokenFile = options.file ?? defaultBearerTokenPath(currentCatalog().root);
|
|
75
|
+
disableBearerToken(tokenFile);
|
|
76
|
+
process.stdout.write(`${JSON.stringify(mcpExposureStatus(tokenFile, mcpLocalEndpoint()), null, 2)}\n`);
|
|
77
|
+
});
|
|
78
|
+
mcpCommand.command('token')
|
|
79
|
+
.description('Print only the bearer token for pasting into an MCP client credential flow.')
|
|
80
|
+
.option('--file <path>', 'non-default token file path')
|
|
81
|
+
.action(async (options) => {
|
|
82
|
+
const tokenFile = options.file ?? defaultBearerTokenPath(currentCatalog().root);
|
|
83
|
+
const token = readBearerToken(tokenFile);
|
|
84
|
+
if (token === null)
|
|
85
|
+
throw new Error(`MCP exposure is not enabled; run 'catence-data mcp enable' first.`);
|
|
86
|
+
process.stdout.write(`${token}\n`);
|
|
87
|
+
});
|
|
88
|
+
const mcpKeyCommand = mcpCommand.command('key').description('Manage per-account MCP bearer keys; raw tokens are printed exactly once and stored only as hashes.');
|
|
89
|
+
mcpKeyCommand.command('add')
|
|
90
|
+
.description('Create a per-account MCP key and print its raw token exactly once.')
|
|
91
|
+
.requiredOption('--account <username>', 'Console account username the key acts for')
|
|
92
|
+
.option('--label <label>', 'human-readable key label')
|
|
93
|
+
.option('--file <path>', 'non-default MCP keys file path')
|
|
94
|
+
.action(async (options) => {
|
|
95
|
+
const { record, token } = await addMcpKey(mcpKeysFile(options.file), { account: options.account, label: options.label });
|
|
96
|
+
process.stdout.write(`${JSON.stringify({ id: record.id, label: record.label, account: record.account, createdAt: record.createdAt, token }, null, 2)}\n`);
|
|
97
|
+
});
|
|
98
|
+
mcpKeyCommand.command('list')
|
|
99
|
+
.description('List MCP keys without their token hashes.')
|
|
100
|
+
.option('--file <path>', 'non-default MCP keys file path')
|
|
101
|
+
.action(async (options) => {
|
|
102
|
+
process.stdout.write(`${JSON.stringify({ keys: await listMcpKeys(mcpKeysFile(options.file)) }, null, 2)}\n`);
|
|
103
|
+
});
|
|
104
|
+
mcpKeyCommand.command('revoke')
|
|
105
|
+
.description('Revoke an MCP key by id; its token stops working immediately.')
|
|
106
|
+
.argument('<id>', 'key id')
|
|
107
|
+
.option('--file <path>', 'non-default MCP keys file path')
|
|
108
|
+
.action(async (id, options) => {
|
|
109
|
+
await revokeMcpKey(mcpKeysFile(options.file), id);
|
|
110
|
+
process.stdout.write(`${JSON.stringify({ revoked: id }, null, 2)}\n`);
|
|
111
|
+
});
|
|
112
|
+
mcpKeyCommand.command('roll')
|
|
113
|
+
.description('Replace an MCP key’s token and print the new raw token exactly once.')
|
|
114
|
+
.argument('<id>', 'key id')
|
|
115
|
+
.option('--file <path>', 'non-default MCP keys file path')
|
|
116
|
+
.action(async (id, options) => {
|
|
117
|
+
const { record, token } = await rollMcpKey(mcpKeysFile(options.file), id);
|
|
118
|
+
process.stdout.write(`${JSON.stringify({ id: record.id, label: record.label, account: record.account, createdAt: record.createdAt, token }, null, 2)}\n`);
|
|
119
|
+
});
|
|
40
120
|
program.command('setup')
|
|
41
121
|
.description('Create a home catalog and its first athlete store. Existing 0.1 stores must be re-synced.')
|
|
42
122
|
.option('--athlete <id>', 'first athlete ID')
|
|
@@ -61,7 +141,7 @@ athleteCommand.command('add')
|
|
|
61
141
|
.action(async (options) => {
|
|
62
142
|
process.stdout.write(`${JSON.stringify(await addAthlete(currentCatalog(), { id: options.id, label: options.label, setDefault: options.default }), null, 2)}\n`);
|
|
63
143
|
});
|
|
64
|
-
const secretCommand = program.command('secret').description('Write a provider value
|
|
144
|
+
const secretCommand = program.command('secret').description('Write or remove a provider value in one athlete’s owner-only secret store.');
|
|
65
145
|
secretCommand.command('set')
|
|
66
146
|
.requiredOption('--provider <provider>', 'garmin, intervals, or strava')
|
|
67
147
|
.requiredOption('--field <field>', 'provider secret field')
|
|
@@ -73,6 +153,16 @@ secretCommand.command('set')
|
|
|
73
153
|
await setAthleteSecret(paths, options.provider, options.field, await stdinSecret());
|
|
74
154
|
process.stdout.write(`${JSON.stringify({ athleteId: program.opts().athlete, provider: options.provider, field: options.field, stored: true }, null, 2)}\n`);
|
|
75
155
|
});
|
|
156
|
+
secretCommand.command('remove')
|
|
157
|
+
.requiredOption('--provider <provider>', 'garmin, intervals, or strava')
|
|
158
|
+
.requiredOption('--field <field>', 'provider secret field')
|
|
159
|
+
.action(async (options) => {
|
|
160
|
+
if (!['garmin', 'intervals', 'strava'].includes(options.provider))
|
|
161
|
+
throw new Error('--provider must be garmin, intervals, or strava.');
|
|
162
|
+
const { paths } = await currentAthlete();
|
|
163
|
+
await deleteAthleteSecret(paths, options.provider, options.field);
|
|
164
|
+
process.stdout.write(`${JSON.stringify({ athleteId: program.opts().athlete, provider: options.provider, field: options.field, removed: true }, null, 2)}\n`);
|
|
165
|
+
});
|
|
76
166
|
program.command('demo')
|
|
77
167
|
.description('Create a generated demo athlete in an isolated demo catalog without contacting providers.')
|
|
78
168
|
.option('--days <days>', 'generated days of wellness and training data', '90')
|
|
@@ -120,6 +210,27 @@ program.command('retry')
|
|
|
120
210
|
const { paths } = await currentAthlete();
|
|
121
211
|
process.stdout.write(`${JSON.stringify(await retryDataSync(paths, options.run), null, 2)}\n`);
|
|
122
212
|
});
|
|
213
|
+
const autoSyncCommand = program.command('auto-sync').description('Manage the automatic sync schedule for one athlete. The scheduler runs inside the Catence server (default: enabled, every 2 hours).');
|
|
214
|
+
autoSyncCommand.command('status')
|
|
215
|
+
.description('Report the athlete’s automatic sync schedule: enabled flag, interval, and next run.')
|
|
216
|
+
.action(async () => {
|
|
217
|
+
const { athlete, paths } = await currentAthlete();
|
|
218
|
+
process.stdout.write(`${JSON.stringify(await readAutoSyncSchedule(paths, athlete.id), null, 2)}\n`);
|
|
219
|
+
});
|
|
220
|
+
autoSyncCommand.command('enable')
|
|
221
|
+
.description('Turn automatic syncing on, optionally changing the interval (15m, 30m, 1h, 2h, 6h, 12h, 24h).')
|
|
222
|
+
.option('--interval <interval>', 'sync interval label or minutes', '2h')
|
|
223
|
+
.action(async (options) => {
|
|
224
|
+
const { athlete, paths } = await currentAthlete();
|
|
225
|
+
const intervalMinutes = parseAutoSyncInterval(options.interval);
|
|
226
|
+
process.stdout.write(`${JSON.stringify(await updateAutoSyncSchedule(paths, athlete.id, { enabled: true, intervalMinutes }), null, 2)}\n`);
|
|
227
|
+
});
|
|
228
|
+
autoSyncCommand.command('disable')
|
|
229
|
+
.description('Turn automatic syncing off. Manual sync keeps working.')
|
|
230
|
+
.action(async () => {
|
|
231
|
+
const { athlete, paths } = await currentAthlete();
|
|
232
|
+
process.stdout.write(`${JSON.stringify(await updateAutoSyncSchedule(paths, athlete.id, { enabled: false }), null, 2)}\n`);
|
|
233
|
+
});
|
|
123
234
|
program.command('auth').description('Connect a source account using one athlete’s local credentials.')
|
|
124
235
|
.command('strava')
|
|
125
236
|
.option('--code <authorization-code>', 'OAuth authorization code returned by Strava')
|
|
@@ -256,6 +367,10 @@ program.command('reimport-nutrition').description('Re-run normalization over cap
|
|
|
256
367
|
const { paths } = await currentAthlete();
|
|
257
368
|
process.stdout.write(`${JSON.stringify(await reimportNutrition(paths), null, 2)}\n`);
|
|
258
369
|
});
|
|
370
|
+
program.command('reimport-weight').description('Backfill weight_kg from captured Garmin weigh-in entities without contacting providers.').action(async () => {
|
|
371
|
+
const { paths } = await currentAthlete();
|
|
372
|
+
process.stdout.write(`${JSON.stringify(await reimportWeight(paths), null, 2)}\n`);
|
|
373
|
+
});
|
|
259
374
|
program
|
|
260
375
|
.command('import')
|
|
261
376
|
.description('Import a staged JSONL file (for example a swim-length backfill) into one athlete’s data store.')
|
|
@@ -3,7 +3,70 @@ export type ServeCliOptions = {
|
|
|
3
3
|
host: string;
|
|
4
4
|
port: number;
|
|
5
5
|
allowedOrigins: string[];
|
|
6
|
+
authTokenFile?: string;
|
|
7
|
+
mcpKeysFile?: string;
|
|
8
|
+
oauthSettingsFile?: string;
|
|
9
|
+
oauth: boolean;
|
|
10
|
+
publicUrl?: string;
|
|
6
11
|
help: boolean;
|
|
7
12
|
};
|
|
8
|
-
export declare const SERVE_USAGE = "Usage: catence serve [options]\n\nRun Catence as a local Streamable HTTP MCP server.\n\nOptions:\n --home <directory> Catence catalog home. Defaults to CATENCE_HOME or ~/.catence.\n --host <host> Listener host. Defaults to 127.0.0.1.\n --port <port> Listener port. Defaults to 8787.\n --allow-origin <origin> Browser origin allowed to call Catence APIs. May be repeated.\n -h, --help Show this help text.";
|
|
13
|
+
export declare const SERVE_USAGE = "Usage: catence serve [options]\n\nRun Catence as a local Streamable HTTP MCP server.\n\nOptions:\n --home <directory> Catence catalog home. Defaults to CATENCE_HOME or ~/.catence.\n --host <host> Listener host. Defaults to 127.0.0.1.\n --port <port> Listener port. Defaults to 8787.\n --allow-origin <origin> Browser origin allowed to call Catence APIs. May be repeated.\n --auth-token-file <path> Bearer-token file for the HTTP gate. Its presence switches authentication on;\n generate/inspect it with 'catence-data mcp enable|token'.\n Defaults to CATENCE_MCP_TOKEN_FILE or <catalog-home>/mcp-token.\n --mcp-keys-file <path> Per-account MCP key registry for the HTTP gate. Any active key in it\n switches authentication on; manage keys with 'catence-data mcp key'.\n Defaults to CATENCE_MCP_KEYS_FILE or <catalog-home>/mcp-keys.json.\n --oauth Run the OAuth 2.1 authorization server (issuer = --public-url) so\n ChatGPT/Codex-style MCP clients connect with a per-account consent\n login. Requires --public-url. Defaults to CATENCE_OAUTH_ENABLED=1/true.\n The Console settings file overrides this at runtime.\n --public-url <url> Public https URL of this server (no query string or fragment), e.g.\n the tunnel URL. Required when --oauth is enabled.\n Defaults to CATENCE_PUBLIC_URL.\n --oauth-settings-file <path>\n Console-managed OAuth settings that override --oauth/--public-url\n at runtime. Defaults to CATENCE_OAUTH_SETTINGS_FILE or\n <catalog-home>/oauth/settings.json.\n -h, --help Show this help text.";
|
|
14
|
+
/** The canonical token location inside a catalog home. */
|
|
15
|
+
export declare function defaultBearerTokenPath(catalogRoot: string): string;
|
|
16
|
+
/** The canonical per-account MCP key registry location inside a catalog home. */
|
|
17
|
+
export declare function defaultMcpKeysPath(catalogRoot: string): string;
|
|
18
|
+
/** The canonical Console-managed OAuth settings location inside a catalog home. */
|
|
19
|
+
export declare function defaultOauthSettingsPath(catalogRoot: string): string;
|
|
20
|
+
/** Read and validate a stored bearer token: trimmed, non-empty, or fail loudly. */
|
|
21
|
+
export declare function loadBearerToken(filePath: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* The bearer token for the server: the stored one, or a freshly generated
|
|
24
|
+
* token written to the file (mode 0600) when it does not exist yet. Users
|
|
25
|
+
* never hand-craft tokens; they enable auth and read the result with
|
|
26
|
+
* `catence-data mcp-token`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function ensureBearerToken(filePath: string): string;
|
|
29
|
+
export declare function disableBearerToken(filePath: string): void;
|
|
30
|
+
/** The bearer token when exposure is enabled, or null when no token file exists. */
|
|
31
|
+
export declare function readBearerToken(filePath: string): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* The currently active bearer token for the HTTP gate, cached by the token
|
|
34
|
+
* file's mtime so enabling or disabling is picked up without a restart. A
|
|
35
|
+
* missing file disables the gate; an existing but unreadable or empty file
|
|
36
|
+
* fails closed (the gate rejects everything except health probes).
|
|
37
|
+
*/
|
|
38
|
+
export declare function activeBearerToken(filePath: string): string | null | 'invalid';
|
|
39
|
+
export type McpExposureStatus = {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
tokenFile: string;
|
|
42
|
+
endpoint: string;
|
|
43
|
+
healthEndpoint: string;
|
|
44
|
+
token?: string;
|
|
45
|
+
oauth?: {
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
publicUrl?: string;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export declare function mcpExposureStatus(filePath: string, endpointBase: string, oauth?: {
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
publicUrl?: string;
|
|
53
|
+
}): McpExposureStatus;
|
|
54
|
+
/** True when CATENCE_OAUTH_ENABLED is 1/true. */
|
|
55
|
+
export declare function oauthEnabledFromEnv(env?: NodeJS.ProcessEnv): boolean;
|
|
56
|
+
/** The OAuth exposure reported by `catence-data mcp status`, read from the environment. */
|
|
57
|
+
export declare function oauthStatusFromEnv(env?: NodeJS.ProcessEnv): {
|
|
58
|
+
enabled: boolean;
|
|
59
|
+
publicUrl?: string;
|
|
60
|
+
};
|
|
61
|
+
export type McpOauthStatusSource = 'console' | 'environment' | 'disabled';
|
|
62
|
+
/**
|
|
63
|
+
* The effective OAuth exposure: the Console settings file when a valid one
|
|
64
|
+
* exists (a malformed one fails closed to disabled), else the environment.
|
|
65
|
+
* `source` records where the effective values come from.
|
|
66
|
+
*/
|
|
67
|
+
export declare function oauthStatusFromSettings(settingsFile: string, env?: NodeJS.ProcessEnv): {
|
|
68
|
+
enabled: boolean;
|
|
69
|
+
publicUrl?: string;
|
|
70
|
+
source: McpOauthStatusSource;
|
|
71
|
+
};
|
|
9
72
|
export declare function parseServeCliOptions(arguments_: readonly string[]): ServeCliOptions;
|