Skip to main content
Glama
historical.ts6.42 kB
import { IntervalsClient } from '../clients/intervals.js'; import { WhoopClient } from '../clients/whoop.js'; import { parseDateString, getToday } from '../utils/date-parser.js'; import type { NormalizedWorkout, RecoveryData, FitnessMetrics, PowerCurve, PaceCurve, TrainingLoadTrends, } from '../types/index.js'; import type { GetWorkoutHistoryInput, GetRecoveryTrendsInput, GetFitnessProgressionInput, } from './types.js'; export class HistoricalTools { constructor( private intervals: IntervalsClient, private whoop: WhoopClient | null ) {} /** * Get workout history with flexible date ranges */ async getWorkoutHistory( params: GetWorkoutHistoryInput ): Promise<NormalizedWorkout[]> { const startDate = parseDateString(params.start_date); const endDate = params.end_date ? parseDateString(params.end_date) : getToday(); try { return await this.intervals.getActivities(startDate, endDate, params.sport); } catch (error) { console.error('Error fetching workout history:', error); throw error; } } /** * Get recovery trends over time */ async getRecoveryTrends( params: GetRecoveryTrendsInput ): Promise<{ data: RecoveryData[]; summary: { avg_recovery: number; avg_hrv: number; avg_sleep_hours: number; min_recovery: number; max_recovery: number; }; }> { if (!this.whoop) { return { data: [], summary: { avg_recovery: 0, avg_hrv: 0, avg_sleep_hours: 0, min_recovery: 0, max_recovery: 0, }, }; } const startDate = parseDateString(params.start_date); const endDate = params.end_date ? parseDateString(params.end_date) : getToday(); try { const data = await this.whoop.getRecoveries(startDate, endDate); // Calculate summary statistics const summary = this.calculateRecoverySummary(data); return { data, summary }; } catch (error) { console.error('Error fetching recovery trends:', error); throw error; } } /** * Get fitness progression (CTL/ATL/TSB) over time */ async getFitnessProgression( params: GetFitnessProgressionInput ): Promise<{ data: FitnessMetrics[]; summary: { start_ctl: number; end_ctl: number; ctl_change: number; peak_ctl: number; peak_ctl_date: string; avg_tsb: number; }; }> { const startDate = parseDateString(params.start_date); const endDate = params.end_date ? parseDateString(params.end_date) : getToday(); try { const data = await this.intervals.getFitnessMetrics(startDate, endDate); // Calculate summary statistics const summary = this.calculateFitnessSummary(data); return { data, summary }; } catch (error) { console.error('Error fetching fitness progression:', error); throw error; } } private calculateRecoverySummary(data: RecoveryData[]): { avg_recovery: number; avg_hrv: number; avg_sleep_hours: number; min_recovery: number; max_recovery: number; } { if (data.length === 0) { return { avg_recovery: 0, avg_hrv: 0, avg_sleep_hours: 0, min_recovery: 0, max_recovery: 0, }; } const recoveryScores = data.map((d) => d.recovery_score); const hrvValues = data.map((d) => d.hrv_rmssd); const sleepHours = data.map((d) => d.sleep_duration_hours); return { avg_recovery: this.average(recoveryScores), avg_hrv: this.average(hrvValues), avg_sleep_hours: this.average(sleepHours), min_recovery: Math.min(...recoveryScores), max_recovery: Math.max(...recoveryScores), }; } private calculateFitnessSummary(data: FitnessMetrics[]): { start_ctl: number; end_ctl: number; ctl_change: number; peak_ctl: number; peak_ctl_date: string; avg_tsb: number; } { if (data.length === 0) { return { start_ctl: 0, end_ctl: 0, ctl_change: 0, peak_ctl: 0, peak_ctl_date: '', avg_tsb: 0, }; } const sortedData = [...data].sort( (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() ); const ctlValues = sortedData.map((d) => d.ctl); const tsbValues = sortedData.map((d) => d.tsb); const peakCtl = Math.max(...ctlValues); const peakCtlEntry = sortedData.find((d) => d.ctl === peakCtl); return { start_ctl: sortedData[0].ctl, end_ctl: sortedData[sortedData.length - 1].ctl, ctl_change: sortedData[sortedData.length - 1].ctl - sortedData[0].ctl, peak_ctl: peakCtl, peak_ctl_date: peakCtlEntry?.date ?? '', avg_tsb: this.average(tsbValues), }; } private average(values: number[]): number { if (values.length === 0) return 0; return Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10; } // ============================================ // Power Curves // ============================================ /** * Get power curve showing best efforts at various durations */ async getPowerCurve( sport: string = 'Ride', period: string = '90d' ): Promise<PowerCurve> { try { return await this.intervals.getPowerCurve(sport, period); } catch (error) { console.error('Error fetching power curve:', error); throw error; } } // ============================================ // Pace Curves // ============================================ /** * Get pace curve showing best paces at various durations */ async getPaceCurve( period: string = '90d', gap: boolean = false ): Promise<PaceCurve> { try { return await this.intervals.getPaceCurve(period, gap); } catch (error) { console.error('Error fetching pace curve:', error); throw error; } } // ============================================ // Training Load Trends // ============================================ /** * Get training load trends (CTL/ATL/TSB) with ACWR analysis */ async getTrainingLoadTrends(days: number = 42): Promise<TrainingLoadTrends> { try { return await this.intervals.getTrainingLoadTrends(days); } catch (error) { console.error('Error fetching training load trends:', error); throw error; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gesteves/domestique'

If you have feedback or need assistance with the MCP directory API, please join our Discord server