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.
Files changed (52) hide show
  1. package/.env.example +4 -0
  2. package/LICENSE +21 -0
  3. package/README.md +182 -0
  4. package/dist/api/client.d.ts +19 -0
  5. package/dist/api/client.d.ts.map +1 -0
  6. package/dist/api/client.js +99 -0
  7. package/dist/api/client.js.map +1 -0
  8. package/dist/api/endpoints.d.ts +10 -0
  9. package/dist/api/endpoints.d.ts.map +1 -0
  10. package/dist/api/endpoints.js +10 -0
  11. package/dist/api/endpoints.js.map +1 -0
  12. package/dist/auth/oauth.d.ts +9 -0
  13. package/dist/auth/oauth.d.ts.map +1 -0
  14. package/dist/auth/oauth.js +122 -0
  15. package/dist/auth/oauth.js.map +1 -0
  16. package/dist/auth/server.d.ts +7 -0
  17. package/dist/auth/server.d.ts.map +1 -0
  18. package/dist/auth/server.js +53 -0
  19. package/dist/auth/server.js.map +1 -0
  20. package/dist/auth/tokens.d.ts +12 -0
  21. package/dist/auth/tokens.d.ts.map +1 -0
  22. package/dist/auth/tokens.js +102 -0
  23. package/dist/auth/tokens.js.map +1 -0
  24. package/dist/cli.d.ts +3 -0
  25. package/dist/cli.d.ts.map +1 -0
  26. package/dist/cli.js +251 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +6 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/types/whoop.d.ts +159 -0
  33. package/dist/types/whoop.d.ts.map +1 -0
  34. package/dist/types/whoop.js +2 -0
  35. package/dist/types/whoop.js.map +1 -0
  36. package/dist/utils/analysis.d.ts +30 -0
  37. package/dist/utils/analysis.d.ts.map +1 -0
  38. package/dist/utils/analysis.js +231 -0
  39. package/dist/utils/analysis.js.map +1 -0
  40. package/dist/utils/date.d.ts +10 -0
  41. package/dist/utils/date.d.ts.map +1 -0
  42. package/dist/utils/date.js +38 -0
  43. package/dist/utils/date.js.map +1 -0
  44. package/dist/utils/errors.d.ts +14 -0
  45. package/dist/utils/errors.d.ts.map +1 -0
  46. package/dist/utils/errors.js +36 -0
  47. package/dist/utils/errors.js.map +1 -0
  48. package/dist/utils/format.d.ts +14 -0
  49. package/dist/utils/format.d.ts.map +1 -0
  50. package/dist/utils/format.js +241 -0
  51. package/dist/utils/format.js.map +1 -0
  52. package/package.json +58 -0
