endurance-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 (50) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +94 -0
  3. package/bin/claude-coach.js +10 -0
  4. package/dist/cli.d.ts +6 -0
  5. package/dist/cli.js +1077 -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 +1 -0
  9. package/dist/db/migrate.js +14 -0
  10. package/dist/db/schema.sql +105 -0
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.js +13 -0
  13. package/dist/lib/config.d.ts +27 -0
  14. package/dist/lib/config.js +86 -0
  15. package/dist/lib/logging.d.ts +13 -0
  16. package/dist/lib/logging.js +28 -0
  17. package/dist/schema/training-plan.d.ts +288 -0
  18. package/dist/schema/training-plan.js +88 -0
  19. package/dist/schema/training-plan.schema.d.ts +1875 -0
  20. package/dist/schema/training-plan.schema.js +418 -0
  21. package/dist/strava/api.d.ts +5 -0
  22. package/dist/strava/api.js +63 -0
  23. package/dist/strava/oauth.d.ts +4 -0
  24. package/dist/strava/oauth.js +113 -0
  25. package/dist/strava/types.d.ts +46 -0
  26. package/dist/strava/types.js +1 -0
  27. package/dist/viewer/lib/UpdatePlan.d.ts +48 -0
  28. package/dist/viewer/lib/UpdatePlan.js +209 -0
  29. package/dist/viewer/lib/export/erg.d.ts +26 -0
  30. package/dist/viewer/lib/export/erg.js +208 -0
  31. package/dist/viewer/lib/export/fit.d.ts +25 -0
  32. package/dist/viewer/lib/export/fit.js +308 -0
  33. package/dist/viewer/lib/export/ics.d.ts +13 -0
  34. package/dist/viewer/lib/export/ics.js +142 -0
  35. package/dist/viewer/lib/export/index.d.ts +50 -0
  36. package/dist/viewer/lib/export/index.js +229 -0
  37. package/dist/viewer/lib/export/zwo.d.ts +21 -0
  38. package/dist/viewer/lib/export/zwo.js +233 -0
  39. package/dist/viewer/lib/utils.d.ts +14 -0
  40. package/dist/viewer/lib/utils.js +123 -0
  41. package/dist/viewer/main.d.ts +5 -0
  42. package/dist/viewer/main.js +6 -0
  43. package/dist/viewer/stores/changes.d.ts +21 -0
  44. package/dist/viewer/stores/changes.js +49 -0
  45. package/dist/viewer/stores/plan.d.ts +11 -0
  46. package/dist/viewer/stores/plan.js +40 -0
  47. package/dist/viewer/stores/settings.d.ts +53 -0
  48. package/dist/viewer/stores/settings.js +215 -0
  49. package/package.json +74 -0
  50. package/templates/plan-viewer.html +70 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Initialize the SQLite backend. Must be called before using other functions.
