@robinthues/rt-claude-coach 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.
Files changed (64) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +98 -0
  3. package/bin/claude-coach.js +10 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +530 -0
  6. package/dist/db/client.d.ts +8 -0
  7. package/dist/db/client.js +111 -0
  8. package/dist/db/migrate.d.ts +2 -0
  9. package/dist/db/migrate.js +30 -0
  10. package/dist/db/schema.sql +108 -0
  11. package/dist/garmin/context.d.ts +31 -0
  12. package/dist/garmin/context.js +25 -0
  13. package/dist/garmin/convert.d.ts +28 -0
  14. package/dist/garmin/convert.js +117 -0
  15. package/dist/garmin/dto.d.ts +132 -0
  16. package/dist/garmin/dto.js +35 -0
  17. package/dist/garmin/index.d.ts +3 -0
  18. package/dist/garmin/index.js +1 -0
  19. package/dist/garmin/pace.d.ts +6 -0
  20. package/dist/garmin/pace.js +23 -0
  21. package/dist/garmin/steps.d.ts +12 -0
  22. package/dist/garmin/steps.js +147 -0
  23. package/dist/garmin/targets.d.ts +15 -0
  24. package/dist/garmin/targets.js +85 -0
  25. package/dist/lib/config.d.ts +27 -0
  26. package/dist/lib/config.js +86 -0
  27. package/dist/lib/logging.d.ts +13 -0
  28. package/dist/lib/logging.js +28 -0
  29. package/dist/schema/training-plan.d.ts +288 -0
  30. package/dist/schema/training-plan.js +88 -0
  31. package/dist/strava/api.d.ts +23 -0
  32. package/dist/strava/api.js +88 -0
  33. package/dist/strava/details.d.ts +11 -0
  34. package/dist/strava/details.js +50 -0
  35. package/dist/strava/oauth.d.ts +4 -0
  36. package/dist/strava/oauth.js +113 -0
  37. package/dist/strava/rate-limit.d.ts +9 -0
  38. package/dist/strava/rate-limit.js +16 -0
  39. package/dist/strava/store.d.ts +13 -0
  40. package/dist/strava/store.js +107 -0
  41. package/dist/strava/types.d.ts +49 -0
  42. package/dist/strava/types.js +1 -0
  43. package/dist/viewer/lib/export/erg.d.ts +26 -0
  44. package/dist/viewer/lib/export/erg.js +208 -0
  45. package/dist/viewer/lib/export/fit.d.ts +25 -0
  46. package/dist/viewer/lib/export/fit.js +308 -0
  47. package/dist/viewer/lib/export/ics.d.ts +13 -0
  48. package/dist/viewer/lib/export/ics.js +142 -0
  49. package/dist/viewer/lib/export/index.d.ts +50 -0
  50. package/dist/viewer/lib/export/index.js +229 -0
  51. package/dist/viewer/lib/export/zwo.d.ts +21 -0
  52. package/dist/viewer/lib/export/zwo.js +233 -0
  53. package/dist/viewer/lib/utils.d.ts +14 -0
  54. package/dist/viewer/lib/utils.js +125 -0
  55. package/dist/viewer/main.d.ts +5 -0
  56. package/dist/viewer/main.js +6 -0
  57. package/dist/viewer/stores/changes.d.ts +21 -0
  58. package/dist/viewer/stores/changes.js +49 -0
  59. package/dist/viewer/stores/plan.d.ts +4 -0
  60. package/dist/viewer/stores/plan.js +19 -0
  61. package/dist/viewer/stores/settings.d.ts +53 -0
  62. package/dist/viewer/stores/settings.js +215 -0
  63. package/package.json +67 -0
  64. package/templates/plan-viewer.html +70 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Build Garmin workout steps from a Claude Coach workout.
