@andreasnlarsen/whoop-cli 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.
@@ -0,0 +1,31 @@
1
+ import { WhoopApiClient } from '../http/client.js';
2
+ import { getGlobalOptions, printData, printError } from './context.js';
3
+ export const registerProfileCommands = (program) => {
4
+ const profile = program.command('profile').description('Profile and body measurements');
5
+ profile
6
+ .command('show')
7
+ .description('Show basic WHOOP profile + body measurements')
8
+ .action(async function showAction() {
9
+ try {
10
+ const globals = getGlobalOptions(this);
11
+ const client = new WhoopApiClient(globals.profile);
12
+ const [basic, body] = await Promise.all([
13
+ client.requestJson({
14
+ path: '/developer/v2/user/profile/basic',
15
+ timeoutMs: globals.timeoutMs,
16
+ }),
17
+ client.requestJson({
18
+ path: '/developer/v2/user/measurement/body',
19
+ timeoutMs: globals.timeoutMs,
20
+ }),
21
+ ]);
22
+ printData(this, {
23
+ profile: basic,
24
+ body,
25
+ });
26
+ }
27
+ catch (err) {
28
+ printError(this, err);
29
+ }
30
+ });
31
+ };
@@ -0,0 +1,58 @@
1
+ import { WhoopApiClient } from '../http/client.js';
2
+ import { fetchRecoveries } from '../http/whoop-data.js';
3
+ import { getGlobalOptions, printData, printError } from './context.js';
4
+ import { parseDateRange, parseMaybeNumber } from '../util/time.js';
5
+ export const registerRecoveryCommands = (program) => {
6
+ const recovery = program.command('recovery').description('Recovery commands');
7
+ recovery
8
+ .command('latest')
9
+ .description('Fetch latest recovery record')
10
+ .action(async function latestAction() {
11
+ try {
12
+ const globals = getGlobalOptions(this);
13
+ const client = new WhoopApiClient(globals.profile);
14
+ const records = await fetchRecoveries(client, {
15
+ limit: 1,
16
+ timeoutMs: globals.timeoutMs,
17
+ });
18
+ printData(this, {
19
+ latest: records[0] ?? null,
20
+ });
21
+ }
22
+ catch (err) {
23
+ printError(this, err);
24
+ }
25
+ });
26
+ recovery
27
+ .command('list')
28
+ .description('List recovery records')
29
+ .option('--start <YYYY-MM-DD>')
30
+ .option('--end <YYYY-MM-DD>')
31
+ .option('--days <n>', 'lookback days if start/end not provided')
32
+ .option('--limit <n>', 'page size', '25')
33
+ .option('--all', 'follow pagination and fetch all pages')
34
+ .action(async function listAction(opts) {
35
+ try {
36
+ const globals = getGlobalOptions(this);
37
+ const client = new WhoopApiClient(globals.profile);
38
+ const range = parseDateRange({
39
+ start: opts.start,
40
+ end: opts.end,
41
+ days: parseMaybeNumber(opts.days),
42
+ });
43
+ const records = await fetchRecoveries(client, {
44
+ ...range,
45
+ limit: parseMaybeNumber(opts.limit),
46
+ all: Boolean(opts.all),
47
+ timeoutMs: globals.timeoutMs,
48
+ });
49
+ printData(this, {
50
+ count: records.length,
51
+ records,
52
+ });
53
+ }
54
+ catch (err) {
55
+ printError(this, err);
56
+ }
57
+ });
58
+ };
@@ -0,0 +1,92 @@
1
+ import { WhoopApiClient } from '../http/client.js';
2
+ import { fetchSleeps } from '../http/whoop-data.js';
3
+ import { getGlobalOptions, printData, printError } from './context.js';
4
+ import { avg, round } from '../util/metrics.js';
5
+ import { parseDateRange, parseMaybeNumber } from '../util/time.js';
6
+ export const registerSleepCommands = (program) => {
7
+ const sleep = program.command('sleep').description('Sleep commands');
8
+ sleep
9
+ .command('latest')
10
+ .description('Fetch latest sleep record')
11
+ .action(async function latestAction() {
12
+ try {
13
+ const globals = getGlobalOptions(this);
14
+ const client = new WhoopApiClient(globals.profile);
15
+ const records = await fetchSleeps(client, {
16
+ limit: 1,
17
+ timeoutMs: globals.timeoutMs,
18
+ });
19
+ printData(this, {
20
+ latest: records[0] ?? null,
21
+ });
22
+ }
23
+ catch (err) {
24
+ printError(this, err);
25
+ }
26
+ });
27
+ sleep
28
+ .command('list')
29
+ .description('List sleep records')
30
+ .option('--start <YYYY-MM-DD>')
31
+ .option('--end <YYYY-MM-DD>')
32
+ .option('--days <n>', 'lookback days if start/end not provided')
33
+ .option('--limit <n>', 'page size', '25')
34
+ .option('--all', 'follow pagination and fetch all pages')
35
+ .action(async function listAction(opts) {
36
+ try {
37
+ const globals = getGlobalOptions(this);
38
+ const client = new WhoopApiClient(globals.profile);
39
+ const range = parseDateRange({
40
+ start: opts.start,
41
+ end: opts.end,
42
+ days: parseMaybeNumber(opts.days),
43
+ });
44
+ const records = await fetchSleeps(client, {
45
+ ...range,
46
+ limit: parseMaybeNumber(opts.limit),
47
+ all: Boolean(opts.all),
48
+ timeoutMs: globals.timeoutMs,
49
+ });
50
+ printData(this, {
51
+ count: records.length,
52
+ records,
53
+ });
54
+ }
55
+ catch (err) {
56
+ printError(this, err);
57
+ }
58
+ });
59
+ sleep
60
+ .command('trend')
61
+ .description('Sleep trend summary over a period')
62
+ .option('--days <n>', 'lookback days', '30')
63
+ .option('--limit <n>', 'page size', '25')
64
+ .action(async function trendAction(opts) {
65
+ try {
66
+ const globals = getGlobalOptions(this);
67
+ const client = new WhoopApiClient(globals.profile);
68
+ const days = parseMaybeNumber(opts.days) ?? 30;
69
+ const records = await fetchSleeps(client, {
70
+ ...parseDateRange({ days }),
71
+ limit: parseMaybeNumber(opts.limit),
72
+ all: true,
73
+ timeoutMs: globals.timeoutMs,
74
+ });
75
+ const performance = round(avg(records.map((r) => r.score?.sleep_performance_percentage)), 1);
76
+ const consistency = round(avg(records.map((r) => r.score?.sleep_consistency_percentage)), 1);
77
+ const efficiency = round(avg(records.map((r) => r.score?.sleep_efficiency_percentage)), 1);
78
+ printData(this, {
79
+ windowDays: days,
80
+ records: records.length,
81
+ averages: {
82
+ sleepPerformancePct: performance,
83
+ sleepConsistencyPct: consistency,
84
+ sleepEfficiencyPct: efficiency,
85
+ },
86
+ });
87
+ }
88
+ catch (err) {
89
+ printError(this, err);
90
+ }
91
+ });
92
+ };
@@ -0,0 +1,115 @@
1
+ import { WhoopApiClient } from '../http/client.js';
2
+ import { fetchCycles, fetchRecoveries, fetchSleeps, fetchWorkouts } from '../http/whoop-data.js';
3
+ import { classifyRecovery } from '../util/metrics.js';
4
+ import { getGlobalOptions, printData, printError } from './context.js';
5
+ const recommendLoad = (recoveryScore) => {
6
+ if (typeof recoveryScore !== 'number') {
7
+ return { target: 'unknown', note: 'No recovery score available yet.' };
8
+ }
9
+ if (recoveryScore >= 67) {
10
+ return { target: 'high', note: 'Push day is appropriate if goals require it.' };
11
+ }
12
+ if (recoveryScore >= 34) {
13
+ return { target: 'moderate', note: 'Maintain and avoid aggressive overload.' };
14
+ }
15
+ return { target: 'restorative', note: 'Prioritize recovery, low strain, and sleep quality.' };
16
+ };
17
+ const fetchDailyCore = async (client, timeoutMs) => {
18
+ const [recovery, sleep, cycle, workouts] = await Promise.all([
19
+ fetchRecoveries(client, { limit: 1, timeoutMs }),
20
+ fetchSleeps(client, { limit: 1, timeoutMs }),
21
+ fetchCycles(client, { limit: 1, timeoutMs }),
22
+ fetchWorkouts(client, { limit: 5, timeoutMs }),
23
+ ]);
24
+ return {
25
+ recovery: recovery[0] ?? null,
26
+ sleep: sleep[0] ?? null,
27
+ cycle: cycle[0] ?? null,
28
+ workouts,
29
+ };
30
+ };
31
+ export const registerSummaryCommands = (program) => {
32
+ program
33
+ .command('summary')
34
+ .description('One-line style WHOOP snapshot')
35
+ .action(async function summaryAction() {
36
+ try {
37
+ const globals = getGlobalOptions(this);
38
+ const client = new WhoopApiClient(globals.profile);
39
+ const core = await fetchDailyCore(client, globals.timeoutMs);
40
+ const payload = {
41
+ recoveryScore: core.recovery?.score?.recovery_score ?? null,
42
+ hrv: core.recovery?.score?.hrv_rmssd_milli ?? null,
43
+ restingHr: core.recovery?.score?.resting_heart_rate ?? null,
44
+ sleepPerformance: core.sleep?.score?.sleep_performance_percentage ?? null,
45
+ cycleStrain: core.cycle?.score?.strain ?? null,
46
+ recentWorkouts: core.workouts.length,
47
+ };
48
+ printData(this, payload);
49
+ }
50
+ catch (err) {
51
+ printError(this, err);
52
+ }
53
+ });
54
+ program
55
+ .command('day-brief')
56
+ .description('Readiness-oriented daily brief for planning your day')
57
+ .action(async function dayBriefAction() {
58
+ try {
59
+ const globals = getGlobalOptions(this);
60
+ const client = new WhoopApiClient(globals.profile);
61
+ const core = await fetchDailyCore(client, globals.timeoutMs);
62
+ const recoveryScore = core.recovery?.score?.recovery_score;
63
+ const load = recommendLoad(recoveryScore);
64
+ printData(this, {
65
+ readiness: {
66
+ recoveryScore: recoveryScore ?? null,
67
+ zone: classifyRecovery(recoveryScore),
68
+ recommendation: load,
69
+ },
70
+ sleep: {
71
+ performancePct: core.sleep?.score?.sleep_performance_percentage ?? null,
72
+ consistencyPct: core.sleep?.score?.sleep_consistency_percentage ?? null,
73
+ efficiencyPct: core.sleep?.score?.sleep_efficiency_percentage ?? null,
74
+ },
75
+ recentCycle: {
76
+ strain: core.cycle?.score?.strain ?? null,
77
+ start: core.cycle?.start ?? null,
78
+ end: core.cycle?.end ?? null,
79
+ },
80
+ guidance: [
81
+ recoveryScore !== undefined && recoveryScore < 34
82
+ ? 'Prioritize recovery actions: hydration, lower-intensity training, stable bedtime.'
83
+ : 'Keep sleep consistency high and align strain to readiness zone.',
84
+ ],
85
+ });
86
+ }
87
+ catch (err) {
88
+ printError(this, err);
89
+ }
90
+ });
91
+ program
92
+ .command('strain-plan')
93
+ .description('Suggest target training load from current recovery')
94
+ .action(async function strainPlanAction() {
95
+ try {
96
+ const globals = getGlobalOptions(this);
97
+ const client = new WhoopApiClient(globals.profile);
98
+ const [recovery] = await fetchRecoveries(client, { limit: 1, timeoutMs: globals.timeoutMs });
99
+ const [cycle] = await fetchCycles(client, { limit: 1, timeoutMs: globals.timeoutMs });
100
+ const recoveryScore = recovery?.score?.recovery_score;
101
+ const load = recommendLoad(recoveryScore);
102
+ const suggestedStrainRange = load.target === 'high' ? '14-18' : load.target === 'moderate' ? '10-14' : '0-9';
103
+ printData(this, {
104
+ recoveryScore: recoveryScore ?? null,
105
+ yesterdayStrain: cycle?.score?.strain ?? null,
106
+ recommendedLoad: load.target,
107
+ suggestedStrainRange,
108
+ note: load.note,
109
+ });
110
+ }
111
+ catch (err) {
112
+ printError(this, err);
113
+ }
114
+ });
115
+ };
@@ -0,0 +1,57 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { WhoopApiClient } from '../http/client.js';
3
+ import { fetchCycles, fetchRecoveries, fetchSleeps, fetchWorkouts } from '../http/whoop-data.js';
4
+ import { getGlobalOptions, printData, printError } from './context.js';
5
+ import { parseDateRange } from '../util/time.js';
6
+ import { usageError } from '../http/errors.js';
7
+ const jsonl = (records) => records.map((r) => JSON.stringify(r)).join('\n') + '\n';
8
+ export const registerSyncCommands = (program) => {
9
+ const sync = program.command('sync').description('Data export helpers');
10
+ sync
11
+ .command('pull')
12
+ .description('Pull WHOOP data and export as JSONL')
13
+ .requiredOption('--start <YYYY-MM-DD>')
14
+ .requiredOption('--end <YYYY-MM-DD>')
15
+ .requiredOption('--out <path>')
16
+ .option('--limit <n>', 'page size', '25')
17
+ .action(async function pullAction(opts) {
18
+ try {
19
+ const globals = getGlobalOptions(this);
20
+ const start = opts.start;
21
+ const end = opts.end;
22
+ const out = opts.out;
23
+ const range = parseDateRange({ start, end });
24
+ if (!range.start || !range.end) {
25
+ throw usageError('start and end are required for sync pull');
26
+ }
27
+ const client = new WhoopApiClient(globals.profile);
28
+ const limit = Number(opts.limit ?? 25);
29
+ const [recoveries, sleeps, cycles, workouts] = await Promise.all([
30
+ fetchRecoveries(client, { ...range, all: true, limit, timeoutMs: globals.timeoutMs }),
31
+ fetchSleeps(client, { ...range, all: true, limit, timeoutMs: globals.timeoutMs }),
32
+ fetchCycles(client, { ...range, all: true, limit, timeoutMs: globals.timeoutMs }),
33
+ fetchWorkouts(client, { ...range, all: true, limit, timeoutMs: globals.timeoutMs }),
34
+ ]);
35
+ const rows = [];
36
+ recoveries.forEach((r) => rows.push({ type: 'recovery', payload: r }));
37
+ sleeps.forEach((r) => rows.push({ type: 'sleep', payload: r }));
38
+ cycles.forEach((r) => rows.push({ type: 'cycle', payload: r }));
39
+ workouts.forEach((r) => rows.push({ type: 'workout', payload: r }));
40
+ await writeFile(out, jsonl(rows), 'utf8');
41
+ printData(this, {
42
+ out,
43
+ range,
44
+ counts: {
45
+ recoveries: recoveries.length,
46
+ sleeps: sleeps.length,
47
+ cycles: cycles.length,
48
+ workouts: workouts.length,
49
+ totalRows: rows.length,
50
+ },
51
+ });
52
+ }
53
+ catch (err) {
54
+ printError(this, err);
55
+ }
56
+ });
57
+ };
@@ -0,0 +1,38 @@
1
+ import { getGlobalOptions, printData, printError } from './context.js';
2
+ import { usageError } from '../http/errors.js';
3
+ import { readRawBodyFromFile, verifyWhoopSignature } from '../util/webhook-signature.js';
4
+ export const registerWebhookCommands = (program) => {
5
+ const webhook = program.command('webhook').description('Webhook helpers');
6
+ webhook
7
+ .command('verify')
8
+ .description('Verify WHOOP webhook signature')
9
+ .requiredOption('--secret <secret>', 'WHOOP app client secret')
10
+ .requiredOption('--timestamp <timestamp>', 'X-WHOOP-Signature-Timestamp header value')
11
+ .requiredOption('--signature <signature>', 'X-WHOOP-Signature header value')
12
+ .requiredOption('--body-file <path>', 'raw webhook body file path')
13
+ .action(async function verifyAction(opts) {
14
+ try {
15
+ getGlobalOptions(this);
16
+ const secret = String(opts.secret ?? '');
17
+ const timestamp = String(opts.timestamp ?? '');
18
+ const signature = String(opts.signature ?? '');
19
+ const bodyFile = String(opts.bodyFile ?? '');
20
+ if (!secret || !timestamp || !signature || !bodyFile) {
21
+ throw usageError('secret, timestamp, signature, and body-file are required');
22
+ }
23
+ const rawBody = await readRawBodyFromFile(bodyFile);
24
+ const valid = verifyWhoopSignature({
25
+ timestamp,
26
+ rawBody,
27
+ clientSecret: secret,
28
+ signature,
29
+ });
30
+ printData(this, {
31
+ valid,
32
+ });
33
+ }
34
+ catch (err) {
35
+ printError(this, err);
36
+ }
37
+ });
38
+ };
@@ -0,0 +1,77 @@
1
+ import { WhoopApiClient } from '../http/client.js';
2
+ import { fetchWorkouts } from '../http/whoop-data.js';
3
+ import { getGlobalOptions, printData, printError } from './context.js';
4
+ import { avg, round } from '../util/metrics.js';
5
+ import { parseDateRange, parseMaybeNumber } from '../util/time.js';
6
+ export const registerWorkoutCommands = (program) => {
7
+ const workout = program.command('workout').description('Workout commands');
8
+ workout
9
+ .command('list')
10
+ .description('List workouts')
11
+ .option('--start <YYYY-MM-DD>')
12
+ .option('--end <YYYY-MM-DD>')
13
+ .option('--days <n>', 'lookback days if start/end not provided', '14')
14
+ .option('--limit <n>', 'page size', '25')
15
+ .option('--all', 'follow pagination and fetch all pages')
16
+ .action(async function listAction(opts) {
17
+ try {
18
+ const globals = getGlobalOptions(this);
19
+ const client = new WhoopApiClient(globals.profile);
20
+ const range = parseDateRange({
21
+ start: opts.start,
22
+ end: opts.end,
23
+ days: parseMaybeNumber(opts.days),
24
+ });
25
+ const records = await fetchWorkouts(client, {
26
+ ...range,
27
+ limit: parseMaybeNumber(opts.limit),
28
+ all: Boolean(opts.all),
29
+ timeoutMs: globals.timeoutMs,
30
+ });
31
+ printData(this, {
32
+ count: records.length,
33
+ records,
34
+ });
35
+ }
36
+ catch (err) {
37
+ printError(this, err);
38
+ }
39
+ });
40
+ workout
41
+ .command('trend')
42
+ .description('Workout trend summary')
43
+ .option('--days <n>', 'lookback days', '14')
44
+ .option('--limit <n>', 'page size', '25')
45
+ .action(async function trendAction(opts) {
46
+ try {
47
+ const globals = getGlobalOptions(this);
48
+ const client = new WhoopApiClient(globals.profile);
49
+ const days = parseMaybeNumber(opts.days) ?? 14;
50
+ const records = await fetchWorkouts(client, {
51
+ ...parseDateRange({ days }),
52
+ limit: parseMaybeNumber(opts.limit),
53
+ all: true,
54
+ timeoutMs: globals.timeoutMs,
55
+ });
56
+ const avgStrain = round(avg(records.map((r) => r.score?.strain)), 2);
57
+ const avgHr = round(avg(records.map((r) => r.score?.average_heart_rate)), 1);
58
+ const bySport = records.reduce((acc, r) => {
59
+ const sport = r.sport_name ?? 'unknown';
60
+ acc[sport] = (acc[sport] ?? 0) + 1;
61
+ return acc;
62
+ }, {});
63
+ printData(this, {
64
+ windowDays: days,
65
+ workouts: records.length,
66
+ averages: {
67
+ strain: avgStrain,
68
+ averageHeartRate: avgHr,
69
+ },
70
+ bySport,
71
+ });
72
+ }
73
+ catch (err) {
74
+ printError(this, err);
75
+ }
76
+ });
77
+ };
@@ -0,0 +1,110 @@
1
+ import { authError, httpError, networkError } from './errors.js';
2
+ import { ensureFreshToken, refreshProfileToken } from '../auth/token-service.js';
3
+ export class WhoopApiClient {
4
+ profileName;
5
+ constructor(profileName) {
6
+ this.profileName = profileName;
7
+ }
8
+ buildUrl(profile, path, query) {
9
+ const url = new URL(path, profile.baseUrl);
10
+ if (query) {
11
+ Object.entries(query).forEach(([key, value]) => {
12
+ if (value === undefined || value === null)
13
+ return;
14
+ url.searchParams.set(key, String(value));
15
+ });
16
+ }
17
+ return url.toString();
18
+ }
19
+ async requestJson(opts) {
20
+ const profile = await ensureFreshToken(this.profileName);
21
+ return this.runRequest(profile, opts);
22
+ }
23
+ async runRequest(profile, opts) {
24
+ const token = profile.tokens?.accessToken;
25
+ if (!token) {
26
+ throw authError('Profile has no access token. Run whoop auth login.');
27
+ }
28
+ const url = this.buildUrl(profile, opts.path, opts.query);
29
+ const controller = new AbortController();
30
+ const timeout = setTimeout(() => controller.abort(), opts.timeoutMs);
31
+ try {
32
+ const res = await fetch(url, {
33
+ method: opts.method ?? 'GET',
34
+ headers: {
35
+ Authorization: `Bearer ${token}`,
36
+ 'content-type': opts.body ? 'application/json' : 'application/json',
37
+ },
38
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
39
+ signal: controller.signal,
40
+ });
41
+ const text = await res.text();
42
+ let parsed = text;
43
+ try {
44
+ parsed = text ? JSON.parse(text) : null;
45
+ }
46
+ catch {
47
+ // leave as text
48
+ }
49
+ if (res.status === 401 && (opts.retryOn401 ?? true)) {
50
+ const refreshed = await refreshProfileToken(this.profileName);
51
+ return this.runRequest(refreshed, { ...opts, retryOn401: false });
52
+ }
53
+ if (!res.ok) {
54
+ throw httpError(`WHOOP API request failed (${res.status})`, {
55
+ status: res.status,
56
+ path: opts.path,
57
+ response: parsed,
58
+ });
59
+ }
60
+ return parsed;
61
+ }
62
+ catch (err) {
63
+ if (err.name === 'AbortError') {
64
+ throw networkError('WHOOP API request timed out', {
65
+ timeoutMs: opts.timeoutMs,
66
+ path: opts.path,
67
+ });
68
+ }
69
+ if (err.name === 'WhoopCliError')
70
+ throw err;
71
+ throw networkError('WHOOP API network request failed', {
72
+ path: opts.path,
73
+ cause: err instanceof Error ? err.message : String(err),
74
+ });
75
+ }
76
+ finally {
77
+ clearTimeout(timeout);
78
+ }
79
+ }
80
+ async getCollection(path, opts) {
81
+ const records = [];
82
+ let nextToken;
83
+ let pages = 0;
84
+ const pageLimit = opts.all ? opts.maxPages ?? 200 : 1;
85
+ const normalizeDateBoundary = (value, kind) => {
86
+ if (!value)
87
+ return undefined;
88
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
89
+ return value;
90
+ return kind === 'start' ? `${value}T00:00:00.000Z` : `${value}T23:59:59.999Z`;
91
+ };
92
+ do {
93
+ const response = await this.requestJson({
94
+ path,
95
+ timeoutMs: opts.timeoutMs,
96
+ query: {
97
+ limit: opts.limit,
98
+ start: normalizeDateBoundary(opts.start, 'start'),
99
+ end: normalizeDateBoundary(opts.end, 'end'),
100
+ nextToken,
101
+ },
102
+ });
103
+ const current = response.records ?? [];
104
+ records.push(...current);
105
+ nextToken = response.next_token;
106
+ pages += 1;
107
+ } while (nextToken && pages < pageLimit);
108
+ return records;
109
+ }
110
+ }
@@ -0,0 +1,27 @@
1
+ export class WhoopCliError extends Error {
2
+ code;
3
+ exitCode;
4
+ details;
5
+ constructor(code, message, exitCode, details) {
6
+ super(message);
7
+ this.name = 'WhoopCliError';
8
+ this.code = code;
9
+ this.exitCode = exitCode;
10
+ this.details = details;
11
+ }
12
+ }
13
+ export const usageError = (message, details) => new WhoopCliError('USAGE_ERROR', message, 2, details);
14
+ export const configError = (message, details) => new WhoopCliError('CONFIG_ERROR', message, 2, details);
15
+ export const authError = (message, details) => new WhoopCliError('AUTH_ERROR', message, 3, details);
16
+ export const httpError = (message, details) => new WhoopCliError('HTTP_ERROR', message, 4, details);
17
+ export const networkError = (message, details) => new WhoopCliError('NETWORK_ERROR', message, 4, details);
18
+ export const notFoundError = (message, details) => new WhoopCliError('NOT_FOUND', message, 4, details);
19
+ export const featureUnavailableError = (message, details) => new WhoopCliError('FEATURE_UNAVAILABLE', message, 2, details);
20
+ export const normalizeError = (err) => {
21
+ if (err instanceof WhoopCliError)
22
+ return err;
23
+ if (err instanceof Error) {
24
+ return new WhoopCliError('INTERNAL_ERROR', err.message, 1);
25
+ }
26
+ return new WhoopCliError('INTERNAL_ERROR', String(err), 1);
27
+ };
@@ -0,0 +1,28 @@
1
+ export const fetchRecoveries = async (client, opts) => client.getCollection('/developer/v2/recovery', {
2
+ start: opts.start,
3
+ end: opts.end,
4
+ limit: opts.limit,
5
+ all: opts.all,
6
+ timeoutMs: opts.timeoutMs,
7
+ });
8
+ export const fetchSleeps = async (client, opts) => client.getCollection('/developer/v2/activity/sleep', {
9
+ start: opts.start,
10
+ end: opts.end,
11
+ limit: opts.limit,
12
+ all: opts.all,
13
+ timeoutMs: opts.timeoutMs,
14
+ });
15
+ export const fetchCycles = async (client, opts) => client.getCollection('/developer/v2/cycle', {
16
+ start: opts.start,
17
+ end: opts.end,
18
+ limit: opts.limit,
19
+ all: opts.all,
20
+ timeoutMs: opts.timeoutMs,
21
+ });
22
+ export const fetchWorkouts = async (client, opts) => client.getCollection('/developer/v2/activity/workout', {
23
+ start: opts.start,
24
+ end: opts.end,
25
+ limit: opts.limit,
26
+ all: opts.all,
27
+ timeoutMs: opts.timeoutMs,
28
+ });
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { program } from './cli.js';
3
+ program.parseAsync(process.argv).catch((err) => {
4
+ const message = err instanceof Error ? err.message : String(err);
5
+ console.error(message);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { normalizeError } from '../http/errors.js';
2
+ export const ok = (data) => ({ data, error: null });
3
+ export const fail = (code, message, details) => ({
4
+ data: null,
5
+ error: {
6
+ code,
7
+ message,
8
+ details,
9
+ },
10
+ });
11
+ export const fromError = (err) => {
12
+ const e = normalizeError(err);
13
+ return fail(e.code, e.message, e.details);
14
+ };
15
+ export const stringifyEnvelope = (envelope, pretty = false) => JSON.stringify(envelope, null, pretty ? 2 : 0);