@insuline/whoop-cli 0.1.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 (50) hide show
  1. package/README.md +183 -0
  2. package/dist/api/client.d.ts +18 -0
  3. package/dist/api/client.d.ts.map +1 -0
  4. package/dist/api/client.js +96 -0
  5. package/dist/api/client.js.map +1 -0
  6. package/dist/api/endpoints.d.ts +10 -0
  7. package/dist/api/endpoints.d.ts.map +1 -0
  8. package/dist/api/endpoints.js +10 -0
  9. package/dist/api/endpoints.js.map +1 -0
  10. package/dist/auth/oauth.d.ts +9 -0
  11. package/dist/auth/oauth.d.ts.map +1 -0
  12. package/dist/auth/oauth.js +122 -0
  13. package/dist/auth/oauth.js.map +1 -0
  14. package/dist/auth/server.d.ts +7 -0
  15. package/dist/auth/server.d.ts.map +1 -0
  16. package/dist/auth/server.js +53 -0
  17. package/dist/auth/server.js.map +1 -0
  18. package/dist/auth/tokens.d.ts +12 -0
  19. package/dist/auth/tokens.d.ts.map +1 -0
  20. package/dist/auth/tokens.js +102 -0
  21. package/dist/auth/tokens.js.map +1 -0
  22. package/dist/cli.d.ts +3 -0
  23. package/dist/cli.d.ts.map +1 -0
  24. package/dist/cli.js +191 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +6 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/types/whoop.d.ts +159 -0
  31. package/dist/types/whoop.d.ts.map +1 -0
  32. package/dist/types/whoop.js +2 -0
  33. package/dist/types/whoop.js.map +1 -0
  34. package/dist/utils/analysis.d.ts +30 -0
  35. package/dist/utils/analysis.d.ts.map +1 -0
  36. package/dist/utils/analysis.js +231 -0
  37. package/dist/utils/analysis.js.map +1 -0
  38. package/dist/utils/date.d.ts +10 -0
  39. package/dist/utils/date.d.ts.map +1 -0
  40. package/dist/utils/date.js +38 -0
  41. package/dist/utils/date.js.map +1 -0
  42. package/dist/utils/errors.d.ts +14 -0
  43. package/dist/utils/errors.d.ts.map +1 -0
  44. package/dist/utils/errors.js +36 -0
  45. package/dist/utils/errors.js.map +1 -0
  46. package/dist/utils/format.d.ts +5 -0
  47. package/dist/utils/format.d.ts.map +1 -0
  48. package/dist/utils/format.js +89 -0
  49. package/dist/utils/format.js.map +1 -0
  50. package/package.json +55 -0
