catence 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +373 -29
- package/config.example.json +63 -0
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.js +5 -0
- package/dist/contracts/release.d.ts +20 -0
- package/dist/contracts/release.js +17 -0
- package/dist/contracts/runtime.d.ts +11 -0
- package/dist/contracts/runtime.js +2 -0
- package/dist/contracts/staging.d.ts +183 -0
- package/dist/contracts/staging.js +36 -0
- package/dist/contracts/storage.d.ts +15 -0
- package/dist/contracts/storage.js +2 -0
- package/dist/core/dashboard/snapshot.d.ts +16 -0
- package/dist/core/dashboard/snapshot.js +65 -0
- package/dist/core/query/activity-discovery.d.ts +21 -0
- package/dist/core/query/activity-discovery.js +133 -0
- package/dist/core/query/analytics.d.ts +59 -0
- package/dist/core/query/analytics.js +395 -0
- package/dist/core/query/catalog.d.ts +26 -0
- package/dist/core/query/catalog.js +141 -0
- package/dist/core/query/fitness.d.ts +40 -0
- package/dist/core/query/fitness.js +297 -0
- package/dist/core/query/repository.d.ts +19 -0
- package/dist/core/query/repository.js +182 -0
- package/dist/core/query/sql-guard.d.ts +7 -0
- package/dist/core/query/sql-guard.js +121 -0
- package/dist/core/query/sql.d.ts +2 -0
- package/dist/core/query/sql.js +5 -0
- package/dist/core/query/swimming.d.ts +19 -0
- package/dist/core/query/swimming.js +184 -0
- package/dist/core/query/wellness.d.ts +45 -0
- package/dist/core/query/wellness.js +292 -0
- package/dist/core/retrieval/index.d.ts +13 -0
- package/dist/core/retrieval/index.js +248 -0
- package/dist/core/runtime/configuration.d.ts +71 -0
- package/dist/core/runtime/configuration.js +106 -0
- package/dist/core/runtime/limiter.d.ts +13 -0
- package/dist/core/runtime/limiter.js +17 -0
- package/dist/elt/application/demo.d.ts +23 -0
- package/dist/elt/application/demo.js +165 -0
- package/dist/elt/application/management.d.ts +25 -0
- package/dist/elt/application/management.js +283 -0
- package/dist/elt/ingestion/importer.d.ts +8 -0
- package/dist/elt/ingestion/importer.js +87 -0
- package/dist/elt/ingestion/providers/intervals.d.ts +21 -0
- package/dist/elt/ingestion/providers/intervals.js +224 -0
- package/dist/elt/ingestion/providers/registry.d.ts +19 -0
- package/dist/elt/ingestion/providers/registry.js +62 -0
- package/dist/elt/ingestion/providers/strava/service.d.ts +42 -0
- package/dist/elt/ingestion/providers/strava/service.js +365 -0
- package/dist/elt/normalization/activities/linking.d.ts +5 -0
- package/dist/elt/normalization/activities/linking.js +165 -0
- package/dist/elt/normalization/normalizers.d.ts +5 -0
- package/dist/elt/normalization/normalizers.js +816 -0
- package/dist/elt/normalization/segments/strava-normalizer.d.ts +3 -0
- package/dist/elt/normalization/segments/strava-normalizer.js +93 -0
- package/dist/elt/storage/artifacts.d.ts +13 -0
- package/dist/elt/storage/artifacts.js +48 -0
- package/dist/elt/storage/database.d.ts +74 -0
- package/dist/elt/storage/database.js +296 -0
- package/dist/elt/storage/migrations.d.ts +3 -0
- package/dist/elt/storage/migrations.js +832 -0
- package/dist/elt/storage/migrations.legacy.d.ts +12 -0
- package/dist/elt/storage/migrations.legacy.js +332 -0
- package/dist/elt/storage/sql.d.ts +2 -0
- package/dist/elt/storage/sql.js +7 -0
- package/dist/elt/storage/write-lock.d.ts +6 -0
- package/dist/elt/storage/write-lock.js +28 -0
- package/dist/elt/streams.d.ts +24 -0
- package/dist/elt/streams.js +62 -0
- package/dist/interfaces/cli/commands.d.ts +2 -0
- package/dist/interfaces/cli/commands.js +101 -0
- package/dist/interfaces/cli/main.d.ts +2 -0
- package/dist/interfaces/cli/main.js +3 -0
- package/dist/interfaces/http/cli-options.d.ts +9 -0
- package/dist/interfaces/http/cli-options.js +81 -0
- package/dist/interfaces/http/server.d.ts +14 -0
- package/dist/interfaces/http/server.js +169 -0
- package/dist/interfaces/mcp/cli-options.d.ts +6 -0
- package/dist/interfaces/mcp/cli-options.js +36 -0
- package/dist/interfaces/mcp/main.d.ts +2 -0
- package/dist/interfaces/mcp/main.js +59 -0
- package/dist/interfaces/mcp/server.d.ts +8 -0
- package/dist/interfaces/mcp/server.js +521 -0
- package/dist/runtime/index.d.ts +29 -0
- package/dist/runtime/index.js +19 -0
- package/docs/architecture.md +100 -0
- package/docs/assets/image.png +0 -0
- package/docs/mcp-activity-retrieval.md +377 -0
- package/package.json +81 -6
- package/pyproject.toml +28 -0
- package/python/__init__.py +1 -0
- package/python/catence/__init__.py +0 -0
- package/python/catence/providers/__init__.py +0 -0
- package/python/catence/providers/garmin/__init__.py +3 -0
- package/python/catence/providers/garmin/cli.py +62 -0
- package/python/catence/providers/garmin/contracts.py +23 -0
- package/python/catence/providers/garmin/registry.py +48 -0
- package/python/catence/providers/garmin/staging.py +87 -0
- package/python/catence/providers/garmin/streams.py +195 -0
- package/python/catence/providers/garmin/worker.py +450 -0
- package/python/catence/providers/strava/__init__.py +3 -0
- package/python/catence/providers/strava/cli.py +404 -0
- package/python/catence/providers/strava/staging.py +73 -0
- package/uv.lock +708 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { config as loadDotenv } from 'dotenv';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
loadDotenv({ quiet: true });
|
|
6
|
+
export function resolvePaths(root = process.env.CATENCE_DATA_DIR ?? '.catence') {
|
|
7
|
+
const absoluteRoot = path.resolve(root);
|
|
8
|
+
return {
|
|
9
|
+
root: absoluteRoot,
|
|
10
|
+
database: path.join(absoluteRoot, 'catence.duckdb'),
|
|
11
|
+
raw: path.join(absoluteRoot, 'raw'),
|
|
12
|
+
lake: path.join(absoluteRoot, 'lake', 'activity_samples'),
|
|
13
|
+
staging: path.join(absoluteRoot, 'staging'),
|
|
14
|
+
config: path.join(absoluteRoot, 'config.json'),
|
|
15
|
+
secrets: path.join(absoluteRoot, 'secrets'),
|
|
16
|
+
lock: path.join(absoluteRoot, '.catence-write.lock'),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export async function ensurePaths(paths) {
|
|
20
|
+
await Promise.all([mkdir(paths.raw, { recursive: true }), mkdir(paths.lake, { recursive: true }), mkdir(paths.staging, { recursive: true }), mkdir(paths.secrets, { recursive: true, mode: 0o700 })]);
|
|
21
|
+
}
|
|
22
|
+
export function defaultFromDate(now = new Date()) {
|
|
23
|
+
const date = new Date(Date.UTC(now.getUTCFullYear() - 1, now.getUTCMonth(), now.getUTCDate()));
|
|
24
|
+
return date.toISOString().slice(0, 10);
|
|
25
|
+
}
|
|
26
|
+
export function requireIntervalsConfig() {
|
|
27
|
+
const apiKey = process.env.INTERVALS_API_KEY;
|
|
28
|
+
if (!apiKey)
|
|
29
|
+
throw new Error('INTERVALS_API_KEY is required for an Intervals sync.');
|
|
30
|
+
return { apiKey, athleteId: process.env.INTERVALS_ATHLETE_ID ?? '0' };
|
|
31
|
+
}
|
|
32
|
+
const rateLimitSchema = z.object({ requests: z.number().int().positive(), windowSeconds: z.number().int().positive() }).strict();
|
|
33
|
+
const nullableRateLimitSchema = rateLimitSchema.nullable();
|
|
34
|
+
const environmentVariableSchema = z.string().regex(/^[A-Z][A-Z0-9_]*$/, 'must be an uppercase environment-variable name');
|
|
35
|
+
const consoleModelSchema = z.object({
|
|
36
|
+
label: z.string().min(1).optional(),
|
|
37
|
+
model: z.string().min(1),
|
|
38
|
+
}).strict();
|
|
39
|
+
const consoleProfileSchema = z.object({
|
|
40
|
+
label: z.string().min(1).optional(),
|
|
41
|
+
model: z.string().min(1).optional(),
|
|
42
|
+
models: z.record(z.string().min(1), consoleModelSchema).optional(),
|
|
43
|
+
defaultModel: z.string().min(1).optional(),
|
|
44
|
+
defaultReasoningEffort: z.enum(['minimal', 'low', 'medium', 'high', 'xhigh']).optional(),
|
|
45
|
+
apiKeyEnv: environmentVariableSchema.optional(),
|
|
46
|
+
apiBaseEnv: environmentVariableSchema.optional(),
|
|
47
|
+
apiVersionEnv: environmentVariableSchema.optional(),
|
|
48
|
+
}).strict().superRefine((profile, context) => {
|
|
49
|
+
if (Boolean(profile.model) === Boolean(profile.models)) {
|
|
50
|
+
context.addIssue({ code: 'custom', message: 'must define exactly one of model or models' });
|
|
51
|
+
}
|
|
52
|
+
if (profile.defaultModel && !profile.models) {
|
|
53
|
+
context.addIssue({ code: 'custom', path: ['defaultModel'], message: 'requires models' });
|
|
54
|
+
}
|
|
55
|
+
if (profile.models && profile.defaultModel && !Object.hasOwn(profile.models, profile.defaultModel)) {
|
|
56
|
+
context.addIssue({ code: 'custom', path: ['defaultModel'], message: 'must name one of models' });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
const consoleLimitsSchema = z.object({
|
|
60
|
+
toolRounds: z.number().int().min(1).max(32).optional(),
|
|
61
|
+
toolResultCharacters: z.number().int().min(1_000).max(250_000).optional(),
|
|
62
|
+
}).strict();
|
|
63
|
+
const catenceConfigSchema = z.object({
|
|
64
|
+
mcp: z.object({
|
|
65
|
+
rateLimits: z.object({
|
|
66
|
+
server: nullableRateLimitSchema.optional(),
|
|
67
|
+
tools: z.record(z.string(), nullableRateLimitSchema).optional(),
|
|
68
|
+
resources: z.record(z.string(), nullableRateLimitSchema).optional(),
|
|
69
|
+
}).strict().optional(),
|
|
70
|
+
}).strict().optional(),
|
|
71
|
+
providers: z.object({
|
|
72
|
+
strava: z.object({
|
|
73
|
+
budget: z.object({
|
|
74
|
+
maxConcurrentRequests: z.number().int().positive().nullable().optional(),
|
|
75
|
+
readRequestsPer15Minutes: z.number().int().positive().nullable().optional(),
|
|
76
|
+
readRequestsPerDay: z.number().int().positive().nullable().optional(),
|
|
77
|
+
}).strict().optional(),
|
|
78
|
+
}).strict().optional(),
|
|
79
|
+
}).strict().optional(),
|
|
80
|
+
console: z.object({
|
|
81
|
+
defaultProfile: z.string().min(1).optional(),
|
|
82
|
+
limits: consoleLimitsSchema.optional(),
|
|
83
|
+
profiles: z.record(z.string().min(1), consoleProfileSchema).optional(),
|
|
84
|
+
}).strict().optional(),
|
|
85
|
+
}).strict();
|
|
86
|
+
export async function loadCatenceConfig(paths) {
|
|
87
|
+
const contents = await readFile(paths.config, 'utf8').catch((error) => {
|
|
88
|
+
if (error.code === 'ENOENT')
|
|
89
|
+
return '';
|
|
90
|
+
throw error;
|
|
91
|
+
});
|
|
92
|
+
if (!contents.trim())
|
|
93
|
+
return {};
|
|
94
|
+
try {
|
|
95
|
+
return catenceConfigSchema.parse(JSON.parse(contents));
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
throw new Error(`Invalid Catence config at ${paths.config}: ${message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function configuredMcpRateLimit(config, type, name) {
|
|
103
|
+
const scoped = config.mcp?.rateLimits?.[type];
|
|
104
|
+
return scoped?.[name] ?? scoped?.['*'] ?? config.mcp?.rateLimits?.server ?? null;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=configuration.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type RateLimit = {
|
|
2
|
+
requests: number;
|
|
3
|
+
windowSeconds: number;
|
|
4
|
+
};
|
|
5
|
+
export declare class SlidingWindowLimiter {
|
|
6
|
+
private readonly entries;
|
|
7
|
+
check(key: string, limit: RateLimit | null, now?: number): {
|
|
8
|
+
allowed: true;
|
|
9
|
+
} | {
|
|
10
|
+
allowed: false;
|
|
11
|
+
retryAfterSeconds: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class SlidingWindowLimiter {
|
|
2
|
+
entries = new Map();
|
|
3
|
+
check(key, limit, now = Date.now()) {
|
|
4
|
+
if (!limit)
|
|
5
|
+
return { allowed: true };
|
|
6
|
+
const windowMs = limit.windowSeconds * 1_000;
|
|
7
|
+
const recent = (this.entries.get(key) ?? []).filter((entry) => entry > now - windowMs);
|
|
8
|
+
if (recent.length >= limit.requests) {
|
|
9
|
+
this.entries.set(key, recent);
|
|
10
|
+
return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil((recent[0] + windowMs - now) / 1_000)) };
|
|
11
|
+
}
|
|
12
|
+
recent.push(now);
|
|
13
|
+
this.entries.set(key, recent);
|
|
14
|
+
return { allowed: true };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=limiter.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CatencePaths } from '../../contracts/runtime.js';
|
|
2
|
+
export type DemoStoreMetadata = {
|
|
3
|
+
formatVersion: number;
|
|
4
|
+
generated: true;
|
|
5
|
+
seed: number;
|
|
6
|
+
days: number;
|
|
7
|
+
startDate: string;
|
|
8
|
+
endDate: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
};
|
|
11
|
+
export type CreateDemoStoreOptions = {
|
|
12
|
+
/** Keep the demo small enough for fast evaluation while still supporting trends. */
|
|
13
|
+
days?: number;
|
|
14
|
+
seed?: number;
|
|
15
|
+
/** Primarily useful for deterministic tests and documentation screenshots. */
|
|
16
|
+
endDate?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function demoStoreMetadata(paths: CatencePaths): Promise<DemoStoreMetadata | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Create generated data only in a fresh directory or one already marked as a
|
|
21
|
+
* Catence demo. A real data directory is never opened for mutation here.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createDemoStore(paths: CatencePaths, options?: CreateDemoStoreOptions): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { ensurePaths } from '../../core/runtime/configuration.js';
|
|
6
|
+
import { importRecord } from '../ingestion/importer.js';
|
|
7
|
+
import { CatenceDatabase } from '../storage/database.js';
|
|
8
|
+
const DEMO_MARKER = 'demo-store.json';
|
|
9
|
+
const DEMO_FORMAT_VERSION = 1;
|
|
10
|
+
function markerPath(paths) {
|
|
11
|
+
return path.join(paths.root, DEMO_MARKER);
|
|
12
|
+
}
|
|
13
|
+
function hash(value) {
|
|
14
|
+
return createHash('sha256').update(value).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
function isoDate(date) {
|
|
17
|
+
return date.toISOString().slice(0, 10);
|
|
18
|
+
}
|
|
19
|
+
function addDays(date, days) {
|
|
20
|
+
return isoDate(new Date(Date.parse(`${date}T00:00:00.000Z`) + days * 86_400_000));
|
|
21
|
+
}
|
|
22
|
+
function seededRandom(seed) {
|
|
23
|
+
let state = seed >>> 0;
|
|
24
|
+
return () => {
|
|
25
|
+
state += 0x6D2B79F5;
|
|
26
|
+
let value = state;
|
|
27
|
+
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
28
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
29
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function rounded(value, decimals = 0) {
|
|
33
|
+
const multiplier = 10 ** decimals;
|
|
34
|
+
return Math.round(value * multiplier) / multiplier;
|
|
35
|
+
}
|
|
36
|
+
function validDate(date) {
|
|
37
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(date) && !Number.isNaN(Date.parse(`${date}T00:00:00.000Z`));
|
|
38
|
+
}
|
|
39
|
+
async function hasDirectoryContents(directory) {
|
|
40
|
+
const entries = await readdir(directory).catch((error) => error.code === 'ENOENT' ? [] : Promise.reject(error));
|
|
41
|
+
return entries.length > 0;
|
|
42
|
+
}
|
|
43
|
+
export async function demoStoreMetadata(paths) {
|
|
44
|
+
const contents = await readFile(markerPath(paths), 'utf8').catch((error) => error.code === 'ENOENT' ? null : Promise.reject(error));
|
|
45
|
+
if (!contents)
|
|
46
|
+
return null;
|
|
47
|
+
try {
|
|
48
|
+
const value = JSON.parse(contents);
|
|
49
|
+
if (value.formatVersion !== DEMO_FORMAT_VERSION || value.generated !== true || !Number.isInteger(value.seed) || !Number.isInteger(value.days)
|
|
50
|
+
|| typeof value.startDate !== 'string' || typeof value.endDate !== 'string' || typeof value.createdAt !== 'string')
|
|
51
|
+
return null;
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create generated data only in a fresh directory or one already marked as a
|
|
60
|
+
* Catence demo. A real data directory is never opened for mutation here.
|
|
61
|
+
*/
|
|
62
|
+
export async function createDemoStore(paths, options = {}) {
|
|
63
|
+
const days = options.days ?? 90;
|
|
64
|
+
const seed = options.seed ?? 17;
|
|
65
|
+
const endDate = options.endDate ?? isoDate(new Date());
|
|
66
|
+
if (!Number.isInteger(days) || days < 14 || days > 365)
|
|
67
|
+
throw new Error('Demo days must be an integer between 14 and 365.');
|
|
68
|
+
if (!Number.isInteger(seed) || seed < 0 || seed > 2_147_483_647)
|
|
69
|
+
throw new Error('Demo seed must be an integer between 0 and 2147483647.');
|
|
70
|
+
if (!validDate(endDate))
|
|
71
|
+
throw new Error('Demo endDate must use YYYY-MM-DD.');
|
|
72
|
+
const existing = await demoStoreMetadata(paths);
|
|
73
|
+
if (!existing && await hasDirectoryContents(paths.root)) {
|
|
74
|
+
throw new Error(`Refusing to overwrite ${paths.root}: it is not a Catence demo store. Choose an empty --data-dir.`);
|
|
75
|
+
}
|
|
76
|
+
if (existsSync(paths.database) && existing) {
|
|
77
|
+
return {
|
|
78
|
+
created: false,
|
|
79
|
+
dataDir: paths.root,
|
|
80
|
+
demoStore: existing,
|
|
81
|
+
message: 'Existing generated Catence demo store is ready. Its values are synthetic, not personal measurements.',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const startDate = addDays(endDate, -(days - 1));
|
|
85
|
+
const random = seededRandom(seed);
|
|
86
|
+
await ensurePaths(paths);
|
|
87
|
+
const database = await CatenceDatabase.open(paths);
|
|
88
|
+
try {
|
|
89
|
+
const runId = await database.beginRun('garmin', startDate);
|
|
90
|
+
let previousTrainingLoad = 0;
|
|
91
|
+
for (let offset = 0; offset < days; offset++) {
|
|
92
|
+
const date = addDays(startDate, offset);
|
|
93
|
+
const weeklyLoad = [0, 26, 58, 76, 18, 54, 118][offset % 7];
|
|
94
|
+
const illness = offset >= Math.floor(days * 0.48) && offset < Math.floor(days * 0.48) + 6;
|
|
95
|
+
const recovery = Math.sin(offset / 5) * 0.7 + (random() - 0.5) * 0.45 - (illness ? 1.8 : 0);
|
|
96
|
+
const trainingLoad = Math.max(0, weeklyLoad + (random() - 0.5) * 12 - (illness ? 28 : 0));
|
|
97
|
+
const restingHeartRate = rounded(52 - recovery * 3.8 + previousTrainingLoad * 0.045 + (random() - 0.5) * 1.3);
|
|
98
|
+
const hrv = rounded(53 + recovery * 8.5 - previousTrainingLoad * 0.025 + (random() - 0.5) * 2.5);
|
|
99
|
+
const sleepScore = rounded(76 + recovery * 9 - previousTrainingLoad * 0.025 + (random() - 0.5) * 4);
|
|
100
|
+
const stress = rounded(34 - recovery * 8 + previousTrainingLoad * 0.04 + (random() - 0.5) * 4);
|
|
101
|
+
const bodyBattery = rounded(59 + recovery * 17 - previousTrainingLoad * 0.04 + (random() - 0.5) * 5);
|
|
102
|
+
const sleepIsMissing = offset % 23 === 8 || offset % 31 === 12;
|
|
103
|
+
const wellnessPayload = {
|
|
104
|
+
calendarDate: date,
|
|
105
|
+
restingHeartRate,
|
|
106
|
+
hrvSDNN: hrv,
|
|
107
|
+
sleepTimeSeconds: rounded(7.25 * 3_600 + recovery * 450 + (random() - 0.5) * 420),
|
|
108
|
+
averageStressLevel: stress,
|
|
109
|
+
bodyBattery,
|
|
110
|
+
trainingReadinessScore: rounded(62 + recovery * 16 - previousTrainingLoad * 0.04),
|
|
111
|
+
averageSpO2: rounded(96.4 + recovery * 0.25 + (random() - 0.5) * 0.3, 1),
|
|
112
|
+
totalSteps: rounded(8_300 + (random() - 0.5) * 3_000),
|
|
113
|
+
};
|
|
114
|
+
if (!sleepIsMissing)
|
|
115
|
+
wellnessPayload.sleepScore = sleepScore;
|
|
116
|
+
await importRecord(database, runId, {
|
|
117
|
+
kind: 'source_entity', schemaVersion: 1, provider: 'garmin', entityType: 'daily_health', remoteId: `demo-wellness:${date}`,
|
|
118
|
+
parentRemoteId: null, occurredOn: date, sourceUpdatedAt: null, rawObjectHash: hash(`demo-wellness:${seed}:${date}`), payload: wellnessPayload, extension: { generated: true },
|
|
119
|
+
});
|
|
120
|
+
if (trainingLoad >= 12) {
|
|
121
|
+
await importRecord(database, runId, {
|
|
122
|
+
kind: 'source_entity', schemaVersion: 1, provider: 'garmin', entityType: 'activity', remoteId: `demo-activity:${date}`,
|
|
123
|
+
parentRemoteId: null, occurredOn: date, sourceUpdatedAt: null, rawObjectHash: hash(`demo-activity:${seed}:${date}`), extension: { generated: true },
|
|
124
|
+
payload: {
|
|
125
|
+
activityId: `demo-activity:${date}`,
|
|
126
|
+
startTimeGMT: `${date}T10:00:00Z`,
|
|
127
|
+
activityType: offset % 3 === 0 ? 'running' : 'road_biking',
|
|
128
|
+
activityName: offset % 3 === 0 ? 'Generated endurance run' : 'Generated endurance ride',
|
|
129
|
+
distance: rounded(5_000 + trainingLoad * 360),
|
|
130
|
+
duration: rounded(1_800 + trainingLoad * 18),
|
|
131
|
+
activityTrainingLoad: rounded(trainingLoad, 1),
|
|
132
|
+
averageHR: rounded(132 + trainingLoad * 0.12),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
previousTrainingLoad = trainingLoad;
|
|
137
|
+
}
|
|
138
|
+
await database.finishRun(runId);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
await database.close();
|
|
142
|
+
}
|
|
143
|
+
const metadata = {
|
|
144
|
+
formatVersion: DEMO_FORMAT_VERSION,
|
|
145
|
+
generated: true,
|
|
146
|
+
seed,
|
|
147
|
+
days,
|
|
148
|
+
startDate,
|
|
149
|
+
endDate,
|
|
150
|
+
createdAt: new Date().toISOString(),
|
|
151
|
+
};
|
|
152
|
+
await writeFile(markerPath(paths), `${JSON.stringify(metadata, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
153
|
+
return {
|
|
154
|
+
created: true,
|
|
155
|
+
dataDir: paths.root,
|
|
156
|
+
demoStore: metadata,
|
|
157
|
+
message: 'Generated Catence demo store is ready. Its values are synthetic, not personal measurements.',
|
|
158
|
+
exampleQuestions: [
|
|
159
|
+
'Use wellness_correlate with metricA training_load, metricB resting_hr_bpm, and scanLags true.',
|
|
160
|
+
'Use wellness_anomalies to find the deliberately generated multi-metric recovery disruption.',
|
|
161
|
+
'Use wellness_coverage to inspect intentionally missing sleep-score dates.',
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=demo.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CatencePaths } from '../../contracts/runtime.js';
|
|
2
|
+
type SyncProviderChoice = 'intervals' | 'garmin';
|
|
3
|
+
export type ProviderChoice = SyncProviderChoice | 'strava' | 'all';
|
|
4
|
+
export declare function initializeDataStore(paths: CatencePaths): Promise<Record<string, unknown>>;
|
|
5
|
+
export declare function syncData(paths: CatencePaths, provider: ProviderChoice, explicitFrom?: string, advanceCursor?: boolean, refreshActivities?: boolean, backfill?: boolean): Promise<Record<string, unknown>>;
|
|
6
|
+
export declare function retryDataSync(paths: CatencePaths, previousRunId: string): Promise<Record<string, unknown>>;
|
|
7
|
+
export declare function connectStrava(paths: CatencePaths, code: string | undefined, redirectUri: string): Promise<unknown>;
|
|
8
|
+
/** Validate a local OAuth return address before opening a callback listener. */
|
|
9
|
+
export declare function loopbackStravaRedirect(redirectUri: string): URL;
|
|
10
|
+
/**
|
|
11
|
+
* Complete Strava OAuth through a short-lived loopback callback listener.
|
|
12
|
+
*
|
|
13
|
+
* The authorization URL is emitted through the caller so the CLI can keep
|
|
14
|
+
* stdout machine-readable for the final connection result.
|
|
15
|
+
*/
|
|
16
|
+
export declare function connectStravaWithCallback(paths: CatencePaths, redirectUri: string, onAuthorizationUrl: (url: string) => void, timeoutMs?: number): Promise<unknown>;
|
|
17
|
+
export declare function disconnectStravaAccount(paths: CatencePaths): Promise<{
|
|
18
|
+
provider: 'strava';
|
|
19
|
+
disconnected: true;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function linkActivity(paths: CatencePaths, source: string, activity: string): Promise<Record<string, string>>;
|
|
22
|
+
export declare function unlinkActivity(paths: CatencePaths, source: string): Promise<Record<string, string>>;
|
|
23
|
+
export declare function dataStatus(paths: CatencePaths): Promise<Record<string, unknown>>;
|
|
24
|
+
export declare function rebuildRetrievalIndex(paths: CatencePaths): Promise<Record<string, unknown>>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { createServer } from 'node:http';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { buildRetrievalIndex } from '../../core/retrieval/index.js';
|
|
10
|
+
import { defaultFromDate, ensurePaths, requireIntervalsConfig } from '../../core/runtime/configuration.js';
|
|
11
|
+
import { importJsonl } from '../ingestion/importer.js';
|
|
12
|
+
import { IntervalsExtractor } from '../ingestion/providers/intervals.js';
|
|
13
|
+
import { completeStravaAuthorization, disconnectStrava, getStravaAuthorizationUrl, syncStravaGear } from '../ingestion/providers/strava/service.js';
|
|
14
|
+
import { setManualActivityLink, unlinkActivitySource } from '../normalization/activities/linking.js';
|
|
15
|
+
import { CatenceDatabase } from '../storage/database.js';
|
|
16
|
+
import { withDataWriteLock } from '../storage/write-lock.js';
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
function packageRoot() {
|
|
19
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
20
|
+
}
|
|
21
|
+
async function syncProvider(database, paths, provider, options) {
|
|
22
|
+
const explicitWindow = options.explicitFrom ? { fromDate: options.explicitFrom, toDate: options.toDate, source: 'explicit' } : null;
|
|
23
|
+
const uncoveredExplicitWindow = async (cursorName) => {
|
|
24
|
+
if (!explicitWindow)
|
|
25
|
+
return null;
|
|
26
|
+
if (!options.backfill)
|
|
27
|
+
return explicitWindow;
|
|
28
|
+
const window = await database.resolveBackfillWindow(provider, cursorName, explicitWindow.fromDate, explicitWindow.toDate, options.refreshActivities);
|
|
29
|
+
return window ? { ...window, source: 'explicit' } : null;
|
|
30
|
+
};
|
|
31
|
+
const dailyWindow = explicitWindow ? await uncoveredExplicitWindow('daily') : await database.resolveIncrementalWindow(provider, 'daily', options.toDate, defaultFromDate(), 3);
|
|
32
|
+
const activityWindow = explicitWindow ? await uncoveredExplicitWindow('activities') : await database.resolveIncrementalWindow(provider, 'activities', options.toDate, defaultFromDate(), 14);
|
|
33
|
+
const hasProviderWork = provider === 'intervals' ? activityWindow !== null : dailyWindow !== null || activityWindow !== null;
|
|
34
|
+
if (!hasProviderWork) {
|
|
35
|
+
return {
|
|
36
|
+
runId: null, provider, fromDate: null,
|
|
37
|
+
dailyFromDate: dailyWindow?.fromDate ?? null, dailyToDate: dailyWindow?.toDate ?? null,
|
|
38
|
+
activityFromDate: activityWindow?.fromDate ?? null, activityToDate: activityWindow?.toDate ?? null,
|
|
39
|
+
toDate: options.toDate, dailyCursorSource: dailyWindow?.source ?? null, activityCursorSource: activityWindow?.source ?? null,
|
|
40
|
+
refreshedActivities: provider === 'garmin' && options.refreshActivities, skipped: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const fromDate = [dailyWindow?.fromDate, activityWindow?.fromDate].filter((date) => Boolean(date)).sort()[0];
|
|
44
|
+
const runId = await database.beginRun(provider, fromDate);
|
|
45
|
+
let extractionStarted = false;
|
|
46
|
+
try {
|
|
47
|
+
if (provider === 'intervals') {
|
|
48
|
+
const config = requireIntervalsConfig();
|
|
49
|
+
extractionStarted = true;
|
|
50
|
+
await new IntervalsExtractor(database, paths, config.apiKey, config.athleteId).sync(runId, activityWindow, !options.backfill || options.refreshActivities);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const output = path.join(paths.staging, 'garmin', `${runId}.jsonl`);
|
|
54
|
+
const knownActivities = path.join(paths.staging, 'garmin', `${runId}.known-activities.json`);
|
|
55
|
+
await mkdir(path.dirname(knownActivities), { recursive: true });
|
|
56
|
+
// An explicit refresh intentionally provides no known hashes, causing
|
|
57
|
+
// the staging worker to re-fetch Garmin activity details, files, and
|
|
58
|
+
// streams even when the list-summary hash is unchanged.
|
|
59
|
+
await writeFile(knownActivities, JSON.stringify(options.refreshActivities ? {} : await database.knownActivitySummaryHashes('garmin')));
|
|
60
|
+
extractionStarted = true;
|
|
61
|
+
const arguments_ = ['run', 'python', '-m', 'python.catence.providers.garmin.cli', '--from', fromDate, '--to', options.toDate, '--data-dir', paths.root, '--output', output, '--run-id', runId, '--known-activities', knownActivities];
|
|
62
|
+
if (dailyWindow)
|
|
63
|
+
arguments_.push('--daily-from', dailyWindow.fromDate, '--daily-to', dailyWindow.toDate);
|
|
64
|
+
else
|
|
65
|
+
arguments_.push('--skip-daily');
|
|
66
|
+
if (activityWindow)
|
|
67
|
+
arguments_.push('--activity-from', activityWindow.fromDate, '--activity-to', activityWindow.toDate);
|
|
68
|
+
else
|
|
69
|
+
arguments_.push('--skip-activities');
|
|
70
|
+
if (options.backfill && !options.refreshActivities)
|
|
71
|
+
arguments_.push('--historical-only');
|
|
72
|
+
await execFileAsync('uv', arguments_, {
|
|
73
|
+
cwd: packageRoot(),
|
|
74
|
+
env: { ...process.env, UV_PROJECT_ENVIRONMENT: process.env.UV_PROJECT_ENVIRONMENT ?? path.join(paths.root, 'python-venv') },
|
|
75
|
+
});
|
|
76
|
+
if (!existsSync(output))
|
|
77
|
+
throw new Error('Garmin staging worker completed without writing a JSONL manifest.');
|
|
78
|
+
await importJsonl(database, runId, output);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
await database.addError(runId, provider, 'sync', null, error instanceof Error ? error.message : String(error), true);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await database.finishRun(runId);
|
|
86
|
+
}
|
|
87
|
+
if (options.advanceCursor && !options.explicitFrom && extractionStarted) {
|
|
88
|
+
await database.advanceIncrementalCursor(provider, 'daily', runId, options.toDate, 3);
|
|
89
|
+
await database.advanceIncrementalCursor(provider, 'activities', runId, options.toDate, 14);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
runId, provider, fromDate,
|
|
93
|
+
dailyFromDate: dailyWindow?.fromDate ?? null, dailyToDate: dailyWindow?.toDate ?? null,
|
|
94
|
+
activityFromDate: activityWindow?.fromDate ?? null, activityToDate: activityWindow?.toDate ?? null,
|
|
95
|
+
toDate: options.toDate, dailyCursorSource: dailyWindow?.source ?? null, activityCursorSource: activityWindow?.source ?? null,
|
|
96
|
+
refreshedActivities: provider === 'garmin' && options.refreshActivities, skipped: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export async function initializeDataStore(paths) {
|
|
100
|
+
await ensurePaths(paths);
|
|
101
|
+
const database = await CatenceDatabase.open(paths);
|
|
102
|
+
await database.close();
|
|
103
|
+
return { dataDir: paths.root, database: paths.database, message: 'Catence data store is ready. Run sync to import provider data.' };
|
|
104
|
+
}
|
|
105
|
+
export async function syncData(paths, provider, explicitFrom, advanceCursor = true, refreshActivities = false, backfill = false) {
|
|
106
|
+
await ensurePaths(paths);
|
|
107
|
+
if (provider === 'strava')
|
|
108
|
+
return { provider, result: await syncStravaGear(paths) };
|
|
109
|
+
const database = await CatenceDatabase.open(paths);
|
|
110
|
+
try {
|
|
111
|
+
const providers = provider === 'all' ? ['intervals', 'garmin'] : [provider];
|
|
112
|
+
const toDate = new Date().toISOString().slice(0, 10);
|
|
113
|
+
const runs = [];
|
|
114
|
+
for (const source of providers)
|
|
115
|
+
runs.push(await syncProvider(database, paths, source, { explicitFrom, toDate, advanceCursor, refreshActivities, backfill }));
|
|
116
|
+
return { runIds: runs.flatMap((run) => run.runId ? [run.runId] : []), runs };
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await database.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export async function retryDataSync(paths, previousRunId) {
|
|
123
|
+
await ensurePaths(paths);
|
|
124
|
+
const database = await CatenceDatabase.open(paths);
|
|
125
|
+
try {
|
|
126
|
+
const previous = await database.getRun(previousRunId);
|
|
127
|
+
if (!previous)
|
|
128
|
+
throw new Error(`No sync run found for ${previousRunId}.`);
|
|
129
|
+
if (previous.provider === 'strava')
|
|
130
|
+
throw new Error('Retry Strava gear sync with `catence-data sync --provider strava`.');
|
|
131
|
+
const run = await syncProvider(database, paths, previous.provider, { explicitFrom: previous.from_date, toDate: new Date().toISOString().slice(0, 10), advanceCursor: false, refreshActivities: false, backfill: false });
|
|
132
|
+
return { retriedRun: previousRunId, runId: run.runId, run };
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await database.close();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export async function connectStrava(paths, code, redirectUri) {
|
|
139
|
+
return code ? completeStravaAuthorization(paths, code, redirectUri) : getStravaAuthorizationUrl(paths, redirectUri);
|
|
140
|
+
}
|
|
141
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
142
|
+
/** Validate a local OAuth return address before opening a callback listener. */
|
|
143
|
+
export function loopbackStravaRedirect(redirectUri) {
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = new URL(redirectUri);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
throw new Error('--redirect-uri must be an absolute URL.');
|
|
150
|
+
}
|
|
151
|
+
if (parsed.protocol !== 'http:' || !LOOPBACK_HOSTS.has(parsed.hostname) || !parsed.port) {
|
|
152
|
+
throw new Error('--callback requires an http loopback --redirect-uri with an explicit port.');
|
|
153
|
+
}
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
156
|
+
function readStravaCallback(requestUrl, callback) {
|
|
157
|
+
const received = new URL(requestUrl ?? '/', callback);
|
|
158
|
+
if (received.pathname !== callback.pathname)
|
|
159
|
+
throw new Error('Strava returned to an unexpected callback path.');
|
|
160
|
+
const error = received.searchParams.get('error');
|
|
161
|
+
if (error)
|
|
162
|
+
throw new Error(`Strava authorization was declined or failed: ${error}.`);
|
|
163
|
+
const code = received.searchParams.get('code');
|
|
164
|
+
if (!code)
|
|
165
|
+
throw new Error('Strava callback did not include an authorization code.');
|
|
166
|
+
return { code, state: received.searchParams.get('state') };
|
|
167
|
+
}
|
|
168
|
+
function callbackPage(response, success) {
|
|
169
|
+
response.writeHead(success ? 200 : 400, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
170
|
+
response.end(success
|
|
171
|
+
? '<!doctype html><title>Catence connected</title><p>Strava is connected to Catence. You can close this tab.</p>'
|
|
172
|
+
: '<!doctype html><title>Catence authorization failed</title><p>Strava authorization did not complete. Return to the terminal for details.</p>');
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Complete Strava OAuth through a short-lived loopback callback listener.
|
|
176
|
+
*
|
|
177
|
+
* The authorization URL is emitted through the caller so the CLI can keep
|
|
178
|
+
* stdout machine-readable for the final connection result.
|
|
179
|
+
*/
|
|
180
|
+
export async function connectStravaWithCallback(paths, redirectUri, onAuthorizationUrl, timeoutMs = 300_000) {
|
|
181
|
+
const callback = loopbackStravaRedirect(redirectUri);
|
|
182
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1_000)
|
|
183
|
+
throw new Error('OAuth callback timeout must be at least one second.');
|
|
184
|
+
const expectedState = randomUUID();
|
|
185
|
+
const listener = createServer();
|
|
186
|
+
const callbackResult = new Promise((resolve, reject) => {
|
|
187
|
+
listener.once('error', reject);
|
|
188
|
+
listener.on('request', (request, response) => {
|
|
189
|
+
if (request.method !== 'GET') {
|
|
190
|
+
response.writeHead(405, { allow: 'GET' }).end();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const result = readStravaCallback(request.url, callback);
|
|
195
|
+
if (result.state !== expectedState)
|
|
196
|
+
throw new Error('Strava callback state did not match this authorization request.');
|
|
197
|
+
callbackPage(response, true);
|
|
198
|
+
resolve(result);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
callbackPage(response, false);
|
|
202
|
+
reject(error);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
await new Promise((resolve, reject) => {
|
|
208
|
+
listener.once('error', reject);
|
|
209
|
+
listener.listen(Number(callback.port), callback.hostname.replace(/^\[|\]$/g, ''), () => {
|
|
210
|
+
listener.off('error', reject);
|
|
211
|
+
resolve();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
const authorization = await getStravaAuthorizationUrl(paths, callback.toString());
|
|
215
|
+
if (!authorization.authorizationUrl)
|
|
216
|
+
throw new Error('Strava did not return an authorization URL.');
|
|
217
|
+
const authorizationUrl = new URL(authorization.authorizationUrl);
|
|
218
|
+
authorizationUrl.searchParams.set('state', expectedState);
|
|
219
|
+
onAuthorizationUrl(authorizationUrl.toString());
|
|
220
|
+
const timed = Promise.race([
|
|
221
|
+
callbackResult,
|
|
222
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timed out waiting for the Strava callback.')), timeoutMs)),
|
|
223
|
+
]);
|
|
224
|
+
const { code } = await timed;
|
|
225
|
+
return completeStravaAuthorization(paths, code, callback.toString());
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
if (listener.listening)
|
|
229
|
+
await new Promise((resolve) => listener.close(() => resolve()));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export async function disconnectStravaAccount(paths) {
|
|
233
|
+
await disconnectStrava(paths);
|
|
234
|
+
return { provider: 'strava', disconnected: true };
|
|
235
|
+
}
|
|
236
|
+
export async function linkActivity(paths, source, activity) {
|
|
237
|
+
await ensurePaths(paths);
|
|
238
|
+
await withDataWriteLock(paths, async () => {
|
|
239
|
+
const database = await CatenceDatabase.open(paths);
|
|
240
|
+
try {
|
|
241
|
+
await setManualActivityLink(database, source, activity);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
await database.close();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
return { source, activity, method: 'manual' };
|
|
248
|
+
}
|
|
249
|
+
export async function unlinkActivity(paths, source) {
|
|
250
|
+
await ensurePaths(paths);
|
|
251
|
+
await withDataWriteLock(paths, async () => {
|
|
252
|
+
const database = await CatenceDatabase.open(paths);
|
|
253
|
+
try {
|
|
254
|
+
await unlinkActivitySource(database, source);
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
await database.close();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
return { source, method: 'source' };
|
|
261
|
+
}
|
|
262
|
+
export async function dataStatus(paths) {
|
|
263
|
+
await ensurePaths(paths);
|
|
264
|
+
const database = await CatenceDatabase.open(paths);
|
|
265
|
+
try {
|
|
266
|
+
return await database.status();
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
await database.close();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
export async function rebuildRetrievalIndex(paths) {
|
|
273
|
+
if (!existsSync(paths.database))
|
|
274
|
+
throw new Error('No Catence database exists yet. Complete a sync before building the retrieval index.');
|
|
275
|
+
const database = await CatenceDatabase.open(paths);
|
|
276
|
+
try {
|
|
277
|
+
return await buildRetrievalIndex(database);
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
await database.close();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=management.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CatenceDatabase } from '../storage/database.js';
|
|
2
|
+
import { type StagingRecord } from '../../contracts/staging.js';
|
|
3
|
+
export declare function importRecord(database: CatenceDatabase, runId: string, record: StagingRecord): Promise<void>;
|
|
4
|
+
export declare function importJsonl(database: CatenceDatabase, runId: string, filePath: string): Promise<number>;
|
|
5
|
+
export declare function importSingleStagingFile(database: CatenceDatabase, runId: string, filePath: string): Promise<number>;
|
|
6
|
+
export declare function queueWorkItem(database: CatenceDatabase, runId: string, provider: 'intervals' | 'garmin' | 'strava', endpoint: string, remoteId: string | null, scope: unknown): Promise<string>;
|
|
7
|
+
export declare function completeWorkItem(database: CatenceDatabase, itemId: string): Promise<void>;
|
|
8
|
+
export declare function failWorkItem(database: CatenceDatabase, itemId: string, message: string): Promise<void>;
|