3
+ */
4
+ export declare function initDatabase(): Promise<void>;
5
+ export declare function query(sql: string): string;
6
+ export declare function queryJson<T>(sql: string): T[];
7
+ export declare function execute(sql: string): void;
8
+ export declare function runScript(script: string): void;
@@ -0,0 +1,111 @@
1
+ import { execSync, spawnSync } from "child_process";
2
+ import { getDbPath } from "../lib/config.js";
3
+ let cachedBackend = null;
4
+ /**
5
+ * Try to use Node's built-in SQLite module (Node 22.5+).
6
+ * Falls back to shelling out to sqlite3 CLI if not available.
7
+ */
8
+ async function detectBackend() {
9
+ // Try Node.js built-in SQLite first (Node 22.5+)
10
+ try {
11
+ // Dynamic import to avoid syntax errors on older Node versions
12
+ const sqlite = await import("node:sqlite");
13
+ const dbPath = getDbPath();
14
+ const db = new sqlite.DatabaseSync(dbPath);
15
+ return {
16
+ query(sql) {
17
+ const stmt = db.prepare(sql);
18
+ const rows = stmt.all();
19
+ if (rows.length === 0)
20
+ return "";
21
+ // Format as simple text output (column values separated by |)
22
+ return rows
23
+ .map((row) => Object.values(row)
24
+ .map((v) => (v === null ? "" : String(v)))
25
+ .join("|"))
26
+ .join("\n");
27
+ },
28
+ queryJson(sql) {
29
+ const stmt = db.prepare(sql);
30
+ return stmt.all();
31
+ },
32
+ execute(sql) {
33
+ db.exec(sql);
34
+ },
35
+ };
36
+ }
37
+ catch {
38
+ // Node.js built-in SQLite not available, try CLI
39
+ }
40
+ // Fallback: Use sqlite3 CLI
41
+ try {
42
+ // Check if sqlite3 is available
43
+ execSync("sqlite3 --version", { stdio: "ignore" });
44
+ return {
45
+ query(sql) {
46
+ const dbPath = getDbPath();
47
+ return execSync(`sqlite3 "${dbPath}" "${sql.replace(/"/g, '\\"')}"`, {
48
+ encoding: "utf-8",
49
+ });
50
+ },
51
+ queryJson(sql) {
52
+ const dbPath = getDbPath();
53
+ const result = execSync(`sqlite3 -json "${dbPath}" "${sql.replace(/"/g, '\\"')}"`, {
54
+ encoding: "utf-8",
55
+ });
56
+ if (!result.trim())
57
+ return [];
58
+ return JSON.parse(result);
59
+ },
60
+ execute(sql) {
61
+ const dbPath = getDbPath();
62
+ const result = spawnSync("sqlite3", [dbPath], {
63
+ input: sql,
64
+ encoding: "utf-8",
65
+ });
66
+ if (result.error)
67
+ throw result.error;
68
+ if (result.status !== 0) {
69
+ throw new Error(`SQLite error: ${result.stderr}`);
70
+ }
71
+ },
72
+ };
73
+ }
74
+ catch {
75
+ throw new Error("SQLite is not available. Please either:\n" +
76
+ " 1. Use Node.js 22.5+ (has built-in SQLite)\n" +
77
+ " 2. Install sqlite3 CLI (brew install sqlite3 / apt install sqlite3)");
78
+ }
79
+ }
80
+ /**
81
+ * Initialize the SQLite backend. Must be called before using other functions.
82
+ */
83
+ export async function initDatabase() {
84
+ if (!cachedBackend) {
85
+ cachedBackend = await detectBackend();
86
+ }
87
+ }
88
+ /**
89
+ * Get the backend, throwing if not initialized.
90
+ */
91
+ function getBackend() {
92
+ if (!cachedBackend) {
93
+ throw new Error("Database not initialized. Call initDatabase() first.");
94
+ }
95
+ return cachedBackend;
96
+ }
97
+ // ============================================================================
98
+ // Public API (synchronous after initialization)
99
+ // ============================================================================
100
+ export function query(sql) {
101
+ return getBackend().query(sql);
102
+ }
103
+ export function queryJson(sql) {
104
+ return getBackend().queryJson(sql);
105
+ }
106
+ export function execute(sql) {
107
+ getBackend().execute(sql);
108
+ }
109
+ export function runScript(script) {
110
+ execute(script);
111
+ }
@@ -0,0 +1 @@
1
+ export declare function migrate(): void;
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { runScript } from "./client.js";
5
+ import { ensureConfigDir } from "../lib/config.js";
6
+ import { log } from "../lib/logging.js";
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ export function migrate() {
9
+ ensureConfigDir();
10
+ const schemaPath = join(__dirname, "schema.sql");
11
+ const schema = readFileSync(schemaPath, "utf-8");
12
+ runScript(schema);
13
+ log.success("Database schema initialized");
14
+ }
@@ -0,0 +1,105 @@
1
+ -- Core activity data
2
+ CREATE TABLE IF NOT EXISTS activities (
3
+ id INTEGER PRIMARY KEY, -- Strava activity ID
4
+ name TEXT,
5
+ sport_type TEXT, -- Run, Ride, Swim, etc.
6
+ start_date TEXT, -- ISO 8601 UTC
7
+ elapsed_time INTEGER, -- seconds
8
+ moving_time INTEGER, -- seconds
9
+ distance REAL, -- meters
10
+ total_elevation_gain REAL, -- meters
11
+ average_speed REAL, -- m/s
12
+ max_speed REAL, -- m/s
13
+ average_heartrate REAL,
14
+ max_heartrate REAL,
15
+ average_watts REAL, -- cycling/running power
16
+ max_watts REAL,
17
+ weighted_average_watts REAL, -- normalized power
18
+ kilojoules REAL,
19
+ suffer_score INTEGER, -- Strava's relative effort
20
+ average_cadence REAL,
21
+ calories REAL,
22
+ description TEXT,
23
+ workout_type INTEGER, -- 0=default, 1=race, 2=workout, 3=long run
24
+ gear_id TEXT,
25
+ raw_json TEXT, -- full Strava response as JSON
26
+ synced_at TEXT DEFAULT (datetime('now'))
27
+ );
28
+
29
+ -- Time-series streams (HR, power, pace over time)
30
+ CREATE TABLE IF NOT EXISTS streams (
31
+ activity_id INTEGER PRIMARY KEY,
32
+ time_data TEXT, -- JSON array: seconds from start
33
+ distance_data TEXT, -- JSON array: cumulative meters
34
+ heartrate_data TEXT, -- JSON array
35
+ watts_data TEXT, -- JSON array
36
+ cadence_data TEXT, -- JSON array
37
+ altitude_data TEXT, -- JSON array
38
+ velocity_data TEXT, -- JSON array: m/s
39
+ FOREIGN KEY (activity_id) REFERENCES activities(id)
40
+ );
41
+
42
+ -- Athlete profile
43
+ CREATE TABLE IF NOT EXISTS athlete (
44
+ id INTEGER PRIMARY KEY,
45
+ firstname TEXT,
46
+ lastname TEXT,
47
+ weight REAL, -- kg
48
+ ftp INTEGER, -- functional threshold power (watts)
49
+ max_heartrate INTEGER,
50
+ raw_json TEXT,
51
+ updated_at TEXT DEFAULT (datetime('now'))
52
+ );
53
+
54
+ -- Training goals
55
+ CREATE TABLE IF NOT EXISTS goals (
56
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ event_name TEXT, -- "Ironman 70.3 Oceanside"
58
+ event_date TEXT, -- ISO 8601
59
+ event_type TEXT, -- triathlon, marathon, ultra, century
60
+ notes TEXT, -- constraints, injuries, etc.
61
+ created_at TEXT DEFAULT (datetime('now'))
62
+ );
63
+
64
+ -- Sync metadata
65
+ CREATE TABLE IF NOT EXISTS sync_log (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ started_at TEXT,
68
+ completed_at TEXT,
69
+ activities_synced INTEGER,
70
+ status TEXT -- success, failed, partial
71
+ );
72
+
73
+ -- Indexes for common queries
74
+ CREATE INDEX IF NOT EXISTS idx_activities_date ON activities(start_date);
75
+ CREATE INDEX IF NOT EXISTS idx_activities_sport ON activities(sport_type);
76
+ CREATE INDEX IF NOT EXISTS idx_activities_sport_date ON activities(sport_type, start_date);
77
+
78
+ -- Useful views
79
+ DROP VIEW IF EXISTS weekly_volume;
80
+ CREATE VIEW weekly_volume AS
81
+ SELECT
82
+ strftime('%Y-W%W', start_date) AS week,
83
+ sport_type,
84
+ COUNT(*) AS sessions,
85
+ ROUND(SUM(moving_time) / 3600.0, 1) AS hours,
86
+ ROUND(SUM(distance) / 1000.0, 1) AS km,
87
+ ROUND(AVG(average_heartrate), 0) AS avg_hr,
88
+ ROUND(AVG(suffer_score), 0) AS avg_effort
89
+ FROM activities
90
+ GROUP BY week, sport_type
91
+ ORDER BY week DESC, sport_type;
92
+
93
+ DROP VIEW IF EXISTS recent_activities;
94
+ CREATE VIEW recent_activities AS
95
+ SELECT
96
+ date(start_date) AS date,
97
+ sport_type,
98
+ name,
99
+ moving_time / 60 AS minutes,
100
+ ROUND(distance / 1000.0, 1) AS km,
101
+ ROUND(average_heartrate, 0) AS hr,
102
+ suffer_score
103
+ FROM activities
104
+ ORDER BY start_date DESC
105
+ LIMIT 50;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Endurance Coach - Training Plan Generator
3
+ *
4
+ * Public API for validating and working with training plans.
5
+ */
6
+ export { validatePlan, validatePlanOrThrow, formatValidationErrors, getJsonSchema, TrainingPlanSchema, WorkoutSchema, TrainingWeekSchema, TrainingDaySchema, TrainingPhaseSchema, AthleteAssessmentSchema, AthleteZonesSchema, RaceStrategySchema, UnitPreferencesSchema, PlanMetaSchema, type TrainingPlan, type Workout, type TrainingWeek, type TrainingDay, type TrainingPhase, type AthleteAssessment, type AthleteZones, type RaceStrategy, type UnitPreferences, type ValidationResult, type ValidationError, } from "./schema/training-plan.schema.js";
7
+ export type { Sport, WorkoutType, IntensityUnit, DurationUnit, StepType, SwimDistanceUnit, LandDistanceUnit, FirstDayOfWeek, IntensityTarget, DurationTarget, WorkoutStep, IntervalSet, StructuredWorkout, WeekSummary, HeartRateZones, PowerZones, SwimZones, PaceZones, } from "./schema/training-plan.js";
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Endurance Coach - Training Plan Generator
3
+ *
4
+ * Public API for validating and working with training plans.
5
+ */
6
+ // Schema validation
7
+ export {
8
+ // Validation functions
9
+ validatePlan, validatePlanOrThrow, formatValidationErrors, getJsonSchema,
10
+ // Main schema
11
+ TrainingPlanSchema,
12
+ // Component schemas (for partial validation)
13
+ WorkoutSchema, TrainingWeekSchema, TrainingDaySchema, TrainingPhaseSchema, AthleteAssessmentSchema, AthleteZonesSchema, RaceStrategySchema, UnitPreferencesSchema, PlanMetaSchema, } from "./schema/training-plan.schema.js";
@@ -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(), ".endurance-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 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🚴 Endurance 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
+ };