package/.env.example ADDED
@@ -0,0 +1,4 @@
1
+ # Required: Register at developer.whoop.com
2
+ WHOOP_CLIENT_ID=
3
+ WHOOP_CLIENT_SECRET=
4
+ WHOOP_REDIRECT_URI=
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
+ [![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
+ ```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":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAa1D,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAExD,KAAK,UAAU,OAAO,CAAI,QAAgB,EAAE,MAAoB;IAC9D,MAAM,MAAM,GAAG,MAAM,cAAc,EAAE,CAAC;IAEtC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAAC;IACzC,IAAI,MAAM,EAAE,KAAK;QAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IAC/D,IAAI,MAAM,EAAE,GAAG;QAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IACzD,IAAI,MAAM,EAAE,KAAK;QAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACvE,IAAI,MAAM,EAAE,SAAS;QAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IAE3E,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE;QAC3C,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,MAAM,CAAC,YAAY,EAAE;YAC9C,cAAc,EAAE,kBAAkB;SACnC;KACF,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,MAAM,IAAI,UAAU,CAAC,uBAAuB,EAAE,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QAC1E,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,MAAM,IAAI,UAAU,CAAC,qBAAqB,EAAE,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QACxE,CAAC;QACD,MAAM,IAAI,UAAU,CAAC,oBAAoB,EAAE,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtF,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,EAAgB,CAAC;AACvC,CAAC;AAED,KAAK,UAAU,QAAQ,CACrB,QAAgB,EAChB,MAAmB,EACnB,aAAsB;IAEtB,MAAM,OAAO,GAAQ,EAAE,CAAC;IACxB,IAAI,SAA6B,CAAC;IAElC,GAAG,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAiB,QAAQ,EAAE,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QACnF,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAClC,SAAS,GAAG,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9D,CAAC,QAAQ,SAAS,EAAE;IAEpB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,OAAO,OAAO,CAAe,SAAS,CAAC,OAAO,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO;IAC3B,OAAO,OAAO,CAAY,SAAS,CAAC,IAAI,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,SAAsB,EAAE,EAAE,GAAG,GAAG,KAAK;IAClE,OAAO,QAAQ,CAAa,SAAS,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,MAAM,EAAE,EAAE,GAAG,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,SAAsB,EAAE,EAAE,GAAG,GAAG,KAAK;IACrE,OAAO,QAAQ,CAAgB,SAAS,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,MAAM,EAAE,EAAE,GAAG,CAAC,CAAC;AACpF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,SAAsB,EAAE,EAAE,GAAG,GAAG,KAAK;IACpE,OAAO,QAAQ,CAAe,SAAS,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,MAAM,EAAE,EAAE,GAAG,CAAC,CAAC;AAClF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,SAAsB,EAAE,EAAE,GAAG,GAAG,KAAK;IAClE,OAAO,QAAQ,CAAa,SAAS,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,MAAM,EAAE,EAAE,GAAG,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAAiB,EACjB,WAAoD,EACpD,UAA6C,EAAE;IAE/C,MAAM,KAAK,GAAG,OAAO,WAAW,KAAK,QAAQ;QAC3C,CAAC,CAAC,YAAY,CAAC,WAAW,CAAC;QAC3B,CAAC,CAAC,EAAE,KAAK,EAAE,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,CAAC;IACvD,MAAM,MAAM,GAAgB,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;IAEzF,MAAM,IAAI,GAAG,OAAO,WAAW,KAAK,QAAQ;QAC1C,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAEpC,MAAM,MAAM,GAAmB;QAC7B,IAAI;QACJ,UAAU,EAAE,MAAM,EAAE;KACrB,CAAC;IAEF,MAAM,QAAQ,GAA0C;QACtD,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,CAAC,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;QACtC,CAAC;QACD,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,MAAM,CAAC,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;QAChC,CAAC;QACD,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,MAAM,CAAC,KAAK,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QACrD,CAAC;QACD,QAAQ,EAAE,KAAK,IAAI,EAAE;YACnB,MAAM,CAAC,QAAQ,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,CAAC,OAAO,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QACzD,CAAC;QACD,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,MAAM,CAAC,KAAK,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QACrD,CAAC;KACF,CAAC;IAEF,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IAEzD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAY,EACZ,UAA6C,EAAE;IAE/C,OAAO,SAAS,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AAChG,CAAC"}
@@ -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":"AAAA,OAAO,EAAE,YAAY,EAAe,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAO/B,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,YAAoB,IAAI;IAC9D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;QAC9B,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,EAAE;YAC5B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACtB,OAAO,CAAC,iBAAiB,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,MAAc,CAAC;QAEnB,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC9B,MAAM,EAAE,KAAK,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC,CAAC;QAC9C,CAAC,EAAE,MAAM,CAAC,CAAC;QAEX,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACjC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,oBAAoB,IAAI,EAAE,CAAC,CAAC;YAEhE,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;gBACjC,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBAC1C,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAE5C,IAAI,KAAK,EAAE,CAAC;oBACV,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;oBACpD,GAAG,CAAC,GAAG,CAAC,gEAAgE,CAAC,CAAC;oBAC1E,YAAY,CAAC,OAAO,CAAC,CAAC;oBACtB,MAAM,CAAC,KAAK,EAAE,CAAC;oBACf,MAAM,CAAC,IAAI,KAAK,CAAC,gBAAgB,KAAK,EAAE,CAAC,CAAC,CAAC;oBAC3C,OAAO;gBACT,CAAC;gBAED,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC;oBAClB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;oBACpD,GAAG,CAAC,GAAG,CAAC,oEAAoE,CAAC,CAAC;oBAC9E,YAAY,CAAC,OAAO,CAAC,CAAC;oBACtB,MAAM,CAAC,KAAK,EAAE,CAAC;oBACf,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;oBACzB,OAAO;gBACT,CAAC;gBAED,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;gBACpD,GAAG,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;gBACvC,OAAO;YACT,CAAC;YAED,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -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"}