claude-coach 0.0.1

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 (43) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +84 -0
  3. package/bin/claude-coach.js +10 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +277 -0
  6. package/dist/db/client.d.ts +4 -0
  7. package/dist/db/client.js +45 -0
  8. package/dist/db/migrate.d.ts +1 -0
  9. package/dist/db/migrate.js +14 -0
  10. package/dist/lib/config.d.ts +27 -0
  11. package/dist/lib/config.js +86 -0
  12. package/dist/lib/logging.d.ts +13 -0
  13. package/dist/lib/logging.js +28 -0
  14. package/dist/schema/training-plan.d.ts +288 -0
  15. package/dist/schema/training-plan.js +88 -0
  16. package/dist/strava/api.d.ts +5 -0
  17. package/dist/strava/api.js +63 -0
  18. package/dist/strava/oauth.d.ts +4 -0
  19. package/dist/strava/oauth.js +113 -0
  20. package/dist/strava/types.d.ts +46 -0
  21. package/dist/strava/types.js +1 -0
  22. package/dist/viewer/lib/export/erg.d.ts +26 -0
  23. package/dist/viewer/lib/export/erg.js +206 -0
  24. package/dist/viewer/lib/export/fit.d.ts +25 -0
  25. package/dist/viewer/lib/export/fit.js +307 -0
  26. package/dist/viewer/lib/export/ics.d.ts +13 -0
  27. package/dist/viewer/lib/export/ics.js +138 -0
  28. package/dist/viewer/lib/export/index.d.ts +50 -0
  29. package/dist/viewer/lib/export/index.js +229 -0
  30. package/dist/viewer/lib/export/zwo.d.ts +21 -0
  31. package/dist/viewer/lib/export/zwo.js +230 -0
  32. package/dist/viewer/lib/utils.d.ts +14 -0
  33. package/dist/viewer/lib/utils.js +118 -0
  34. package/dist/viewer/main.d.ts +5 -0
  35. package/dist/viewer/main.js +6 -0
  36. package/dist/viewer/stores/changes.d.ts +21 -0
  37. package/dist/viewer/stores/changes.js +49 -0
  38. package/dist/viewer/stores/plan.d.ts +4 -0
  39. package/dist/viewer/stores/plan.js +19 -0
  40. package/dist/viewer/stores/settings.d.ts +53 -0
  41. package/dist/viewer/stores/settings.js +207 -0
  42. package/package.json +55 -0
  43. package/templates/plan-viewer.html +70 -0
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2025 Felix Rieseberg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # Claude Coach
2
+
3
+ Claude Coach allows you to use Claude to create custom-tailored training programs for triathlons, marathons, and other endurance activities. Using a data-driven approach and principles from top training plans, Claude will create a training plan that's uniquely fit for you, your personal fitness, and the constraints you have in the next couple of weeks. Maybe you're recovering from an injury, maybe you're traveling and don't have access to a pool or track in a certain week - tell Claude about it and it'll create a plan that works for you.
4
+
5
+ The output is a beautiful training plan app that allows you to add, edit, or move workouts, mark them as complete, and update key training data like heart rate zones, LTHR, threshold paces, FTP, and others. Your data is kept locally in your browser.
6
+
7
+ Workouts can be exported as simple calendar events (.ics), Zwift (.zwo), Garmin (.fit), or TrainerRoad/ERG (.mrc) workouts.
8
+
9
+ ## Example
10
+
11
+ ## Installation & Creating a training plan
12
+
13
+ I happen to work at Anthropic, so this tool is optimized for Claude. To use this tool, you need access to Claude.ai or Claude Code with network access for Skills. Depending on user/admin settings, Skills may have full, partial, or no network access.
14
+
15
+ ### Installing the Skill
16
+
17
+ First, [download the latest skill from GitHub Releases](https://github.com/felixrieseberg/claude-coach/releases/latest/download/coach-skill.zip).
18
+
19
+ - If you're using Claude.ai, open the Settings, navigate to Capabilities, then click the `+ Add` button. Upload the `coach-skill.zip` file you just downloaded.
20
+ - If you're using Claude Code, run `/install-skill` and provide the path to the `coach-skill.zip` file you downloaded.
21
+
22
+ ### Creating a plan
23
+
24
+ Prompt Claude with something like this:
25
+
26
+ > Help me create a training plan for the Ironman 70.3 Oceanside on March 29th 2026 using the "coach" skill.
27
+
28
+ Claude will ask how you'd like to provide your fitness data. You have two options: You can either tell Claude about your fitness history manually - or you can give it access to your Strava activities. I recommend the later - data doesn't lie and more data allows Claude to make a training plan that really fits you.
29
+
30
+ #### Option 1: Connect to Strava (Recommended)
31
+
32
+ The easiest way to get a personalized plan is to let Claude analyze your Strava training history. This gives Claude real data about your current fitness, training patterns, and progress.
33
+
34
+ Claude needs a `Client ID` and `Client Secret` to access your Strava activities. You're only giving Claude access to your data - nobody else gets to see it.
35
+
36
+ 1. Go to [strava.com/settings/api](https://www.strava.com/settings/api) and log in with your Strava account
37
+ 2. You'll see a form titled "My API Application" - fill it out:
38
+ - **Application Name**: Enter anything you like (e.g., "Claude Coach")
39
+ - **Category**: Select "Data Importer"
40
+ - **Club**: Leave this blank
41
+ - **Website**: Enter any URL (e.g., `https://claude.ai`)
42
+ - **Application Description**: Enter anything (e.g., "Training plan generation")
43
+ - **Authorization Callback Domain**: Enter `localhost`
44
+ 3. Check the box to agree to Strava's API Agreement and click **Create**
45
+ 4. Copy your **Client ID** and **Client Secret** and give them to Claude when prompted
46
+
47
+ #### Option 2: Manual Entry
48
+
49
+ Don't use Strava, or prefer not to connect it? No problem. You can tell Claude about your fitness directly. Be prepared to share:
50
+
51
+ **Current Training (recent 4-8 weeks):**
52
+
53
+ - Weekly training hours by sport (swim/bike/run)
54
+ - Typical long session distances (longest ride, longest run, etc.)
55
+ - Training consistency (how many weeks have you been training regularly?)
56
+
57
+ **Performance Benchmarks (any you know):**
58
+
59
+ - Bike FTP (Functional Threshold Power) in watts
60
+ - Run threshold pace or recent race times (5K, 10K, half marathon, etc.)
61
+ - Swim CSS (Critical Swim Speed) or recent time trial (e.g., 1000m time)
62
+ - Max heart rate and/or lactate threshold heart rate
63
+
64
+ ### Telling Claude about your event & constraints
65
+
66
+ In the next step, Claude will ask you about yourself, the event you're training for, and any constraints it should keep in mind. Examples of information you'd tell any coach:
67
+
68
+ **Training History:**
69
+
70
+ - Years in the sport
71
+ - Previous races completed (distances and approximate times)
72
+ - Any recent breaks from training
73
+
74
+ **Constraints & Considerations:**
75
+
76
+ - Injuries or health issues
77
+ - Schedule limitations (work travel, family, etc.)
78
+ - Equipment access (pool availability, trainer, etc.)
79
+
80
+ Claude will use this information to create a plan tailored to your current fitness level. The more detail you provide, the better your plan will be.
81
+
82
+ # About
83
+
84
+ License: MIT.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const cliPath = join(__dirname, "..", "dist", "cli.js");
8
+
9
+ // Dynamically import the compiled CLI
10
+ await import(cliPath);
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,277 @@
1
+ import { configExists, loadConfig, promptForConfig, saveConfig, getDbPath, createConfig, } from "./lib/config.js";
2
+ import { log } from "./lib/logging.js";
3
+ import { migrate } from "./db/migrate.js";
4
+ import { execute } from "./db/client.js";
5
+ import { getValidTokens } from "./strava/oauth.js";
6
+ import { getAllActivities, getAthlete } from "./strava/api.js";
7
+ import { readFileSync, writeFileSync } from "fs";
8
+ import { dirname, join } from "path";
9
+ import { fileURLToPath } from "url";
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ function parseArgs() {
12
+ const args = process.argv.slice(2);
13
+ if (args.length === 0 || args[0] === "sync") {
14
+ // Sync command (default)
15
+ const syncArgs = { command: "sync" };
16
+ for (const arg of args) {
17
+ if (arg.startsWith("--client-id=")) {
18
+ syncArgs.clientId = arg.split("=")[1];
19
+ }
20
+ else if (arg.startsWith("--client-secret=")) {
21
+ syncArgs.clientSecret = arg.split("=")[1];
22
+ }
23
+ else if (arg.startsWith("--days=")) {
24
+ syncArgs.days = parseInt(arg.split("=")[1]);
25
+ }
26
+ }
27
+ return syncArgs;
28
+ }
29
+ if (args[0] === "render") {
30
+ if (!args[1]) {
31
+ log.error("render command requires an input file");
32
+ process.exit(1);
33
+ }
34
+ const renderArgs = {
35
+ command: "render",
36
+ inputFile: args[1],
37
+ };
38
+ for (let i = 2; i < args.length; i++) {
39
+ if (args[i] === "--output" || args[i] === "-o") {
40
+ renderArgs.outputFile = args[i + 1];
41
+ i++;
42
+ }
43
+ else if (args[i].startsWith("--output=")) {
44
+ renderArgs.outputFile = args[i].split("=")[1];
45
+ }
46
+ }
47
+ return renderArgs;
48
+ }
49
+ if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
50
+ return { command: "help" };
51
+ }
52
+ log.error(`Unknown command: ${args[0]}`);
53
+ process.exit(1);
54
+ }
55
+ function printHelp() {
56
+ console.log(`
57
+ Claude Coach - Training Plan Tools
58
+
59
+ Usage: npx claude-coach <command> [options]
60
+
61
+ Commands:
62
+ sync Sync activities from Strava (default)
63
+ render <file> Render a training plan JSON to HTML
64
+ help Show this help message
65
+
66
+ Sync Options:
67
+ --client-id=ID Strava API client ID
68
+ --client-secret=SEC Strava API client secret
69
+ --days=N Days of history to sync (default: 730)
70
+
71
+ Render Options:
72
+ --output, -o FILE Output HTML file (default: <input>.html)
73
+
74
+ Examples:
75
+ # Sync from Strava (interactive)
76
+ npx claude-coach
77
+
78
+ # Sync with credentials
79
+ npx claude-coach sync --client-id=12345 --client-secret=abc123
80
+
81
+ # Render a training plan to HTML
82
+ npx claude-coach render plan.json --output my-plan.html
83
+
84
+ # Render to stdout
85
+ npx claude-coach render plan.json
86
+ `);
87
+ }
88
+ // ============================================================================
89
+ // Sync Command
90
+ // ============================================================================
91
+ function escapeString(str) {
92
+ if (str == null)
93
+ return "NULL";
94
+ return `'${str.replace(/'/g, "''")}'`;
95
+ }
96
+ function insertActivity(activity) {
97
+ const sql = `
98
+ INSERT OR REPLACE INTO activities (
99
+ id, name, sport_type, start_date, elapsed_time, moving_time,
100
+ distance, total_elevation_gain, average_speed, max_speed,
101
+ average_heartrate, max_heartrate, average_watts, max_watts,
102
+ weighted_average_watts, kilojoules, suffer_score, average_cadence,
103
+ calories, description, workout_type, gear_id, raw_json, synced_at
104
+ ) VALUES (
105
+ ${activity.id},
106
+ ${escapeString(activity.name)},
107
+ ${escapeString(activity.sport_type)},
108
+ ${escapeString(activity.start_date)},
109
+ ${activity.elapsed_time ?? "NULL"},
110
+ ${activity.moving_time ?? "NULL"},
111
+ ${activity.distance ?? "NULL"},
112
+ ${activity.total_elevation_gain ?? "NULL"},
113
+ ${activity.average_speed ?? "NULL"},
114
+ ${activity.max_speed ?? "NULL"},
115
+ ${activity.average_heartrate ?? "NULL"},
116
+ ${activity.max_heartrate ?? "NULL"},
117
+ ${activity.average_watts ?? "NULL"},
118
+ ${activity.max_watts ?? "NULL"},
119
+ ${activity.weighted_average_watts ?? "NULL"},
120
+ ${activity.kilojoules ?? "NULL"},
121
+ ${activity.suffer_score ?? "NULL"},
122
+ ${activity.average_cadence ?? "NULL"},
123
+ ${activity.calories ?? "NULL"},
124
+ ${escapeString(activity.description)},
125
+ ${activity.workout_type ?? "NULL"},
126
+ ${escapeString(activity.gear_id)},
127
+ ${escapeString(JSON.stringify(activity))},
128
+ datetime('now')
129
+ );
130
+ `;
131
+ execute(sql);
132
+ }
133
+ function insertAthlete(athlete) {
134
+ const sql = `
135
+ INSERT OR REPLACE INTO athlete (id, firstname, lastname, weight, ftp, raw_json, updated_at)
136
+ VALUES (
137
+ ${athlete.id},
138
+ ${escapeString(athlete.firstname)},
139
+ ${escapeString(athlete.lastname)},
140
+ ${athlete.weight ?? "NULL"},
141
+ ${athlete.ftp ?? "NULL"},
142
+ ${escapeString(JSON.stringify(athlete))},
143
+ datetime('now')
144
+ );
145
+ `;
146
+ execute(sql);
147
+ }
148
+ async function runSync(args) {
149
+ log.box("Claude Coach - Strava Sync");
150
+ // Step 1: Check/create config
151
+ if (!configExists()) {
152
+ if (args.clientId && args.clientSecret) {
153
+ log.info("Creating configuration from command line arguments...");
154
+ const config = createConfig(args.clientId, args.clientSecret, args.days || 730);
155
+ saveConfig(config);
156
+ log.success("Configuration saved");
157
+ }
158
+ else {
159
+ log.info("No configuration found. Let's set things up.");
160
+ const config = await promptForConfig();
161
+ saveConfig(config);
162
+ log.success("Configuration saved");
163
+ }
164
+ }
165
+ const config = loadConfig();
166
+ const syncDays = args.days || config.sync_days || 730;
167
+ // Step 2: Initialize database
168
+ migrate();
169
+ // Step 3: Authenticate with Strava
170
+ const tokens = await getValidTokens();
171
+ // Step 4: Fetch and store athlete profile
172
+ log.start("Fetching athlete profile...");
173
+ const athlete = await getAthlete(tokens);
174
+ insertAthlete(athlete);
175
+ log.success(`Athlete: ${athlete.firstname} ${athlete.lastname}`);
176
+ // Step 5: Fetch activities
177
+ const afterDate = new Date();
178
+ afterDate.setDate(afterDate.getDate() - syncDays);
179
+ const activities = await getAllActivities(tokens, afterDate);
180
+ // Step 6: Store activities
181
+ log.start("Storing activities in database...");
182
+ let count = 0;
183
+ for (const activity of activities) {
184
+ insertActivity(activity);
185
+ count++;
186
+ if (count % 50 === 0) {
187
+ log.progress(` Stored ${count}/${activities.length}...`);
188
+ }
189
+ }
190
+ log.progressEnd();
191
+ log.success(`Stored ${activities.length} activities`);
192
+ // Step 7: Log sync
193
+ execute(`
194
+ INSERT INTO sync_log (started_at, completed_at, activities_synced, status)
195
+ VALUES (datetime('now'), datetime('now'), ${activities.length}, 'success');
196
+ `);
197
+ log.info(`Database: ${getDbPath()}`);
198
+ log.ready(`Query with: sqlite3 -json "${getDbPath()}" "SELECT * FROM weekly_volume"`);
199
+ }
200
+ // ============================================================================
201
+ // Render Command
202
+ // ============================================================================
203
+ function getTemplatePath() {
204
+ // Look for template in multiple locations
205
+ const locations = [
206
+ join(__dirname, "..", "templates", "plan-viewer.html"),
207
+ join(__dirname, "..", "..", "templates", "plan-viewer.html"),
208
+ join(process.cwd(), "templates", "plan-viewer.html"),
209
+ ];
210
+ for (const loc of locations) {
211
+ try {
212
+ readFileSync(loc);
213
+ return loc;
214
+ }
215
+ catch {
216
+ // Continue to next location
217
+ }
218
+ }
219
+ throw new Error("Could not find plan-viewer.html template");
220
+ }
221
+ function runRender(args) {
222
+ log.start("Rendering training plan...");
223
+ // Read the plan JSON
224
+ let planJson;
225
+ try {
226
+ planJson = readFileSync(args.inputFile, "utf-8");
227
+ }
228
+ catch (err) {
229
+ log.error(`Could not read input file: ${args.inputFile}`);
230
+ process.exit(1);
231
+ }
232
+ // Validate it's valid JSON
233
+ try {
234
+ JSON.parse(planJson);
235
+ }
236
+ catch (err) {
237
+ log.error("Input file is not valid JSON");
238
+ process.exit(1);
239
+ }
240
+ // Read the template
241
+ const templatePath = getTemplatePath();
242
+ let template = readFileSync(templatePath, "utf-8");
243
+ // Replace the plan data in the template
244
+ const planDataRegex = /<script type="application\/json" id="plan-data">[\s\S]*?<\/script>/;
245
+ const newPlanData = `<script type="application/json" id="plan-data">\n${planJson}\n</script>`;
246
+ template = template.replace(planDataRegex, newPlanData);
247
+ // Output
248
+ if (args.outputFile) {
249
+ writeFileSync(args.outputFile, template);
250
+ log.success(`Training plan rendered to: ${args.outputFile}`);
251
+ }
252
+ else {
253
+ // Output to stdout
254
+ console.log(template);
255
+ }
256
+ }
257
+ // ============================================================================
258
+ // Main
259
+ // ============================================================================
260
+ async function main() {
261
+ const args = parseArgs();
262
+ switch (args.command) {
263
+ case "help":
264
+ printHelp();
265
+ break;
266
+ case "sync":
267
+ await runSync(args);
268
+ break;
269
+ case "render":
270
+ runRender(args);
271
+ break;
272
+ }
273
+ }
274
+ main().catch((err) => {
275
+ log.error(err.message);
276
+ process.exit(1);
277
+ });
@@ -0,0 +1,4 @@
1
+ export declare function query(sql: string): string;
2
+ export declare function queryJson<T>(sql: string): T[];
3
+ export declare function execute(sql: string): void;
4
+ export declare function runScript(script: string): void;
@@ -0,0 +1,45 @@
1
+ import { execSync, spawnSync } from "child_process";
2
+ import { getDbPath } from "../lib/config.js";
3
+ export function query(sql) {
4
+ const dbPath = getDbPath();
5
+ try {
6
+ return execSync(`sqlite3 "${dbPath}" "${sql.replace(/"/g, '\\"')}"`, {
7
+ encoding: "utf-8",
8
+ });
9
+ }
10
+ catch (error) {
11
+ const err = error;
12
+ throw new Error(`SQLite error: ${err.stderr || err.message}`);
13
+ }
14
+ }
15
+ export function queryJson(sql) {
16
+ const dbPath = getDbPath();
17
+ try {
18
+ const result = execSync(`sqlite3 -json "${dbPath}" "${sql.replace(/"/g, '\\"')}"`, {
19
+ encoding: "utf-8",
20
+ });
21
+ if (!result.trim())
22
+ return [];
23
+ return JSON.parse(result);
24
+ }
25
+ catch (error) {
26
+ const err = error;
27
+ throw new Error(`SQLite error: ${err.stderr || err.message}`);
28
+ }
29
+ }
30
+ export function execute(sql) {
31
+ const dbPath = getDbPath();
32
+ const result = spawnSync("sqlite3", [dbPath], {
33
+ input: sql,
34
+ encoding: "utf-8",
35
+ });
36
+ if (result.error) {
37
+ throw result.error;
38
+ }
39
+ if (result.status !== 0) {
40
+ throw new Error(`SQLite error: ${result.stderr}`);
41
+ }
42
+ }
43
+ export function runScript(script) {
44
+ execute(script);
45
+ }
@@ -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,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 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
+ };