3
+ *
4
+ * Structured workouts map warmup/main/cooldown steps to ExecutableStepDTO and
5
+ * interval sets to RepeatGroupDTO. Simple workouts synthesize warmup(~10%) /
6
+ * main(~80%) / cooldown(~10%), matching the old FIT exporter heuristic.
7
+ */
8
+ import { STEP_TYPES, END_CONDITIONS, TARGET_TYPES, } from "./dto.js";
9
+ import { resolveStepTarget, resolveWorkoutTarget } from "./targets.js";
10
+ function stepTypeFor(type) {
11
+ switch (type) {
12
+ case "warmup":
13
+ return STEP_TYPES.warmup;
14
+ case "cooldown":
15
+ return STEP_TYPES.cooldown;
16
+ case "recovery":
17
+ return STEP_TYPES.recovery;
18
+ case "rest":
19
+ return STEP_TYPES.rest;
20
+ case "work":
21
+ case "interval_set":
22
+ default:
23
+ return STEP_TYPES.interval;
24
+ }
25
+ }
26
+ /** Map a DurationTarget to a Garmin end condition + value (seconds or meters). */
27
+ function endConditionFor(duration) {
28
+ const unit = duration?.unit ?? "minutes";
29
+ const value = duration?.value ?? 0;
30
+ switch (unit) {
31
+ case "seconds":
32
+ return { endCondition: END_CONDITIONS.time, endConditionValue: value };
33
+ case "minutes":
34
+ return { endCondition: END_CONDITIONS.time, endConditionValue: value * 60 };
35
+ case "hours":
36
+ return { endCondition: END_CONDITIONS.time, endConditionValue: value * 3600 };
37
+ case "meters":
38
+ case "yards":
39
+ return { endCondition: END_CONDITIONS.distance, endConditionValue: value };
40
+ case "kilometers":
41
+ return { endCondition: END_CONDITIONS.distance, endConditionValue: value * 1000 };
42
+ case "miles":
43
+ return {
44
+ endCondition: END_CONDITIONS.distance,
45
+ endConditionValue: Math.round(value * 1609.344),
46
+ };
47
+ default:
48
+ return { endCondition: END_CONDITIONS.time, endConditionValue: value * 60 };
49
+ }
50
+ }
51
+ function buildExecutableStep(step, ctx, counter) {
52
+ const order = counter.next++;
53
+ const { endCondition, endConditionValue } = endConditionFor(step.duration);
54
+ const target = resolveStepTarget(step.intensity, ctx.sportZones);
55
+ const dto = {
56
+ type: "ExecutableStepDTO",
57
+ stepId: order,
58
+ stepOrder: order,
59
+ stepType: stepTypeFor(step.type),
60
+ endCondition,
61
+ endConditionValue,
62
+ targetType: target.targetType,
63
+ };
64
+ if (target.targetValueOne !== undefined)
65
+ dto.targetValueOne = target.targetValueOne;
66
+ if (target.targetValueTwo !== undefined)
67
+ dto.targetValueTwo = target.targetValueTwo;
68
+ const description = step.notes ?? step.name;
69
+ if (description)
70
+ dto.description = description;
71
+ return dto;
72
+ }
73
+ function buildRepeatGroup(set, ctx, counter) {
74
+ const order = counter.next++;
75
+ const children = set.steps.map((child) => buildExecutableStep(child, ctx, counter));
76
+ return {
77
+ type: "RepeatGroupDTO",
78
+ stepId: order,
79
+ stepOrder: order,
80
+ stepType: STEP_TYPES.repeat,
81
+ numberOfIterations: set.repeats,
82
+ endCondition: END_CONDITIONS.iterations,
83
+ workoutSteps: children,
84
+ };
85
+ }
86
+ export function buildStructuredSteps(structure, ctx) {
87
+ const counter = { next: 1 };
88
+ const steps = [];
89
+ for (const step of structure.warmup ?? []) {
90
+ steps.push(buildExecutableStep(step, ctx, counter));
91
+ }
92
+ for (const item of structure.main) {
93
+ if ("repeats" in item) {
94
+ steps.push(buildRepeatGroup(item, ctx, counter));
95
+ }
96
+ else {
97
+ steps.push(buildExecutableStep(item, ctx, counter));
98
+ }
99
+ }
100
+ for (const step of structure.cooldown ?? []) {
101
+ steps.push(buildExecutableStep(step, ctx, counter));
102
+ }
103
+ return steps;
104
+ }
105
+ export function buildSimpleSteps(workout, ctx) {
106
+ const totalMinutes = workout.durationMinutes ?? 60;
107
+ const warmupMinutes = Math.min(15, Math.max(5, Math.round(totalMinutes * 0.1)));
108
+ const cooldownMinutes = Math.min(10, Math.max(5, Math.round(totalMinutes * 0.1)));
109
+ const mainMinutes = Math.max(0, totalMinutes - warmupMinutes - cooldownMinutes);
110
+ // Warmup and cooldown are always open (no.target); the workout-level target
111
+ // applies only to the main step.
112
+ const mainTarget = resolveWorkoutTarget(workout, ctx.sportZones);
113
+ const warmup = {
114
+ type: "ExecutableStepDTO",
115
+ stepId: 1,
116
+ stepOrder: 1,
117
+ stepType: STEP_TYPES.warmup,
118
+ endCondition: END_CONDITIONS.time,
119
+ endConditionValue: warmupMinutes * 60,
120
+ targetType: TARGET_TYPES.noTarget,
121
+ };
122
+ const main = {
123
+ type: "ExecutableStepDTO",
124
+ stepId: 2,
125
+ stepOrder: 2,
126
+ stepType: STEP_TYPES.interval,
127
+ endCondition: END_CONDITIONS.time,
128
+ endConditionValue: mainMinutes * 60,
129
+ targetType: mainTarget.targetType,
130
+ };
131
+ if (mainTarget.targetValueOne !== undefined)
132
+ main.targetValueOne = mainTarget.targetValueOne;
133
+ if (mainTarget.targetValueTwo !== undefined)
134
+ main.targetValueTwo = mainTarget.targetValueTwo;
135
+ if (workout.description)
136
+ main.description = workout.description;
137
+ const cooldown = {
138
+ type: "ExecutableStepDTO",
139
+ stepId: 3,
140
+ stepOrder: 3,
141
+ stepType: STEP_TYPES.cooldown,
142
+ endCondition: END_CONDITIONS.time,
143
+ endConditionValue: cooldownMinutes * 60,
144
+ targetType: TARGET_TYPES.noTarget,
145
+ };
146
+ return [warmup, main, cooldown];
147
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Resolve Claude Coach intensity/target data into Garmin workout targets.
3
+ *
4
+ * Target rules (HR targets are ALWAYS absolute bpm — never zoneNumber — so the
5
+ * on-watch effort matches the plan's Friel model regardless of the athlete's
6
+ * Garmin zone configuration):
7
+ * - Named HR zone -> heart.rate.zone (id 4) with the zone's hrLow/hrHigh bpm.
8
+ * - Explicit bpm -> heart.rate.zone (id 4) with targetValueOne/Two.
9
+ * - Pace range -> pace.zone (id 6) with targetValueOne/Two in m/s.
10
+ * - Otherwise -> no.target (id 1).
11
+ */
12
+ import type { IntensityTarget, Workout } from "../schema/training-plan.js";
13
+ import type { SportZones, TargetResolution } from "./context.js";
14
+ export declare function resolveStepTarget(intensity: IntensityTarget | undefined, sportZones: SportZones): TargetResolution;
15
+ export declare function resolveWorkoutTarget(workout: Workout, sportZones: SportZones): TargetResolution;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Resolve Claude Coach intensity/target data into Garmin workout targets.
3
+ *
4
+ * Target rules (HR targets are ALWAYS absolute bpm — never zoneNumber — so the
5
+ * on-watch effort matches the plan's Friel model regardless of the athlete's
6
+ * Garmin zone configuration):
7
+ * - Named HR zone -> heart.rate.zone (id 4) with the zone's hrLow/hrHigh bpm.
8
+ * - Explicit bpm -> heart.rate.zone (id 4) with targetValueOne/Two.
9
+ * - Pace range -> pace.zone (id 6) with targetValueOne/Two in m/s.
10
+ * - Otherwise -> no.target (id 1).
11
+ */
12
+ import { TARGET_TYPES } from "./dto.js";
13
+ import { parsePaceToMetersPerSecond } from "./pace.js";
14
+ function noTarget() {
15
+ return { targetType: TARGET_TYPES.noTarget };
16
+ }
17
+ function hrBpmRange(low, high) {
18
+ return {
19
+ targetType: TARGET_TYPES.heartRateZone,
20
+ targetValueOne: Math.min(low, high),
21
+ targetValueTwo: Math.max(low, high),
22
+ };
23
+ }
24
+ /** Build a pace.zone target from one or two pace strings (m/s bounds, slow..fast). */
25
+ function paceRange(low, high) {
26
+ const speedLow = parsePaceToMetersPerSecond(low);
27
+ const speedHigh = high ? parsePaceToMetersPerSecond(high) : speedLow;
28
+ return {
29
+ targetType: TARGET_TYPES.paceZone,
30
+ targetValueOne: Math.min(speedLow, speedHigh),
31
+ targetValueTwo: Math.max(speedLow, speedHigh),
32
+ };
33
+ }
34
+ /** Look up a pace zone's pace string by its zone id (number or string key). */
35
+ function lookupPaceZone(sportZones, zone) {
36
+ return sportZones.paceZones?.find((z) => `${z.zone}` === `${zone}`)?.pace;
37
+ }
38
+ /** Resolve a named HR zone (by number) to its absolute bpm boundaries. */
39
+ function hrZoneByNumber(sportZones, zone) {
40
+ const match = sportZones.hrZones?.find((z) => z.zone === zone);
41
+ return match ? hrBpmRange(match.hrLow, match.hrHigh) : noTarget();
42
+ }
43
+ /** Resolve a named HR zone (by name, e.g. "Tempo" or "Zone 3") to its bpm boundaries. */
44
+ function hrZoneByName(sportZones, name) {
45
+ const target = name.trim().toLowerCase();
46
+ const match = sportZones.hrZones?.find((z) => z.name.toLowerCase() === target || `zone ${z.zone}` === target);
47
+ return match ? hrBpmRange(match.hrLow, match.hrHigh) : noTarget();
48
+ }
49
+ export function resolveStepTarget(intensity, sportZones) {
50
+ if (!intensity)
51
+ return noTarget();
52
+ switch (intensity.unit) {
53
+ case "hr_zone": {
54
+ // Explicit bpm range wins; otherwise resolve the named zone to bpm.
55
+ if (intensity.valueLow !== undefined && intensity.valueHigh !== undefined) {
56
+ return hrBpmRange(intensity.valueLow, intensity.valueHigh);
57
+ }
58
+ if (intensity.value >= 1 && intensity.value <= 5) {
59
+ return hrZoneByNumber(sportZones, intensity.value);
60
+ }
61
+ return noTarget();
62
+ }
63
+ case "pace_zone": {
64
+ const pace = lookupPaceZone(sportZones, intensity.value);
65
+ return pace ? paceRange(pace) : noTarget();
66
+ }
67
+ default:
68
+ return noTarget();
69
+ }
70
+ }
71
+ export function resolveWorkoutTarget(workout, sportZones) {
72
+ if (workout.targetHR) {
73
+ return hrBpmRange(workout.targetHR.low, workout.targetHR.high);
74
+ }
75
+ if (workout.primaryZone) {
76
+ const resolved = hrZoneByName(sportZones, workout.primaryZone);
77
+ if (resolved.targetType.workoutTargetTypeId === TARGET_TYPES.heartRateZone.workoutTargetTypeId) {
78
+ return resolved;
79
+ }
80
+ }
81
+ if (workout.targetPace) {
82
+ return paceRange(workout.targetPace.low, workout.targetPace.high);
83
+ }
84
+ return noTarget();
85
+ }
@@ -0,0 +1,27 @@
1
+ export interface StravaConfig {
2
+ client_id: string;
3
+ client_secret: string;
4
+ }
5
+ export interface Config {
6
+ strava: StravaConfig;
7
+ sync_days: number;
8
+ }
9
+ export interface Tokens {
10
+ access_token: string;
11
+ refresh_token: string;
12
+ expires_at: number;
13
+ athlete_id: number;
14
+ }
15
+ export declare function ensureConfigDir(): void;
16
+ export declare function getConfigPath(): string;
17
+ export declare function getTokensPath(): string;
18
+ export declare function getDbPath(): string;
19
+ export declare function configExists(): boolean;
20
+ export declare function tokensExist(): boolean;
21
+ export declare function loadConfig(): Config;
22
+ export declare function saveConfig(config: Config): void;
23
+ export declare function loadTokens(): Tokens;
24
+ export declare function saveTokens(tokens: Tokens): void;
25
+ export declare function tokensExpired(tokens: Tokens): boolean;
26
+ export declare function promptForConfig(): Promise<Config>;
27
+ export declare function createConfig(client_id: string, client_secret: string, sync_days?: number): Config;
@@ -0,0 +1,86 @@
1
+ import { homedir } from "os";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import * as readline from "readline";
5
+ const CONFIG_DIR = join(homedir(), ".claude-coach");
6
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
+ const TOKENS_FILE = join(CONFIG_DIR, "tokens.json");
8
+ const DB_FILE = join(CONFIG_DIR, "coach.db");
9
+ export function ensureConfigDir() {
10
+ if (!existsSync(CONFIG_DIR)) {
11
+ mkdirSync(CONFIG_DIR, { recursive: true });
12
+ }
13
+ }
14
+ export function getConfigPath() {
15
+ return CONFIG_FILE;
16
+ }
17
+ export function getTokensPath() {
18
+ return TOKENS_FILE;
19
+ }
20
+ export function getDbPath() {
21
+ return process.env.CLAUDE_COACH_DB ?? DB_FILE;
22
+ }
23
+ export function configExists() {
24
+ return existsSync(CONFIG_FILE);
25
+ }
26
+ export function tokensExist() {
27
+ return existsSync(TOKENS_FILE);
28
+ }
29
+ export function loadConfig() {
30
+ if (!configExists()) {
31
+ throw new Error(`Config not found at ${CONFIG_FILE}. Run setup first.`);
32
+ }
33
+ const data = readFileSync(CONFIG_FILE, "utf-8");
34
+ return JSON.parse(data);
35
+ }
36
+ export function saveConfig(config) {
37
+ ensureConfigDir();
38
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
39
+ }
40
+ export function loadTokens() {
41
+ if (!tokensExist()) {
42
+ throw new Error(`Tokens not found at ${TOKENS_FILE}. Run auth first.`);
43
+ }
44
+ const data = readFileSync(TOKENS_FILE, "utf-8");
45
+ return JSON.parse(data);
46
+ }
47
+ export function saveTokens(tokens) {
48
+ ensureConfigDir();
49
+ writeFileSync(TOKENS_FILE, JSON.stringify(tokens, null, 2));
50
+ }
51
+ export function tokensExpired(tokens) {
52
+ // Add 60 second buffer
53
+ return Date.now() / 1000 > tokens.expires_at - 60;
54
+ }
55
+ async function prompt(question) {
56
+ const rl = readline.createInterface({
57
+ input: process.stdin,
58
+ output: process.stdout,
59
+ });
60
+ return new Promise((resolve) => {
61
+ rl.question(question, (answer) => {
62
+ rl.close();
63
+ resolve(answer.trim());
64
+ });
65
+ });
66
+ }
67
+ export async function promptForConfig() {
68
+ console.log("\n🚴 Claude Coach Setup\n");
69
+ console.log("To use this tool, you need a Strava API application.");
70
+ console.log("Create one at: https://www.strava.com/settings/api");
71
+ console.log('Set "Authorization Callback Domain" to: localhost\n');
72
+ const client_id = await prompt("Enter your Strava Client ID: ");
73
+ const client_secret = await prompt("Enter your Strava Client Secret: ");
74
+ const sync_days_str = await prompt("Days of history to sync (default 730): ");
75
+ const sync_days = parseInt(sync_days_str) || 730;
76
+ return {
77
+ strava: { client_id, client_secret },
78
+ sync_days,
79
+ };
80
+ }
81
+ export function createConfig(client_id, client_secret, sync_days = 730) {
82
+ return {
83
+ strava: { client_id, client_secret },
84
+ sync_days,
85
+ };
86
+ }
@@ -0,0 +1,13 @@
1
+ export declare const log: {
2
+ info: (message: string, ...args: unknown[]) => void;
3
+ success: (message: string, ...args: unknown[]) => void;
4
+ warn: (message: string, ...args: unknown[]) => void;
5
+ error: (message: string, ...args: unknown[]) => void;
6
+ debug: (message: string, ...args: unknown[]) => void;
7
+ box: (message: string) => void;
8
+ start: (message: string) => void;
9
+ ready: (message: string) => void;
10
+ progress: (message: string) => void;
11
+ progressEnd: () => void;
12
+ };
13
+ export type Logger = typeof log;
@@ -0,0 +1,28 @@
1
+ import { consola } from "consola";
2
+ // Configure consola with pretty formatting
3
+ const logger = consola.create({
4
+ level: process.env.LOG_LEVEL === "debug" ? 4 : 3,
5
+ formatOptions: {
6
+ date: false,
7
+ colors: true,
8
+ compact: false,
9
+ },
10
+ });
11
+ export const log = {
12
+ info: (message, ...args) => logger.info(message, ...args),
13
+ success: (message, ...args) => logger.success(message, ...args),
14
+ warn: (message, ...args) => logger.warn(message, ...args),
15
+ error: (message, ...args) => logger.error(message, ...args),
16
+ debug: (message, ...args) => logger.debug(message, ...args),
17
+ box: (message) => logger.box(message),
18
+ start: (message) => logger.start(message),
19
+ ready: (message) => logger.ready(message),
20
+ // Progress-style logging (overwrites current line)
21
+ progress: (message) => {
22
+ process.stdout.write(`\r${message}`);
23
+ },
24
+ // End progress line
25
+ progressEnd: () => {
26
+ process.stdout.write("\n");
27
+ },
28
+ };
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Training Plan JSON Schema
3
+ *
4
+ * Designed to be comprehensive enough for export to:
5
+ * - Zwift (.zwo workouts)
6
+ * - Garmin Connect (.fit workouts)
7
+ * - TrainingPeaks
8
+ * - Other training platforms
9
+ */
10
+ export type Sport = "swim" | "bike" | "run" | "trailrun" | "strength" | "brick" | "race" | "rest";
11
+ export type WorkoutType = "rest" | "recovery" | "endurance" | "tempo" | "threshold" | "intervals" | "vo2max" | "sprint" | "race" | "brick" | "technique" | "openwater" | "hills" | "long";
12
+ export type IntensityUnit = "percent_ftp" | "percent_lthr" | "hr_zone" | "pace_zone" | "rpe" | "css_offset";
13
+ export type DurationUnit = "seconds" | "minutes" | "hours" | "meters" | "kilometers" | "miles" | "yards" | "laps";
14
+ export type StepType = "warmup" | "work" | "recovery" | "rest" | "cooldown" | "interval_set";
15
+ export type SwimDistanceUnit = "meters" | "yards";
16
+ export type LandDistanceUnit = "kilometers" | "miles";
17
+ export type FirstDayOfWeek = "monday" | "sunday";
18
+ export interface UnitPreferences {
19
+ swim: SwimDistanceUnit;
20
+ bike: LandDistanceUnit;
21
+ run: LandDistanceUnit;
22
+ firstDayOfWeek: FirstDayOfWeek;
23
+ }
24
+ export declare const defaultPreferences: UnitPreferences;
25
+ export interface IntensityTarget {
26
+ unit: IntensityUnit;
27
+ value: number;
28
+ valueLow?: number;
29
+ valueHigh?: number;
30
+ description?: string;
31
+ }
32
+ export interface DurationTarget {
33
+ unit: DurationUnit;
34
+ value: number;
35
+ }
36
+ export interface WorkoutStep {
37
+ type: StepType;
38
+ name?: string;
39
+ duration: DurationTarget;
40
+ intensity: IntensityTarget;
41
+ cadence?: {
42
+ low: number;
43
+ high: number;
44
+ };
45
+ notes?: string;
46
+ }
47
+ export interface IntervalSet {
48
+ type: "interval_set";
49
+ name?: string;
50
+ repeats: number;
51
+ steps: WorkoutStep[];
52
+ }
53
+ export interface StructuredWorkout {
54
+ warmup?: WorkoutStep[];
55
+ main: (WorkoutStep | IntervalSet)[];
56
+ cooldown?: WorkoutStep[];
57
+ totalDuration?: DurationTarget;
58
+ estimatedTSS?: number;
59
+ estimatedIF?: number;
60
+ }
61
+ export interface Workout {
62
+ id: string;
63
+ sport: Sport;
64
+ type: WorkoutType;
65
+ name: string;
66
+ description: string;
67
+ durationMinutes?: number;
68
+ distanceMeters?: number;
69
+ primaryZone?: string;
70
+ targetHR?: {
71
+ low: number;
72
+ high: number;
73
+ };
74
+ targetPower?: {
75
+ low: number;
76
+ high: number;
77
+ };
78
+ targetPace?: {
79
+ low: string;
80
+ high: string;
81
+ };
82
+ rpe?: number;
83
+ structure?: StructuredWorkout;
84
+ humanReadable?: string;
85
+ completed: boolean;
86
+ completedAt?: string;
87
+ actualDuration?: number;
88
+ actualDistance?: number;
89
+ notes?: string;
90
+ }
91
+ export interface TrainingDay {
92
+ date: string;
93
+ dayOfWeek: string;
94
+ workouts: Workout[];
95
+ }
96
+ export interface WeekSummary {
97
+ totalHours: number;
98
+ totalTSS?: number;
99
+ bySport: {
100
+ [key in Sport]?: {
101
+ sessions: number;
102
+ hours: number;
103
+ km?: number;
104
+ };
105
+ };
106
+ }
107
+ export interface TrainingWeek {
108
+ weekNumber: number;
109
+ startDate: string;
110
+ endDate: string;
111
+ phase: string;
112
+ focus: string;
113
+ targetHours: number;
114
+ days: TrainingDay[];
115
+ summary: WeekSummary;
116
+ isRecoveryWeek: boolean;
117
+ }
118
+ export interface HeartRateZones {
119
+ lthr: number;
120
+ zones: {
121
+ zone: number;
122
+ name: string;
123
+ percentLow: number;
124
+ percentHigh: number;
125
+ hrLow: number;
126
+ hrHigh: number;
127
+ }[];
128
+ }
129
+ export interface PowerZones {
130
+ ftp: number;
131
+ zones: {
132
+ zone: number;
133
+ name: string;
134
+ percentLow: number;
135
+ percentHigh: number;
136
+ wattsLow: number;
137
+ wattsHigh: number;
138
+ }[];
139
+ }
140
+ export interface SwimZones {
141
+ css: string;
142
+ cssSeconds: number;
143
+ zones: {
144
+ zone: number;
145
+ name: string;
146
+ paceOffset: number;
147
+ pace: string;
148
+ }[];
149
+ }
150
+ export interface PaceZones {
151
+ thresholdPace: string;
152
+ thresholdPaceSeconds: number;
153
+ zones: {
154
+ zone: string;
155
+ name: string;
156
+ pace: string;
157
+ paceSeconds: number;
158
+ }[];
159
+ }
160
+ export interface AthleteZones {
161
+ run?: {
162
+ hr?: HeartRateZones;
163
+ pace?: PaceZones;
164
+ };
165
+ bike?: {
166
+ hr?: HeartRateZones;
167
+ power?: PowerZones;
168
+ };
169
+ swim?: SwimZones;
170
+ maxHR?: number;
171
+ restingHR?: number;
172
+ weight?: number;
173
+ }
174
+ export interface AthleteAssessment {
175
+ foundation: {
176
+ raceHistory: string[];
177
+ peakTrainingLoad: number;
178
+ foundationLevel: "beginner" | "intermediate" | "advanced" | "elite";
179
+ yearsInSport: number;
180
+ };
181
+ currentForm: {
182
+ weeklyVolume: {
183
+ total: number;
184
+ swim?: number;
185
+ bike?: number;
186
+ run?: number;
187
+ };
188
+ longestSessions: {
189
+ swim?: number;
190
+ bike?: number;
191
+ run?: number;
192
+ };
193
+ consistency: number;
194
+ timeSincePeakFitness?: string;
195
+ reasonForTimeOff?: string;
196
+ };
197
+ strengths: {
198
+ sport: Sport;
199
+ evidence: string;
200
+ }[];
201
+ limiters: {
202
+ sport: Sport;
203
+ evidence: string;
204
+ }[];
205
+ constraints: string[];
206
+ }
207
+ export interface TrainingPhase {
208
+ name: string;
209
+ startWeek: number;
210
+ endWeek: number;
211
+ focus: string;
212
+ weeklyHoursRange: {
213
+ low: number;
214
+ high: number;
215
+ };
216
+ keyWorkouts: string[];
217
+ physiologicalGoals: string[];
218
+ }
219
+ export interface RaceStrategy {
220
+ event: {
221
+ name: string;
222
+ date: string;
223
+ type: string;
224
+ distances?: {
225
+ swim?: number;
226
+ bike?: number;
227
+ run?: number;
228
+ };
229
+ };
230
+ pacing: {
231
+ swim?: {
232
+ target: string;
233
+ notes: string;
234
+ };
235
+ bike?: {
236
+ targetPower: string;
237
+ targetHR: string;
238
+ notes: string;
239
+ };
240
+ run?: {
241
+ targetPace: string;
242
+ targetHR: string;
243
+ notes: string;
244
+ };
245
+ };
246
+ nutrition: {
247
+ preRace: string;
248
+ during: {
249
+ carbsPerHour: number;
250
+ fluidPerHour: string;
251
+ products: string[];
252
+ };
253
+ notes: string;
254
+ };
255
+ taper: {
256
+ startDate: string;
257
+ volumeReduction: number;
258
+ notes: string;
259
+ };
260
+ raceDay: {
261
+ wakeUpTime?: string;
262
+ preRaceMeal?: string;
263
+ warmUp?: string;
264
+ mentalCues?: string[];
265
+ };
266
+ }
267
+ export interface TrainingPlan {
268
+ version: "1.0";
269
+ meta: {
270
+ id: string;
271
+ athlete: string;
272
+ event: string;
273
+ eventDate: string;
274
+ planStartDate: string;
275
+ planEndDate: string;
276
+ createdAt: string;
277
+ updatedAt: string;
278
+ totalWeeks: number;
279
+ generatedBy: string;
280
+ };
281
+ preferences: UnitPreferences;
282
+ assessment: AthleteAssessment;
283
+ zones: AthleteZones;
284
+ phases: TrainingPhase[];
285
+ weeks: TrainingWeek[];
286
+ raceStrategy: RaceStrategy;
287
+ }
288
+ export declare const exampleWorkout: Workout;