catence 0.2.0-beta.23 → 0.2.0-beta.25

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.
@@ -3,7 +3,7 @@
3
3
  * Keep these values synchronized with release/manifest.json; release checks
4
4
  * deliberately reject drift before a package can be published.
5
5
  */
6
- export declare const CATENCE_RUNTIME_VERSION = "0.2.0-beta.23";
6
+ export declare const CATENCE_RUNTIME_VERSION = "0.2.0-beta.25";
7
7
  export declare const CATENCE_PROTOCOL_VERSION = 1;
8
8
  export type CatenceRuntimeCapabilities = {
9
9
  mcp: true;
@@ -3,7 +3,7 @@
3
3
  * Keep these values synchronized with release/manifest.json; release checks
4
4
  * deliberately reject drift before a package can be published.
5
5
  */
6
- export const CATENCE_RUNTIME_VERSION = '0.2.0-beta.23';
6
+ export const CATENCE_RUNTIME_VERSION = '0.2.0-beta.25';
7
7
  export const CATENCE_PROTOCOL_VERSION = 1;
8
8
  export function catenceRuntimeHealth() {
9
9
  return {
@@ -1,6 +1,8 @@
1
1
  import { ReadOnlyRepository } from './repository.js';
2
2
  export type FindActivitiesRequest = {
3
3
  sports?: string[];
4
+ /** Accepted as a convenience alias for `sports: [sport]`. */
5
+ sport?: string;
4
6
  distanceKm?: [number, number];
5
7
  nameContains?: string[];
6
8
  startDate?: string;
@@ -49,7 +49,7 @@ export class ActivityDiscoveryService {
49
49
  this.repository = repository;
50
50
  }
51
51
  async findActivities(request = {}) {
52
- const sports = [...new Set((request.sports ?? []).map((sport) => sport.trim()).filter(Boolean))];
52
+ const sports = [...new Set((request.sports ?? (request.sport ? [request.sport] : [])).map((sport) => sport.trim()).filter(Boolean))];
53
53
  const names = [...new Set((request.nameContains ?? []).map((name) => name.trim()).filter(Boolean))];
54
54
  if (sports.length > 12 || names.length > 12)
55
55
  throw new QueryValidationError('sports and nameContains each permit at most 12 values.');
@@ -25,7 +25,7 @@ export const DATASET_CATALOG = {
25
25
  columns: [column('activity_source_id', 'VARCHAR', { filterable: true, groupable: true }), column('activity_id', 'VARCHAR', { filterable: true, groupable: true }), column('provider', 'VARCHAR', { filterable: true, groupable: true }), column('started_at_utc', 'TIMESTAMPTZ', { filterable: true }), column('sport', 'VARCHAR', { filterable: true, groupable: true }), column('interval_key', 'VARCHAR', { filterable: true }), column('label', 'VARCHAR', { filterable: true, groupable: true }), column('source_type', 'VARCHAR', { filterable: true, groupable: true }), column('start_s', 'DOUBLE', { unit: 's', metric: true }), column('end_s', 'DOUBLE', { unit: 's', metric: true }), column('duration_s', 'DOUBLE', { unit: 's', metric: true }), column('moving_s', 'DOUBLE', { unit: 's', metric: true }), column('distance_m', 'DOUBLE', { unit: 'm', metric: true }), column('avg_power', 'DOUBLE', { unit: 'W', metric: true }), column('avg_hr', 'DOUBLE', { unit: 'bpm', metric: true }), column('avg_pace', 'DOUBLE', { metric: true }), column('intensity', 'DOUBLE', { metric: true })],
26
26
  },
27
27
  swim_lengths: {
28
- name: 'swim_lengths', relation: 'swim_length_facts', description: 'Explicit provider-supplied swim-length facts. An empty result means lengths were not supplied; Catence does not infer them from activity samples.', dateColumn: 'started_at_utc', provenanceColumns: ['activity_source_id', 'source', 'confidence', 'raw_object_hash'], permittedGroupings: ['provider', 'sport', 'source', 'confidence', 'is_rest'],
28
+ name: 'swim_lengths', relation: 'swim_length_facts', description: 'Explicit provider-supplied swim-length facts. An empty result means lengths were not supplied; Catence does not infer them from activity samples. Per-length swolf may be null (not supplied by all providers); derive as duration_s + stroke_count. Per-length distance is typically null in Garmin FIT; use pool_length_m to compute pace (duration_s / pool_length_m * 100 for s/100m).', dateColumn: 'started_at_utc', provenanceColumns: ['activity_source_id', 'source', 'confidence', 'raw_object_hash'], permittedGroupings: ['provider', 'sport', 'source', 'confidence', 'is_rest'],
29
29
  columns: [column('activity_source_id', 'VARCHAR', { filterable: true, groupable: true }), column('activity_id', 'VARCHAR', { filterable: true, groupable: true }), column('provider', 'VARCHAR', { filterable: true, groupable: true }), column('started_at_utc', 'TIMESTAMPTZ', { filterable: true }), column('sport', 'VARCHAR', { filterable: true, groupable: true }), column('source', 'VARCHAR', { filterable: true, groupable: true }), column('confidence', 'VARCHAR', { filterable: true, groupable: true }), column('length_index', 'INTEGER', { filterable: true, groupable: true }), column('lap_index', 'INTEGER', { filterable: true, groupable: true }), column('pool_length_m', 'DOUBLE', { unit: 'm', metric: true }), column('start_time', 'TIMESTAMPTZ', { filterable: true }), column('duration_s', 'DOUBLE', { unit: 's', metric: true }), column('active_duration_s', 'DOUBLE', { unit: 's', metric: true }), column('distance_m', 'DOUBLE', { unit: 'm', metric: true }), column('stroke_count', 'DOUBLE', { unit: 'count', metric: true }), column('stroke_rate', 'DOUBLE', { unit: 'strokes/min', metric: true }), column('swolf', 'DOUBLE', { metric: true }), column('avg_hr', 'DOUBLE', { unit: 'bpm', metric: true }), column('max_hr', 'DOUBLE', { unit: 'bpm', metric: true }), column('is_rest', 'BOOLEAN', { filterable: true, groupable: true }), column('raw_object_hash', 'VARCHAR')],
30
30
  },
31
31
  swim_sets: {
@@ -86,7 +86,7 @@ export class SwimmingService {
86
86
  availability: {
87
87
  explicitLengthFacts: lengths.length > 0,
88
88
  setFacts: sets.length > 0,
89
- perLengthPaceAvailable: lengths.some((length) => Number(length.duration_s) > 0 && Number(length.distance_m) > 0),
89
+ perLengthPaceAvailable: lengths.some((length) => Number(length.duration_s) > 0),
90
90
  },
91
91
  };
92
92
  }));
@@ -123,7 +123,7 @@ export class SwimmingService {
123
123
  FROM activity_quality_flags GROUP BY activity_source_id
124
124
  ), lengths AS (
125
125
  SELECT activity_source_id, count(*) AS length_fact_count,
126
- count(*) FILTER (WHERE duration_s > 0 AND distance_m > 0) AS pace_length_fact_count
126
+ count(*) FILTER (WHERE duration_s > 0) AS pace_length_fact_count
127
127
  FROM swim_lengths GROUP BY activity_source_id
128
128
  ), sets AS (
129
129
  SELECT activity_source_id, count(*) AS set_fact_count FROM swim_sets GROUP BY activity_source_id
@@ -32,6 +32,7 @@ const MCP_INSTRUCTIONS = [
32
32
  'When summary metric is NULL (e.g. avg_power), fall through to FIT-derived/samples layer before declaring absence.',
33
33
  'Before reusing any prior course/elevation profile, resolve event\'s courseId and diff it against past activity\'s course; different courseId => different profile.',
34
34
  'If a needed tool is not visible in your current tool list (the host may truncate it to a subset), read the catence://tools resource to discover all available tools before concluding a capability is absent.',
35
+ 'For swim analysis: use find_activities (sports: ["lap_swimming"] or sport: "lap_swimming") to list pool sessions. Use get_swim_laps for per-length data (pace from duration_s/pool_length_m, SPL from stroke_count, SWOLF from duration_s+stroke_count). Use swim_progress_report for session-level SWOLF, fastest-100, and stroke cadence. Per-length SWOLF is derived when the provider does not supply it. Per-length distance is typically null in Garmin FIT; use pool_length_m for pace calculations.',
35
36
  ].join(' ');
36
37
  function textResult(value) {
37
38
  return { content: [{ type: 'text', text: JSON.stringify(jsonSafe(value), null, 2) }] };
@@ -162,10 +163,29 @@ export function createCatenceMcpServer(paths = resolveCatalogPaths(), dependenci
162
163
  const originalRegister = server.registerTool.bind(server);
163
164
  server.registerTool = (name, configuration, handler) => {
164
165
  const originalInput = configuration.inputSchema;
166
+ let mergedInputSchema;
167
+ if (originalInput) {
168
+ const hasV4 = '_zod' in originalInput;
169
+ const hasV3 = '_def' in originalInput;
170
+ if (hasV4 || hasV3) {
171
+ // originalInput is already a Zod object schema; extract its shape and
172
+ // merge athleteId into a new object of the same Zod version.
173
+ const rawShape = originalInput.shape;
174
+ const shape = typeof rawShape === 'object' && rawShape !== null ? rawShape : {};
175
+ mergedInputSchema = z.object({ ...shape, athleteId: athleteIdSchema });
176
+ }
177
+ else {
178
+ // originalInput is a raw shape (plain object of Zod schemas).
179
+ mergedInputSchema = { athleteId: athleteIdSchema, ...originalInput };
180
+ }
181
+ }
182
+ else {
183
+ mergedInputSchema = { athleteId: athleteIdSchema };
184
+ }
165
185
  return originalRegister(name, {
166
186
  ...configuration,
167
187
  description: `${String(configuration.description ?? '')} Requires athleteId from list_athletes.`,
168
- inputSchema: { athleteId: athleteIdSchema, ...(originalInput ?? {}) },
188
+ inputSchema: mergedInputSchema,
169
189
  }, handler);
170
190
  };
171
191
  }
@@ -460,6 +480,7 @@ export function createCatenceMcpServer(paths = resolveCatalogPaths(), dependenci
460
480
  title: 'Find activities and likely races',
461
481
  description: 'Find canonical activities by sport, distance, name, and date. Results are paginated and transparently flag likely-race signals without claiming provider-confirmed race metadata. Read-only.',
462
482
  inputSchema: {
483
+ sport: z.string().min(1).optional(),
463
484
  sports: z.array(z.string().min(1)).min(1).max(12).optional(),
464
485
  distanceKm: z.tuple([z.number().min(0), z.number().min(0)]).optional(),
465
486
  nameContains: z.array(z.string().min(1)).min(1).max(12).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catence",
3
- "version": "0.2.0-beta.23",
3
+ "version": "0.2.0-beta.25",
4
4
  "description": "Local MCP access to normalized Garmin, Intervals.icu, and targeted Strava fitness data.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "repository": {