whoop-cli 1.2.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.
- package/.env.example +4 -0
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/dist/api/client.d.ts +19 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +99 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/endpoints.d.ts +10 -0
- package/dist/api/endpoints.d.ts.map +1 -0
- package/dist/api/endpoints.js +10 -0
- package/dist/api/endpoints.js.map +1 -0
- package/dist/auth/oauth.d.ts +9 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +122 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/server.d.ts +7 -0
- package/dist/auth/server.d.ts.map +1 -0
- package/dist/auth/server.js +53 -0
- package/dist/auth/server.js.map +1 -0
- package/dist/auth/tokens.d.ts +12 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +102 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +251 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/types/whoop.d.ts +159 -0
- package/dist/types/whoop.d.ts.map +1 -0
- package/dist/types/whoop.js +2 -0
- package/dist/types/whoop.js.map +1 -0
- package/dist/utils/analysis.d.ts +30 -0
- package/dist/utils/analysis.d.ts.map +1 -0
- package/dist/utils/analysis.js +231 -0
- package/dist/utils/analysis.js.map +1 -0
- package/dist/utils/date.d.ts +10 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +38 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/errors.d.ts +14 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +36 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/format.d.ts +14 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +241 -0
- package/dist/utils/format.js.map +1 -0
- package/package.json +58 -0
package/.env.example
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 xonika9
|
|
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,182 @@
|
|
|
1
|
+
# whoop-cli
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/whoop-cli)
|
|
4
|
+
|
|
5
|
+
CLI for fetching WHOOP health data via the WHOOP API v2.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g whoop-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
whoop-cli auth login # Authenticate via browser
|
|
15
|
+
whoop-cli summary # One-liner health snapshot
|
|
16
|
+
whoop-cli dashboard # Full health dashboard with 7-day trends
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
1. Register a WHOOP application at [developer.whoop.com](https://developer.whoop.com)
|
|
22
|
+
- Apps with <10 users don't need WHOOP review (immediate use)
|
|
23
|
+
|
|
24
|
+
2. Set environment variables:
|
|
25
|
+
```bash
|
|
26
|
+
export WHOOP_CLIENT_ID=your_client_id
|
|
27
|
+
export WHOOP_CLIENT_SECRET=your_client_secret
|
|
28
|
+
export WHOOP_REDIRECT_URI=https://your-redirect-uri.com/callback
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or create a `.env` file in your working directory.
|
|
32
|
+
|
|
33
|
+
3. Authenticate:
|
|
34
|
+
```bash
|
|
35
|
+
whoop-cli auth login
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Tokens are stored in `~/.whoop-cli/tokens.json` and auto-refresh when expired.
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
### Data Commands
|
|
43
|
+
|
|
44
|
+
| Command | Description |
|
|
45
|
+
| -------------------- | -------------------------------------------- |
|
|
46
|
+
| `whoop-cli sleep` | Sleep stages, efficiency, respiratory rate |
|
|
47
|
+
| `whoop-cli recovery` | Recovery score, HRV, RHR, SpO2, skin temp |
|
|
48
|
+
| `whoop-cli workout` | Workouts with strain, HR zones, calories |
|
|
49
|
+
| `whoop-cli cycle` | Daily physiological cycle (strain, calories) |
|
|
50
|
+
| `whoop-cli profile` | User info (name, email) |
|
|
51
|
+
| `whoop-cli body` | Body measurements (height, weight, max HR) |
|
|
52
|
+
|
|
53
|
+
Data commands output JSON by default. Use `--pretty` for human-readable format.
|
|
54
|
+
|
|
55
|
+
### Analysis Commands
|
|
56
|
+
|
|
57
|
+
| Command | Description |
|
|
58
|
+
| --------------------------- | ---------------------------------------------- |
|
|
59
|
+
| `whoop-cli summary` | One-liner: Recovery, HRV, RHR, sleep, strain |
|
|
60
|
+
| `whoop-cli summary --color` | Color-coded with 🟢🟡🔴 status indicators |
|
|
61
|
+
| `whoop-cli dashboard` | Full health dashboard with 7-day trends |
|
|
62
|
+
| `whoop-cli trends` | Multi-day trend analysis with direction arrows |
|
|
63
|
+
| `whoop-cli insights` | Health recommendations based on your data |
|
|
64
|
+
|
|
65
|
+
Analysis commands output pretty format by default. Use `--json` for raw JSON.
|
|
66
|
+
|
|
67
|
+
### Auth Commands
|
|
68
|
+
|
|
69
|
+
| Command | Description |
|
|
70
|
+
| ------------------------ | ---------------------------------------- |
|
|
71
|
+
| `whoop-cli auth login` | OAuth flow (opens browser) |
|
|
72
|
+
| `whoop-cli auth status` | Check token status (does not refresh) |
|
|
73
|
+
| `whoop-cli auth refresh` | Refresh access token using refresh token |
|
|
74
|
+
| `whoop-cli auth logout` | Clear stored tokens |
|
|
75
|
+
|
|
76
|
+
## Options
|
|
77
|
+
|
|
78
|
+
### Data command flags
|
|
79
|
+
|
|
80
|
+
| Flag | Description |
|
|
81
|
+
| -------------------- | ---------------------------------- |
|
|
82
|
+
| `-d, --date <date>` | Date in ISO format (YYYY-MM-DD) |
|
|
83
|
+
| `-s, --start <date>` | Start date for range query |
|
|
84
|
+
| `-e, --end <date>` | End date for range query |
|
|
85
|
+
| `-l, --limit <n>` | Max results per page (default: 25) |
|
|
86
|
+
| `-a, --all` | Fetch all pages |
|
|
87
|
+
| `-p, --pretty` | Human-readable output with emojis |
|
|
88
|
+
|
|
89
|
+
### Analysis command flags
|
|
90
|
+
|
|
91
|
+
| Flag | Applies to | Description |
|
|
92
|
+
| --------------------- | ---------------------------- | ----------------------------------------- |
|
|
93
|
+
| `-d, --date <date>` | summary, dashboard, insights | Date in ISO format |
|
|
94
|
+
| `-n, --days <number>` | trends | Number of days: 7, 14, or 30 only |
|
|
95
|
+
| `--json` | dashboard, trends, insights | Output raw JSON |
|
|
96
|
+
| `-c, --color` | summary | Color-coded output with status indicators |
|
|
97
|
+
|
|
98
|
+
### Global flags (combine data types)
|
|
99
|
+
|
|
100
|
+
| Flag | Description |
|
|
101
|
+
| ------------ | ------------------------- |
|
|
102
|
+
| `--sleep` | Include sleep data |
|
|
103
|
+
| `--recovery` | Include recovery data |
|
|
104
|
+
| `--workout` | Include workout data |
|
|
105
|
+
| `--cycle` | Include cycle data |
|
|
106
|
+
| `--profile` | Include profile data |
|
|
107
|
+
| `--body` | Include body measurements |
|
|
108
|
+
|
|
109
|
+
Usage: `whoop-cli --sleep --recovery --body`
|
|
110
|
+
|
|
111
|
+
Running `whoop-cli` with no arguments fetches all data types.
|
|
112
|
+
|
|
113
|
+
## Output
|
|
114
|
+
|
|
115
|
+
Data commands output JSON to stdout by default:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"date": "2025-01-05",
|
|
120
|
+
"fetched_at": "2025-01-05T12:00:00.000Z",
|
|
121
|
+
"profile": { "user_id": 123, "first_name": "John" },
|
|
122
|
+
"body": { "height_meter": 1.83, "weight_kilogram": 82.5, "max_heart_rate": 182 },
|
|
123
|
+
"recovery": [{ "score": { "recovery_score": 52, "hrv_rmssd_milli": 38.9 }}],
|
|
124
|
+
"sleep": [{ "score": { "sleep_performance_percentage": 40 }}],
|
|
125
|
+
"workout": [{ "sport_name": "hiit", "score": { "strain": 6.2 }}],
|
|
126
|
+
"cycle": [{ "score": { "strain": 6.7 }}]
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Analysis commands output formatted text by default.
|
|
131
|
+
|
|
132
|
+
## Keeping tokens fresh
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
136
|
+
Important:
|
|
137
|
+
- `whoop-cli auth status` **does not refresh tokens** — it only reports whether they're expired.
|
|
138
|
+
- For automation, you must call `whoop-cli auth refresh` periodically.
|
|
139
|
+
|
|
140
|
+
Recommended pattern:
|
|
141
|
+
- Run `whoop-cli auth login` once interactively (creates `~/.whoop-cli/tokens.json`).
|
|
142
|
+
- Run a small periodic monitor that calls `whoop-cli auth refresh` and performs a lightweight fetch.
|
|
143
|
+
|
|
144
|
+
An example monitor script + systemd timer/cron examples are included here:
|
|
145
|
+
- `examples/monitor/whoop-refresh-monitor.sh`
|
|
146
|
+
- `examples/monitor/systemd/*`
|
|
147
|
+
- `examples/monitor/cron/README-cron.txt`
|
|
148
|
+
|
|
149
|
+
If refresh fails with an expired refresh token, you must re-authenticate:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
whoop-cli auth login
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Exit Codes
|
|
156
|
+
|
|
157
|
+
| Code | Meaning |
|
|
158
|
+
| ---- | -------------------- |
|
|
159
|
+
| 0 | Success |
|
|
160
|
+
| 1 | General error |
|
|
161
|
+
| 2 | Authentication error |
|
|
162
|
+
| 3 | Rate limit exceeded |
|
|
163
|
+
| 4 | Network error |
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
git clone https://github.com/xonika9/whoop-cli.git
|
|
169
|
+
cd whoop-cli
|
|
170
|
+
npm install
|
|
171
|
+
npm run dev # Run with tsx
|
|
172
|
+
npm run build # Compile TypeScript
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Requirements
|
|
176
|
+
|
|
177
|
+
- Node.js 22+
|
|
178
|
+
- WHOOP membership with API access
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
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[], dateOrRange: string | {
|
|
9
|
+
start: string;
|
|
10
|
+
end: string;
|
|
11
|
+
}, options?: {
|
|
12
|
+
limit?: number;
|
|
13
|
+
all?: boolean;
|
|
14
|
+
}): Promise<CombinedOutput>;
|
|
15
|
+
export declare function fetchAllTypes(date: string, options?: {
|
|
16
|
+
limit?: number;
|
|
17
|
+
all?: boolean;
|
|
18
|
+
}): Promise<CombinedOutput>;
|
|
19
|
+
//# 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,WAAW,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EACpD,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9C,OAAO,CAAC,cAAc,CAAC,CAuCzB;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,99 @@
|
|
|
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, dateOrRange, options = {}) {
|
|
62
|
+
const range = typeof dateOrRange === 'string'
|
|
63
|
+
? getDateRange(dateOrRange)
|
|
64
|
+
: { start: dateOrRange.start, end: dateOrRange.end };
|
|
65
|
+
const params = { start: range.start, end: range.end, limit: options.limit };
|
|
66
|
+
const date = typeof dateOrRange === 'string'
|
|
67
|
+
? dateOrRange
|
|
68
|
+
: dateOrRange.start.split('T')[0];
|
|
69
|
+
const output = {
|
|
70
|
+
date,
|
|
71
|
+
fetched_at: nowISO(),
|
|
72
|
+
};
|
|
73
|
+
const fetchers = {
|
|
74
|
+
profile: async () => {
|
|
75
|
+
output.profile = await getProfile();
|
|
76
|
+
},
|
|
77
|
+
body: async () => {
|
|
78
|
+
output.body = await getBody();
|
|
79
|
+
},
|
|
80
|
+
sleep: async () => {
|
|
81
|
+
output.sleep = await getSleep(params, options.all);
|
|
82
|
+
},
|
|
83
|
+
recovery: async () => {
|
|
84
|
+
output.recovery = await getRecovery(params, options.all);
|
|
85
|
+
},
|
|
86
|
+
workout: async () => {
|
|
87
|
+
output.workout = await getWorkout(params, options.all);
|
|
88
|
+
},
|
|
89
|
+
cycle: async () => {
|
|
90
|
+
output.cycle = await getCycle(params, options.all);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
await Promise.all(types.map((type) => fetchers[type]()));
|
|
94
|
+
return output;
|
|
95
|
+
}
|
|
96
|
+
export async function fetchAllTypes(date, options = {}) {
|
|
97
|
+
return fetchData(['profile', 'body', 'sleep', 'recovery', 'workout', 'cycle'], date, options);
|
|
98
|
+
}
|
|
99
|
+
//# 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"}
|