package/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # WHOOP Skill
2
+
3
+ [![npm version](https://img.shields.io/npm/v/whoop-cli.svg)](https://www.npmjs.com/package/whoop-cli)
4
+
5
+ CLI for fetching WHOOP health data via the WHOOP API v2.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g whoop-cli
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # One-liner health snapshot
17
+ whoop-cli summary
18
+ # Output: 2026-01-05 | Recovery: 52% | HRV: 39ms | RHR: 60 | Sleep: 40% | Strain: 6.7
19
+
20
+ # Human-readable output
21
+ whoop-cli --pretty
22
+
23
+ # JSON output (default)
24
+ whoop-cli
25
+ ```
26
+
27
+ ## Setup
28
+
29
+ Before using, you need to configure WHOOP API credentials:
30
+
31
+ 1. Register a WHOOP application at [developer.whoop.com](https://developer.whoop.com)
32
+ - Apps with <10 users don't need WHOOP review (immediate use)
33
+
34
+ 2. Set environment variables:
35
+ ```bash
36
+ export WHOOP_CLIENT_ID=your_client_id
37
+ export WHOOP_CLIENT_SECRET=your_client_secret
38
+ export WHOOP_REDIRECT_URI=https://your-redirect-uri.com/callback
39
+ ```
40
+
41
+ Or create a `.env` file in your working directory.
42
+
43
+ 3. Authenticate:
44
+ ```bash
45
+ whoop-cli auth login
46
+ ```
47
+
48
+ Tokens are stored in `~/.whoop-cli/tokens.json` and auto-refresh when expired.
49
+
50
+ ## Usage
51
+
52
+ ```bash
53
+ # Fetch all today's data
54
+ whoop-cli
55
+
56
+ # One-liner health snapshot
57
+ whoop-cli summary
58
+
59
+ # Human-readable output
60
+ whoop-cli --pretty
61
+
62
+ # Specific data type
63
+ whoop-cli profile
64
+ whoop-cli body
65
+ whoop-cli sleep
66
+ whoop-cli recovery
67
+ whoop-cli workout
68
+ whoop-cli cycle
69
+
70
+ # Multiple types
71
+ whoop-cli --sleep --recovery --body
72
+
73
+ # Specific date (ISO format)
74
+ whoop-cli --date 2025-01-03
75
+
76
+ # Pagination
77
+ whoop-cli workout --limit 50
78
+ whoop-cli workout --all
79
+ ```
80
+
81
+ ## Auth Commands
82
+
83
+ ```bash
84
+ whoop-cli auth login # OAuth flow (opens browser)
85
+ whoop-cli auth status # Check token status
86
+ whoop-cli auth refresh # Refresh access token using refresh token
87
+ whoop-cli auth logout # Clear tokens
88
+ ```
89
+
90
+ ## Keeping tokens fresh (recommended for cron/servers)
91
+
92
+ If you run `whoop-cli` from cron/systemd, you may occasionally see authentication failures if a token refresh is missed or the token file becomes stale.
93
+
94
+ Important:
95
+ - `whoop-cli auth status` **does not refresh tokens** — it only reports whether they’re expired.
96
+ - For automation, you must call `whoop-cli auth refresh` periodically.
97
+
98
+ Recommended pattern:
99
+ - Run `whoop-cli auth login` once interactively (creates `~/.whoop-cli/tokens.json`).
100
+ - Run a small periodic monitor that calls `whoop-cli auth refresh` and performs a lightweight fetch.
101
+
102
+ An example monitor script + systemd timer/cron examples are included here:
103
+ - `examples/monitor/whoop-refresh-monitor.sh`
104
+ - `examples/monitor/systemd/*`
105
+ - `examples/monitor/cron/README-cron.txt`
106
+
107
+ If refresh fails with an expired refresh token, you must re-authenticate:
108
+
109
+ ```bash
110
+ whoop-cli auth login
111
+ ```
112
+
113
+ ## Data Types
114
+
115
+ | Type | Description |
116
+ |------|-------------|
117
+ | `profile` | User info (name, email) |
118
+ | `body` | Body measurements (height, weight, max HR) |
119
+ | `sleep` | Sleep records with stages, efficiency, respiratory rate |
120
+ | `recovery` | Recovery score, HRV, RHR, SpO2, skin temp |
121
+ | `workout` | Workouts with strain, HR zones, calories |
122
+ | `cycle` | Daily physiological cycle (strain, calories) |
123
+
124
+ ## Options
125
+
126
+ | Flag | Description |
127
+ |------|-------------|
128
+ | `-d, --date <date>` | Date in ISO format (YYYY-MM-DD) |
129
+ | `-l, --limit <n>` | Max results per page (default: 25) |
130
+ | `-a, --all` | Fetch all pages |
131
+ | `-p, --pretty` | Human-readable output |
132
+ | `--profile` | Include profile |
133
+ | `--body` | Include body measurements |
134
+ | `--sleep` | Include sleep |
135
+ | `--recovery` | Include recovery |
136
+ | `--workout` | Include workouts |
137
+ | `--cycle` | Include cycle |
138
+
139
+ ## Output
140
+
141
+ JSON to stdout by default. Use `--pretty` for human-readable format.
142
+
143
+ ```json
144
+ {
145
+ "date": "2025-01-05",
146
+ "fetched_at": "2025-01-05T12:00:00.000Z",
147
+ "profile": { "user_id": 123, "first_name": "John" },
148
+ "body": { "height_meter": 1.83, "weight_kilogram": 82.5, "max_heart_rate": 182 },
149
+ "recovery": [{ "score": { "recovery_score": 52, "hrv_rmssd_milli": 38.9 }}],
150
+ "sleep": [{ "score": { "sleep_performance_percentage": 40 }}],
151
+ "workout": [{ "sport_name": "hiit", "score": { "strain": 6.2 }}],
152
+ "cycle": [{ "score": { "strain": 6.7 }}]
153
+ }
154
+ ```
155
+
156
+ ## Exit Codes
157
+
158
+ | Code | Meaning |
159
+ |------|---------|
160
+ | 0 | Success |
161
+ | 1 | General error |
162
+ | 2 | Authentication error |
163
+ | 3 | Rate limit exceeded |
164
+ | 4 | Network error |
165
+
166
+ ## Requirements
167
+
168
+ - Node.js 22+
169
+ - WHOOP membership with API access
170
+
171
+ ## Development
172
+
173
+ ```bash
174
+ git clone https://github.com/insulineru/whoop-cli.git
175
+ cd whoop-cli
176
+ npm install
177
+ npm run dev # Run with tsx
178
+ npm run build # Compile TypeScript
179
+ ```
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,18 @@
1
+ import type { WhoopProfile, WhoopBody, WhoopSleep, WhoopRecovery, WhoopWorkout, WhoopCycle, QueryParams, CombinedOutput, DataType } from '../types/whoop.js';
2
+ export declare function getProfile(): Promise<WhoopProfile>;
3
+ export declare function getBody(): Promise<WhoopBody>;
4
+ export declare function getSleep(params?: QueryParams, all?: boolean): Promise<WhoopSleep[]>;
5
+ export declare function getRecovery(params?: QueryParams, all?: boolean): Promise<WhoopRecovery[]>;
6
+ export declare function getWorkout(params?: QueryParams, all?: boolean): Promise<WhoopWorkout[]>;
7
+ export declare function getCycle(params?: QueryParams, all?: boolean): Promise<WhoopCycle[]>;
8
+ export declare function fetchData(types: DataType[], date: string, options?: {
9
+ limit?: number;
10
+ all?: boolean;
11
+ start?: string;
12
+ end?: string;
13
+ }): Promise<CombinedOutput>;
14
+ export declare function fetchAllTypes(date: string, options?: {
15
+ limit?: number;
16
+ all?: boolean;
17
+ }): Promise<CombinedOutput>;
18
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,UAAU,EACV,aAAa,EACb,YAAY,EACZ,UAAU,EAEV,WAAW,EACX,cAAc,EACd,QAAQ,EACT,MAAM,mBAAmB,CAAC;AAiD3B,wBAAsB,UAAU,IAAI,OAAO,CAAC,YAAY,CAAC,CAExD;AAED,wBAAsB,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC,CAElD;AAED,wBAAsB,QAAQ,CAAC,MAAM,GAAE,WAAgB,EAAE,GAAG,UAAQ,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAE3F;AAED,wBAAsB,WAAW,CAAC,MAAM,GAAE,WAAgB,EAAE,GAAG,UAAQ,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAEjG;AAED,wBAAsB,UAAU,CAAC,MAAM,GAAE,WAAgB,EAAE,GAAG,UAAQ,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAE/F;AAED,wBAAsB,QAAQ,CAAC,MAAM,GAAE,WAAgB,EAAE,GAAG,UAAQ,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAE3F;AAED,wBAAsB,SAAS,CAC7B,KAAK,EAAE,QAAQ,EAAE,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5E,OAAO,CAAC,cAAc,CAAC,CAmCzB;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9C,OAAO,CAAC,cAAc,CAAC,CAEzB"}
@@ -0,0 +1,96 @@
1
+ import { getValidTokens } from '../auth/tokens.js';
2
+ import { BASE_URL, ENDPOINTS } from './endpoints.js';
3
+ import { WhoopError, ExitCode } from '../utils/errors.js';
4
+ import { getDateRange, nowISO } from '../utils/date.js';
5
+ async function request(endpoint, params) {
6
+ const tokens = await getValidTokens();
7
+ const url = new URL(BASE_URL + endpoint);
8
+ if (params?.start)
9
+ url.searchParams.set('start', params.start);
10
+ if (params?.end)
11
+ url.searchParams.set('end', params.end);
12
+ if (params?.limit)
13
+ url.searchParams.set('limit', String(params.limit));
14
+ if (params?.nextToken)
15
+ url.searchParams.set('nextToken', params.nextToken);
16
+ const response = await fetch(url.toString(), {
17
+ headers: {
18
+ Authorization: `Bearer ${tokens.access_token}`,
19
+ 'Content-Type': 'application/json',
20
+ },
21
+ });
22
+ if (!response.ok) {
23
+ if (response.status === 401) {
24
+ throw new WhoopError('Authentication failed', ExitCode.AUTH_ERROR, 401);
25
+ }
26
+ if (response.status === 429) {
27
+ throw new WhoopError('Rate limit exceeded', ExitCode.RATE_LIMIT, 429);
28
+ }
29
+ throw new WhoopError(`API request failed`, ExitCode.GENERAL_ERROR, response.status);
30
+ }
31
+ return response.json();
32
+ }
33
+ async function fetchAll(endpoint, params, fetchAllPages) {
34
+ const results = [];
35
+ let nextToken;
36
+ do {
37
+ const response = await request(endpoint, { ...params, nextToken });
38
+ results.push(...response.records);
39
+ nextToken = fetchAllPages ? response.next_token : undefined;
40
+ } while (nextToken);
41
+ return results;
42
+ }
43
+ export async function getProfile() {
44
+ return request(ENDPOINTS.profile);
45
+ }
46
+ export async function getBody() {
47
+ return request(ENDPOINTS.body);
48
+ }
49
+ export async function getSleep(params = {}, all = false) {
50
+ return fetchAll(ENDPOINTS.sleep, { limit: 25, ...params }, all);
51
+ }
52
+ export async function getRecovery(params = {}, all = false) {
53
+ return fetchAll(ENDPOINTS.recovery, { limit: 25, ...params }, all);
54
+ }
55
+ export async function getWorkout(params = {}, all = false) {
56
+ return fetchAll(ENDPOINTS.workout, { limit: 25, ...params }, all);
57
+ }
58
+ export async function getCycle(params = {}, all = false) {
59
+ return fetchAll(ENDPOINTS.cycle, { limit: 25, ...params }, all);
60
+ }
61
+ export async function fetchData(types, date, options = {}) {
62
+ const { start, end } = options.start && options.end
63
+ ? { start: options.start, end: options.end }
64
+ : getDateRange(date);
65
+ const params = { start, end, limit: options.limit };
66
+ const output = {
67
+ date,
68
+ fetched_at: nowISO(),
69
+ };
70
+ const fetchers = {
71
+ profile: async () => {
72
+ output.profile = await getProfile();
73
+ },
74
+ body: async () => {
75
+ output.body = await getBody();
76
+ },
77
+ sleep: async () => {
78
+ output.sleep = await getSleep(params, options.all);
79
+ },
80
+ recovery: async () => {
81
+ output.recovery = await getRecovery(params, options.all);
82
+ },
83
+ workout: async () => {
84
+ output.workout = await getWorkout(params, options.all);
85
+ },
86
+ cycle: async () => {
87
+ output.cycle = await getCycle(params, options.all);
88
+ },
89
+ };
90
+ await Promise.all(types.map((type) => fetchers[type]()));
91
+ return output;
92
+ }
93
+ export async function fetchAllTypes(date, options = {}) {
94
+ return fetchData(['profile', 'body', 'sleep', 'recovery', 'workout', 'cycle'], date, options);
95
+ }
96
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export declare const BASE_URL = "https://api.prod.whoop.com/developer/v2";
2
+ export declare const ENDPOINTS: {
3
+ readonly profile: "/user/profile/basic";
4
+ readonly body: "/user/measurement/body";
5
+ readonly workout: "/activity/workout";
6
+ readonly sleep: "/activity/sleep";
7
+ readonly recovery: "/recovery";
8
+ readonly cycle: "/cycle";
9
+ };
10
+ //# sourceMappingURL=endpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoints.d.ts","sourceRoot":"","sources":["../../src/api/endpoints.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,QAAQ,4CAA4C,CAAC;AAElE,eAAO,MAAM,SAAS;;;;;;;CAOZ,CAAC"}
@@ -0,0 +1,10 @@
1
+ export const BASE_URL = 'https://api.prod.whoop.com/developer/v2';
2
+ export const ENDPOINTS = {
3
+ profile: '/user/profile/basic',
4
+ body: '/user/measurement/body',
5
+ workout: '/activity/workout',
6
+ sleep: '/activity/sleep',
7
+ recovery: '/recovery',
8
+ cycle: '/cycle',
9
+ };
10
+ //# sourceMappingURL=endpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoints.js","sourceRoot":"","sources":["../../src/api/endpoints.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,QAAQ,GAAG,yCAAyC,CAAC;AAElE,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,OAAO,EAAE,qBAAqB;IAC9B,IAAI,EAAE,wBAAwB;IAC9B,OAAO,EAAE,mBAAmB;IAC5B,KAAK,EAAE,iBAAiB;IACxB,QAAQ,EAAE,WAAW;IACrB,KAAK,EAAE,QAAQ;CACP,CAAC"}
@@ -0,0 +1,9 @@
1
+ export declare function login(): Promise<void>;
2
+ export declare function logout(): void;
3
+ export declare function status(): void;
4
+ /**
5
+ * Proactively refresh the access token.
6
+ * Use this in cron jobs to keep tokens fresh.
7
+ */
8
+ export declare function refresh(): Promise<void>;
9
+ //# sourceMappingURL=oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"AAoCA,wBAAsB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAsD3C;AAED,wBAAgB,MAAM,IAAI,IAAI,CAG7B;AAED,wBAAgB,MAAM,IAAI,IAAI,CAoB7B;AAED;;;GAGG;AACH,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CA6B7C"}
@@ -0,0 +1,122 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { createInterface } from 'node:readline';
3
+ import open from 'open';
4
+ import { saveTokens, clearTokens, getTokenStatus, getValidTokens, isTokenExpired, loadTokens } from './tokens.js';
5
+ import { WhoopError, ExitCode } from '../utils/errors.js';
6
+ const WHOOP_AUTH_URL = 'https://api.prod.whoop.com/oauth/oauth2/auth';
7
+ const WHOOP_TOKEN_URL = 'https://api.prod.whoop.com/oauth/oauth2/token';
8
+ const SCOPES = 'read:profile read:body_measurement read:workout read:recovery read:sleep read:cycles offline';
9
+ function getCredentials() {
10
+ const clientId = process.env.WHOOP_CLIENT_ID;
11
+ const clientSecret = process.env.WHOOP_CLIENT_SECRET;
12
+ const redirectUri = process.env.WHOOP_REDIRECT_URI;
13
+ if (!clientId || !clientSecret || !redirectUri) {
14
+ throw new WhoopError('Missing WHOOP_CLIENT_ID, WHOOP_CLIENT_SECRET, or WHOOP_REDIRECT_URI in environment', ExitCode.AUTH_ERROR);
15
+ }
16
+ return { clientId, clientSecret, redirectUri };
17
+ }
18
+ function prompt(question) {
19
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
20
+ return new Promise((resolve) => {
21
+ rl.question(question, (answer) => {
22
+ rl.close();
23
+ resolve(answer.trim());
24
+ });
25
+ });
26
+ }
27
+ export async function login() {
28
+ const { clientId, clientSecret, redirectUri } = getCredentials();
29
+ const state = randomBytes(16).toString('hex');
30
+ const authUrl = new URL(WHOOP_AUTH_URL);
31
+ authUrl.searchParams.set('client_id', clientId);
32
+ authUrl.searchParams.set('redirect_uri', redirectUri);
33
+ authUrl.searchParams.set('response_type', 'code');
34
+ authUrl.searchParams.set('scope', SCOPES);
35
+ authUrl.searchParams.set('state', state);
36
+ console.log('Opening browser for authorization...');
37
+ console.log('\nIf browser does not open, visit this URL:\n');
38
+ console.log(authUrl.toString());
39
+ console.log('');
40
+ await open(authUrl.toString()).catch(() => { });
41
+ const callbackUrl = await prompt('Paste the callback URL here: ');
42
+ const url = new URL(callbackUrl);
43
+ const code = url.searchParams.get('code');
44
+ const returnedState = url.searchParams.get('state');
45
+ if (!code) {
46
+ throw new WhoopError('No authorization code in callback URL', ExitCode.AUTH_ERROR);
47
+ }
48
+ if (returnedState !== state) {
49
+ throw new WhoopError('OAuth state mismatch', ExitCode.AUTH_ERROR);
50
+ }
51
+ const tokenResponse = await fetch(WHOOP_TOKEN_URL, {
52
+ method: 'POST',
53
+ headers: {
54
+ 'Content-Type': 'application/x-www-form-urlencoded',
55
+ },
56
+ body: new URLSearchParams({
57
+ grant_type: 'authorization_code',
58
+ code,
59
+ redirect_uri: redirectUri,
60
+ client_id: clientId,
61
+ client_secret: clientSecret,
62
+ }),
63
+ });
64
+ if (!tokenResponse.ok) {
65
+ const text = await tokenResponse.text();
66
+ throw new WhoopError(`Token exchange failed: ${text}`, ExitCode.AUTH_ERROR, tokenResponse.status);
67
+ }
68
+ const tokens = (await tokenResponse.json());
69
+ saveTokens(tokens);
70
+ console.log('Authentication successful');
71
+ }
72
+ export function logout() {
73
+ clearTokens();
74
+ console.log('Logged out');
75
+ }
76
+ export function status() {
77
+ const tokenStatus = getTokenStatus();
78
+ const tokens = loadTokens();
79
+ if (!tokenStatus.authenticated) {
80
+ console.log(JSON.stringify({ authenticated: false, message: 'Not logged in. Run: whoop-cli auth login' }, null, 2));
81
+ return;
82
+ }
83
+ const now = Math.floor(Date.now() / 1000);
84
+ const expiresIn = tokenStatus.expires_at - now;
85
+ const needsRefresh = isTokenExpired(tokens);
86
+ console.log(JSON.stringify({
87
+ authenticated: true,
88
+ expires_at: tokenStatus.expires_at,
89
+ expires_in_seconds: expiresIn,
90
+ expires_in_human: expiresIn > 0 ? `${Math.floor(expiresIn / 60)} minutes` : 'EXPIRED',
91
+ needs_refresh: needsRefresh,
92
+ }, null, 2));
93
+ }
94
+ /**
95
+ * Proactively refresh the access token.
96
+ * Use this in cron jobs to keep tokens fresh.
97
+ */
98
+ export async function refresh() {
99
+ const tokens = loadTokens();
100
+ if (!tokens) {
101
+ throw new WhoopError('Not authenticated. Run: whoop-cli auth login', ExitCode.AUTH_ERROR);
102
+ }
103
+ try {
104
+ const newTokens = await getValidTokens();
105
+ const now = Math.floor(Date.now() / 1000);
106
+ const expiresIn = newTokens.expires_at - now;
107
+ console.log(JSON.stringify({
108
+ success: true,
109
+ message: 'Token refreshed successfully',
110
+ expires_at: newTokens.expires_at,
111
+ expires_in_seconds: expiresIn,
112
+ expires_in_human: `${Math.floor(expiresIn / 60)} minutes`,
113
+ }, null, 2));
114
+ }
115
+ catch (error) {
116
+ if (error instanceof WhoopError && error.message.includes('refresh')) {
117
+ throw new WhoopError('Refresh token expired. Please re-authenticate with: whoop-cli auth login', ExitCode.AUTH_ERROR);
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export interface CallbackResult {
2
+ code: string;
3
+ state: string;
4
+ }
5
+ export declare function findAvailablePort(startPort?: number): Promise<number>;
6
+ export declare function startCallbackServer(port: number): Promise<CallbackResult>;
7
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/auth/server.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,iBAAiB,CAAC,SAAS,GAAE,MAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAUjF;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CA8CzE"}
@@ -0,0 +1,53 @@
1
+ import { createServer } from 'node:http';
2
+ import { URL } from 'node:url';
3
+ export async function findAvailablePort(startPort = 3000) {
4
+ return new Promise((resolve) => {
5
+ const server = createServer();
6
+ server.listen(startPort, () => {
7
+ server.close(() => resolve(startPort));
8
+ });
9
+ server.on('error', () => {
10
+ resolve(findAvailablePort(startPort + 1));
11
+ });
12
+ });
13
+ }
14
+ export function startCallbackServer(port) {
15
+ return new Promise((resolve, reject) => {
16
+ let server;
17
+ const timeout = setTimeout(() => {
18
+ server?.close();
19
+ reject(new Error('OAuth callback timeout'));
20
+ }, 120000);
21
+ server = createServer((req, res) => {
22
+ const url = new URL(req.url || '/', `http://localhost:${port}`);
23
+ if (url.pathname === '/callback') {
24
+ const code = url.searchParams.get('code');
25
+ const state = url.searchParams.get('state');
26
+ const error = url.searchParams.get('error');
27
+ if (error) {
28
+ res.writeHead(400, { 'Content-Type': 'text/html' });
29
+ res.end('<h1>Authorization Failed</h1><p>You can close this window.</p>');
30
+ clearTimeout(timeout);
31
+ server.close();
32
+ reject(new Error(`OAuth error: ${error}`));
33
+ return;
34
+ }
35
+ if (code && state) {
36
+ res.writeHead(200, { 'Content-Type': 'text/html' });
37
+ res.end('<h1>Authorization Successful</h1><p>You can close this window.</p>');
38
+ clearTimeout(timeout);
39
+ server.close();
40
+ resolve({ code, state });
41
+ return;
42
+ }
43
+ res.writeHead(400, { 'Content-Type': 'text/html' });
44
+ res.end('<h1>Missing Parameters</h1>');
45
+ return;
46
+ }
47
+ res.writeHead(404);
48
+ res.end('Not Found');
49
+ });
50
+ server.listen(port);
51
+ });
52
+ }
53
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/auth/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { TokenData, OAuthTokenResponse } from '../types/whoop.js';
2
+ export declare function saveTokens(response: OAuthTokenResponse): void;
3
+ export declare function loadTokens(): TokenData | null;
4
+ export declare function clearTokens(): void;
5
+ export declare function isTokenExpired(tokens: TokenData): boolean;
6
+ export declare function refreshAccessToken(tokens: TokenData): Promise<TokenData>;
7
+ export declare function getValidTokens(): Promise<TokenData>;
8
+ export declare function getTokenStatus(): {
9
+ authenticated: boolean;
10
+ expires_at?: number;
11
+ };
12
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAevE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAa7D;AAED,wBAAgB,UAAU,IAAI,SAAS,GAAG,IAAI,CAW7C;AAED,wBAAgB,WAAW,IAAI,IAAI,CAIlC;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAGzD;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAqC9E;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,SAAS,CAAC,CAYzD;AAED,wBAAgB,cAAc,IAAI;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAShF"}