@betterness/cli 1.0.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 (2) hide show
  1. package/dist/index.js +1835 -0
  2. package/package.json +49 -0
package/dist/index.js ADDED
@@ -0,0 +1,1835 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/program.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/auth.ts
7
+ import { createInterface } from "readline/promises";
8
+ import { stdin, stdout } from "process";
9
+
10
+ // src/auth/credentialStore.ts
11
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
12
+ import { join } from "path";
13
+ import { homedir } from "os";
14
+ var CONFIG_DIR = join(homedir(), ".betterness");
15
+ var CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
16
+ function ensureConfigDir() {
17
+ if (!existsSync(CONFIG_DIR)) {
18
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
19
+ }
20
+ }
21
+ function loadCredentials() {
22
+ if (!existsSync(CREDENTIALS_FILE)) {
23
+ return null;
24
+ }
25
+ try {
26
+ const content = readFileSync(CREDENTIALS_FILE, "utf-8");
27
+ return JSON.parse(content);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ function saveCredentials(credentials) {
33
+ ensureConfigDir();
34
+ const data = {
35
+ ...credentials,
36
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
37
+ };
38
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
39
+ }
40
+ function deleteCredentials() {
41
+ if (!existsSync(CREDENTIALS_FILE)) {
42
+ return false;
43
+ }
44
+ unlinkSync(CREDENTIALS_FILE);
45
+ return true;
46
+ }
47
+
48
+ // src/types/errors.ts
49
+ var CliError = class extends Error {
50
+ code;
51
+ constructor(message, code) {
52
+ super(message);
53
+ this.name = "CliError";
54
+ this.code = code;
55
+ }
56
+ toJSON() {
57
+ return {
58
+ error: {
59
+ code: this.code,
60
+ message: this.message
61
+ }
62
+ };
63
+ }
64
+ };
65
+
66
+ // src/auth/resolve.ts
67
+ function resolveCredentials(explicitApiKey) {
68
+ if (explicitApiKey) {
69
+ return { apiKey: explicitApiKey };
70
+ }
71
+ const envKey = process.env.BETTERNESS_API_KEY;
72
+ if (envKey) {
73
+ return { apiKey: envKey };
74
+ }
75
+ const stored = loadCredentials();
76
+ if (stored) {
77
+ return { apiKey: stored.apiKey };
78
+ }
79
+ throw new CliError(
80
+ "No credentials found. Use one of:\n 1. betterness auth login\n 2. --api-key <key>\n 3. BETTERNESS_API_KEY environment variable",
81
+ "AUTH_MISSING"
82
+ );
83
+ }
84
+
85
+ // src/client/apiClient.ts
86
+ var MAX_RETRIES = 3;
87
+ var RETRY_BASE_DELAY_MS = 500;
88
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
89
+ var ApiClient = class {
90
+ apiKey;
91
+ apiUrl;
92
+ constructor(options = {}) {
93
+ const credentials = resolveCredentials(options.apiKey);
94
+ this.apiKey = credentials.apiKey;
95
+ const apiUrl = process.env.BETTERNESS_API_URL || "";
96
+ if (!apiUrl) {
97
+ throw new CliError(
98
+ "No API URL configured. Set BETTERNESS_API_URL or rebuild with BETTERNESS_BUILD_API_URL.",
99
+ "CONFIG_MISSING"
100
+ );
101
+ }
102
+ this.apiUrl = apiUrl;
103
+ }
104
+ async request(path, options = {}) {
105
+ const { method = "GET", body, params, timeout = 3e4 } = options;
106
+ const url = new URL(path, this.apiUrl);
107
+ if (params) {
108
+ for (const [key, value] of Object.entries(params)) {
109
+ if (value !== void 0) {
110
+ url.searchParams.set(key, String(value));
111
+ }
112
+ }
113
+ }
114
+ let lastError;
115
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
116
+ if (attempt > 0) {
117
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
118
+ await new Promise((resolve) => setTimeout(resolve, delay));
119
+ }
120
+ const controller = new AbortController();
121
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
122
+ try {
123
+ const response = await fetch(url.toString(), {
124
+ method,
125
+ headers: {
126
+ "Authorization": `Bearer ${this.apiKey}`,
127
+ "Content-Type": "application/json",
128
+ "User-Agent": "betterness-cli/0.1.0",
129
+ "Accept": "application/json"
130
+ },
131
+ body: body ? JSON.stringify(body) : void 0,
132
+ signal: controller.signal
133
+ });
134
+ clearTimeout(timeoutId);
135
+ if (!response.ok) {
136
+ if (attempt < MAX_RETRIES && RETRYABLE_STATUS_CODES.has(response.status)) {
137
+ lastError = new CliError(`Request failed with status ${response.status}`, `HTTP_${response.status}`);
138
+ continue;
139
+ }
140
+ await this.handleErrorResponse(response);
141
+ }
142
+ const text = await response.text();
143
+ if (!text) return void 0;
144
+ return JSON.parse(text);
145
+ } catch (error) {
146
+ clearTimeout(timeoutId);
147
+ if (error instanceof CliError) throw error;
148
+ if (error instanceof Error && error.name === "AbortError") {
149
+ lastError = new CliError(`Request timed out after ${timeout}ms: ${method} ${path}`, "TIMEOUT");
150
+ if (attempt < MAX_RETRIES) continue;
151
+ throw lastError;
152
+ }
153
+ lastError = new CliError(`Network error: ${error instanceof Error ? error.message : "Unknown error"}`, "NETWORK_ERROR");
154
+ if (attempt < MAX_RETRIES) continue;
155
+ }
156
+ }
157
+ throw lastError;
158
+ }
159
+ async get(path, params, schema) {
160
+ const raw = await this.request(path, { method: "GET", params });
161
+ return this.unwrap(raw, schema);
162
+ }
163
+ async post(path, body, params, schema) {
164
+ const raw = await this.request(path, { method: "POST", body, params });
165
+ return this.unwrap(raw, schema);
166
+ }
167
+ async put(path, body, schema) {
168
+ const raw = await this.request(path, { method: "PUT", body });
169
+ return this.unwrap(raw, schema);
170
+ }
171
+ async delete(path, schema) {
172
+ const raw = await this.request(path, { method: "DELETE" });
173
+ return this.unwrap(raw, schema);
174
+ }
175
+ async upload(path, filePath, fieldName = "file", schema) {
176
+ const { readFileSync: readFileSync2 } = await import("fs");
177
+ const { basename, extname } = await import("path");
178
+ const buffer = readFileSync2(filePath);
179
+ const fileName = basename(filePath);
180
+ const ext = extname(filePath).toLowerCase();
181
+ const mimeType = ext === ".pdf" ? "application/pdf" : "application/octet-stream";
182
+ const formData = new FormData();
183
+ formData.append(fieldName, new Blob([buffer], { type: mimeType }), fileName);
184
+ const url = new URL(path, this.apiUrl);
185
+ const controller = new AbortController();
186
+ const timeoutId = setTimeout(() => controller.abort(), 6e4);
187
+ try {
188
+ const response = await fetch(url.toString(), {
189
+ method: "POST",
190
+ headers: {
191
+ "Authorization": `Bearer ${this.apiKey}`,
192
+ "User-Agent": "betterness-cli/0.1.0",
193
+ "Accept": "application/json"
194
+ },
195
+ body: formData,
196
+ signal: controller.signal
197
+ });
198
+ clearTimeout(timeoutId);
199
+ if (!response.ok) {
200
+ await this.handleErrorResponse(response);
201
+ }
202
+ const text = await response.text();
203
+ if (!text) return void 0;
204
+ const raw = JSON.parse(text);
205
+ return this.unwrap(raw, schema);
206
+ } catch (error) {
207
+ clearTimeout(timeoutId);
208
+ if (error instanceof CliError) throw error;
209
+ if (error instanceof Error && error.name === "AbortError") {
210
+ throw new CliError("Upload timed out after 60s", "TIMEOUT");
211
+ }
212
+ throw new CliError(`Upload failed: ${error instanceof Error ? error.message : "Unknown error"}`, "NETWORK_ERROR");
213
+ }
214
+ }
215
+ unwrap(result, schema) {
216
+ if (!result.success) {
217
+ throw new CliError(result.message ?? "Request failed", result.errorCode ?? "API_ERROR");
218
+ }
219
+ if (schema) {
220
+ const parsed = schema.safeParse(result.content);
221
+ if (!parsed.success) {
222
+ throw new CliError(
223
+ `Unexpected response shape: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")}`,
224
+ "VALIDATION_ERROR"
225
+ );
226
+ }
227
+ return parsed.data;
228
+ }
229
+ return result.content;
230
+ }
231
+ async handleErrorResponse(response) {
232
+ let errorBody;
233
+ try {
234
+ errorBody = await response.text();
235
+ } catch {
236
+ errorBody = "";
237
+ }
238
+ let message;
239
+ let code;
240
+ switch (response.status) {
241
+ case 401:
242
+ code = "AUTH_UNAUTHORIZED";
243
+ message = "Invalid or expired API key. Run 'betterness auth login' to re-authenticate.";
244
+ break;
245
+ case 403:
246
+ code = "AUTH_FORBIDDEN";
247
+ message = "Access denied. Your API key does not have permission for this operation.";
248
+ break;
249
+ case 404:
250
+ code = "NOT_FOUND";
251
+ message = "Resource not found.";
252
+ break;
253
+ case 429:
254
+ code = "RATE_LIMITED";
255
+ message = "Rate limit exceeded. Please wait before retrying.";
256
+ break;
257
+ default:
258
+ code = `HTTP_${response.status}`;
259
+ message = `Request failed with status ${response.status}`;
260
+ if (errorBody) {
261
+ try {
262
+ const parsed = JSON.parse(errorBody);
263
+ if (parsed.message) message = parsed.message;
264
+ if (parsed.error_description) message = parsed.error_description;
265
+ } catch {
266
+ if (errorBody.length < 200) message = errorBody;
267
+ }
268
+ }
269
+ }
270
+ throw new CliError(message, code);
271
+ }
272
+ };
273
+
274
+ // src/formatters/json.ts
275
+ function formatJson(data) {
276
+ return JSON.stringify(data, null, 2);
277
+ }
278
+
279
+ // src/formatters/table.ts
280
+ function formatTable(rows, columns) {
281
+ if (rows.length === 0) {
282
+ return "No results.";
283
+ }
284
+ const cols = columns ?? inferColumns(rows);
285
+ const widths = calculateWidths(rows, cols);
286
+ const header = cols.map((col, i) => padCell(col.label, widths[i], col.align)).join(" ");
287
+ const separator = cols.map((_, i) => "-".repeat(widths[i])).join(" ");
288
+ const body = rows.map(
289
+ (row) => cols.map((col, i) => padCell(String(row[col.key] ?? ""), widths[i], col.align)).join(" ")
290
+ );
291
+ return [header, separator, ...body].join("\n");
292
+ }
293
+ function formatKeyValue(data) {
294
+ const entries = Object.entries(data).filter(([_, v]) => v !== null && v !== void 0);
295
+ const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
296
+ return entries.map(([key, value]) => `${key.padEnd(maxKeyLen)} ${String(value)}`).join("\n");
297
+ }
298
+ function inferColumns(rows) {
299
+ const keys = /* @__PURE__ */ new Set();
300
+ for (const row of rows) {
301
+ for (const key of Object.keys(row)) {
302
+ keys.add(key);
303
+ }
304
+ }
305
+ return Array.from(keys).map((key) => ({
306
+ key,
307
+ label: key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim()
308
+ }));
309
+ }
310
+ function calculateWidths(rows, columns) {
311
+ return columns.map((col) => {
312
+ if (col.width) return col.width;
313
+ const headerLen = col.label.length;
314
+ const maxDataLen = rows.reduce((max, row) => {
315
+ const val = String(row[col.key] ?? "");
316
+ return Math.max(max, val.length);
317
+ }, 0);
318
+ return Math.min(Math.max(headerLen, maxDataLen), 60);
319
+ });
320
+ }
321
+ function padCell(value, width, align) {
322
+ const truncated = value.length > width ? value.slice(0, width - 1) + "\u2026" : value;
323
+ return align === "right" ? truncated.padStart(width) : truncated.padEnd(width);
324
+ }
325
+
326
+ // src/formatters/markdown.ts
327
+ function formatMarkdownTable(rows, keys) {
328
+ if (rows.length === 0) {
329
+ return "*No results.*";
330
+ }
331
+ const columns = keys ?? Object.keys(rows[0]);
332
+ const header = "| " + columns.join(" | ") + " |";
333
+ const separator = "| " + columns.map(() => "---").join(" | ") + " |";
334
+ const body = rows.map(
335
+ (row) => "| " + columns.map((col) => String(row[col] ?? "")).join(" | ") + " |"
336
+ );
337
+ return [header, separator, ...body].join("\n");
338
+ }
339
+ function formatMarkdownKeyValue(data) {
340
+ return Object.entries(data).filter(([_, v]) => v !== null && v !== void 0).map(([key, value]) => `**${key}:** ${String(value)}`).join("\n\n");
341
+ }
342
+
343
+ // src/formatters/output.ts
344
+ function isQuiet(cmd) {
345
+ return cmd.optsWithGlobals().quiet === true;
346
+ }
347
+ function getOutputFormat(cmd) {
348
+ const opts = cmd.optsWithGlobals();
349
+ if (opts.json) return "json";
350
+ if (opts.markdown) return "markdown";
351
+ const env = process.env.BETTERNESS_OUTPUT?.toLowerCase();
352
+ if (env === "json") return "json";
353
+ if (env === "markdown") return "markdown";
354
+ return "table";
355
+ }
356
+ function outputList(cmd, rows, columns) {
357
+ if (isQuiet(cmd)) return;
358
+ const format = getOutputFormat(cmd);
359
+ switch (format) {
360
+ case "json":
361
+ console.log(formatJson(rows));
362
+ break;
363
+ case "markdown":
364
+ console.log(formatMarkdownTable(rows, columns?.map((c) => c.key)));
365
+ break;
366
+ case "table":
367
+ console.log(formatTable(rows, columns));
368
+ break;
369
+ }
370
+ }
371
+ function outputRecord(cmd, data) {
372
+ if (isQuiet(cmd)) return;
373
+ const format = getOutputFormat(cmd);
374
+ switch (format) {
375
+ case "json":
376
+ console.log(formatJson(data));
377
+ break;
378
+ case "markdown":
379
+ console.log(formatMarkdownKeyValue(data));
380
+ break;
381
+ case "table":
382
+ console.log(formatKeyValue(data));
383
+ break;
384
+ }
385
+ }
386
+ function outputError(error) {
387
+ if (error instanceof CliError) {
388
+ console.error(JSON.stringify(error.toJSON(), null, 2));
389
+ process.exit(error.code === "AUTH_MISSING" || error.code === "AUTH_UNAUTHORIZED" ? 2 : 1);
390
+ }
391
+ const message = error instanceof Error ? error.message : String(error);
392
+ console.error(JSON.stringify({ error: { code: "UNKNOWN", message } }, null, 2));
393
+ process.exit(1);
394
+ }
395
+
396
+ // src/types/api.ts
397
+ import { z } from "zod";
398
+ var simplePageSchema = (itemSchema) => z.object({
399
+ content: z.array(itemSchema),
400
+ number: z.number(),
401
+ last: z.boolean(),
402
+ totalResults: z.number()
403
+ });
404
+ var springPageSchema = (itemSchema) => z.object({
405
+ content: z.array(itemSchema),
406
+ totalPages: z.number(),
407
+ totalElements: z.number(),
408
+ number: z.number(),
409
+ size: z.number(),
410
+ numberOfElements: z.number(),
411
+ first: z.boolean(),
412
+ last: z.boolean(),
413
+ empty: z.boolean()
414
+ });
415
+ var homeAddressSchema = z.object({
416
+ fullAddress: z.string().nullable().optional(),
417
+ city: z.string().nullable().optional(),
418
+ state: z.string().nullable().optional(),
419
+ zipCode: z.string().nullable().optional(),
420
+ streetNumber: z.string().nullable().optional(),
421
+ streetName: z.string().nullable().optional(),
422
+ country: z.string().nullable().optional(),
423
+ latitude: z.number().nullable().optional(),
424
+ longitude: z.number().nullable().optional()
425
+ });
426
+ var vitalConnectionSchema = z.object({
427
+ externalId: z.string(),
428
+ provider: z.string(),
429
+ disabledAt: z.string().nullable().optional()
430
+ });
431
+ var betternessUserSchema = z.object({
432
+ externalId: z.string(),
433
+ firstName: z.string().nullable().optional(),
434
+ lastName: z.string().nullable().optional(),
435
+ birthDate: z.string().nullable().optional(),
436
+ location: z.string().nullable().optional(),
437
+ phone: z.string().nullable().optional(),
438
+ phoneDialCode: z.string().nullable().optional(),
439
+ email: z.string().nullable().optional(),
440
+ gender: z.string().nullable().optional(),
441
+ type: z.string().nullable().optional(),
442
+ fullName: z.string().nullable().optional(),
443
+ avatarUrl: z.string().nullable().optional(),
444
+ createdAtUnixSeconds: z.number().nullable().optional(),
445
+ onboardingStep: z.string().nullable().optional(),
446
+ vitalId: z.string().nullable().optional(),
447
+ vitalConnections: z.array(vitalConnectionSchema).nullable().optional(),
448
+ interests: z.array(z.string()).nullable().optional(),
449
+ phoneVerified: z.boolean().nullable().optional(),
450
+ homeAddressV2: homeAddressSchema.nullable().optional(),
451
+ heightCm: z.number().nullable().optional(),
452
+ weight: z.number().nullable().optional()
453
+ }).passthrough();
454
+ var loincSchema = z.object({
455
+ externalId: z.string().nullable().optional(),
456
+ slug: z.string().nullable().optional(),
457
+ code: z.string().nullable().optional(),
458
+ name: z.string().nullable().optional(),
459
+ description: z.string().nullable().optional(),
460
+ categories: z.array(z.string()).nullable().optional(),
461
+ syncedAt: z.string().nullable().optional()
462
+ }).passthrough();
463
+ var biomarkerSchema = z.object({
464
+ externalId: z.string(),
465
+ name: z.string(),
466
+ category: z.string().nullable().optional(),
467
+ type: z.string().nullable().optional(),
468
+ loinc: loincSchema.nullable().optional()
469
+ }).passthrough();
470
+ var biomarkerResultSchema = z.object({
471
+ externalId: z.string(),
472
+ biomarker: biomarkerSchema,
473
+ sourceCreationDate: z.string().nullable().optional(),
474
+ value: z.number().nullable().optional(),
475
+ unit: z.string().nullable().optional(),
476
+ range: z.string().nullable().optional(),
477
+ minRangeValue: z.number().nullable().optional(),
478
+ maxRangeValue: z.number().nullable().optional(),
479
+ metadataType: z.string().nullable().optional(),
480
+ metadataInterpretation: z.string().nullable().optional(),
481
+ metadataResult: z.string().nullable().optional(),
482
+ healthRecordExternalId: z.string().nullable().optional(),
483
+ dateCollected: z.string().nullable().optional()
484
+ });
485
+ var markerValueSchema = z.object({
486
+ loincCode: z.string().nullable().optional(),
487
+ label: z.string().nullable().optional(),
488
+ rawValue: z.number().nullable().optional(),
489
+ rawUnit: z.string().nullable().optional(),
490
+ standardizedValue: z.number().nullable().optional(),
491
+ standardizedUnit: z.string().nullable().optional(),
492
+ isOptimal: z.boolean().nullable().optional()
493
+ });
494
+ var biologicalAgeResultSchema = z.object({
495
+ externalId: z.string(),
496
+ calculatedAt: z.string().nullable().optional(),
497
+ measurementDate: z.string().nullable().optional(),
498
+ currentAge: z.number().nullable().optional(),
499
+ biologicalAge: z.number().nullable().optional(),
500
+ source: z.string().nullable().optional(),
501
+ markers: z.array(markerValueSchema).nullable().optional()
502
+ });
503
+ var labTestSchema = z.object({
504
+ externalId: z.string(),
505
+ objectKey: z.string().nullable().optional(),
506
+ integrationProductId: z.string().nullable().optional(),
507
+ name: z.string(),
508
+ description: z.string().nullable().optional(),
509
+ lab: z.string().nullable().optional(),
510
+ price: z.number().nullable().optional(),
511
+ imageUrl: z.string().nullable().optional(),
512
+ isPopular: z.boolean().nullable().optional()
513
+ }).passthrough();
514
+ var paymentMethodSchema = z.object({
515
+ externalId: z.string(),
516
+ brand: z.string().nullable().optional(),
517
+ last4: z.string().nullable().optional(),
518
+ expiresAt: z.string().nullable().optional(),
519
+ id: z.string().nullable().optional(),
520
+ active: z.boolean().nullable().optional()
521
+ });
522
+ var healthRecordSchema = z.object({
523
+ externalId: z.string(),
524
+ source: z.string().nullable().optional()
525
+ }).passthrough();
526
+ var connectionsResponseSchema = z.object({
527
+ connections: z.array(z.string()),
528
+ nativeConnections: z.array(z.object({
529
+ provider: z.string(),
530
+ deviceId: z.string().nullable().optional(),
531
+ syncEnabled: z.boolean().nullable().optional()
532
+ }).passthrough())
533
+ }).passthrough();
534
+
535
+ // src/commands/auth.ts
536
+ function registerAuthCommands(program2) {
537
+ const auth = program2.command("auth").description("Manage authentication");
538
+ auth.command("login").description("Save API key for authentication").option("--key <apiKey>", "API key to save (if not provided, will prompt)").action(async (opts) => {
539
+ try {
540
+ let apiKey = opts.key;
541
+ if (!apiKey) {
542
+ const rl = createInterface({ input: stdin, output: stdout });
543
+ apiKey = await rl.question("Enter your Betterness API key: ");
544
+ rl.close();
545
+ }
546
+ if (!apiKey || !apiKey.trim()) {
547
+ console.error("API key cannot be empty.");
548
+ process.exit(1);
549
+ }
550
+ apiKey = apiKey.trim();
551
+ console.error("Verifying...");
552
+ const client = new ApiClient({ apiKey });
553
+ const user = await client.get("/api/betterness-user/detail", void 0, betternessUserSchema);
554
+ saveCredentials({
555
+ apiKey,
556
+ email: user.email ?? void 0,
557
+ name: user.firstName ?? void 0
558
+ });
559
+ console.log(`Logged in as: ${user.firstName ?? "Unknown"} (${user.email ?? "no email"})`);
560
+ console.log("Credentials saved to ~/.betterness/credentials.json");
561
+ } catch (error) {
562
+ outputError(error);
563
+ }
564
+ });
565
+ auth.command("logout").description("Remove stored credentials").action(() => {
566
+ const deleted = deleteCredentials();
567
+ if (deleted) {
568
+ console.log("Credentials removed.");
569
+ } else {
570
+ console.log("No stored credentials found.");
571
+ }
572
+ });
573
+ auth.command("whoami").description("Show the currently authenticated user").action(async (_, cmd) => {
574
+ try {
575
+ const parentOpts = cmd.optsWithGlobals();
576
+ const client = new ApiClient({ apiKey: parentOpts.apiKey });
577
+ const user = await client.get("/api/betterness-user/detail", void 0, betternessUserSchema);
578
+ outputRecord(cmd, {
579
+ name: user.fullName ?? user.firstName,
580
+ email: user.email,
581
+ id: user.externalId
582
+ });
583
+ } catch (error) {
584
+ outputError(error);
585
+ }
586
+ });
587
+ }
588
+
589
+ // src/commands/profile.ts
590
+ function registerProfileCommands(program2) {
591
+ const profile = program2.command("profile").description("User profile information");
592
+ profile.command("get").description("Retrieve current user profile (name, email, phone, gender, DOB, address)").action(async (_, cmd) => {
593
+ try {
594
+ const globalOpts = cmd.optsWithGlobals();
595
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
596
+ const user = await client.get(
597
+ "/api/betterness-user/detail",
598
+ void 0,
599
+ betternessUserSchema
600
+ );
601
+ outputRecord(cmd, {
602
+ firstName: user.firstName,
603
+ lastName: user.lastName,
604
+ email: user.email,
605
+ phone: user.phone,
606
+ gender: user.gender,
607
+ birthDate: user.birthDate,
608
+ heightCm: user.heightCm,
609
+ weight: user.weight,
610
+ homeAddress: user.homeAddressV2?.fullAddress,
611
+ city: user.homeAddressV2?.city,
612
+ state: user.homeAddressV2?.state,
613
+ zipCode: user.homeAddressV2?.zipCode,
614
+ country: user.homeAddressV2?.country
615
+ });
616
+ } catch (error) {
617
+ outputError(error);
618
+ }
619
+ });
620
+ profile.command("update").description("Update profile information (only provided fields are changed)").option("--first-name <name>", "First name").option("--last-name <name>", "Last name").option("--phone <number>", "Phone number without dial code").option("--phone-dial-code <code>", "Phone dial code (e.g. +1, +44)").option("--gender <value>", "Gender: MALE, FEMALE, OTHER, or PREF_NOT").option("--birth-date <YYYY-MM-DD>", "Date of birth").option("--address <street>", "Home street address").option("--city <city>", "City").option("--state <state>", "State or province").option("--zip-code <zip>", "ZIP or postal code").option("--country <code>", "Country (e.g. US, GB)").option("--dry-run", "Preview changes without applying").action(async (opts, cmd) => {
621
+ try {
622
+ const dto = {};
623
+ if (opts.firstName) dto.firstName = opts.firstName;
624
+ if (opts.lastName) dto.lastName = opts.lastName;
625
+ if (opts.phone) dto.phone = opts.phone;
626
+ if (opts.phoneDialCode) dto.phoneDialCode = opts.phoneDialCode;
627
+ if (opts.gender) dto.gender = opts.gender;
628
+ if (opts.birthDate) dto.birthDate = opts.birthDate;
629
+ const hasAddress = opts.address || opts.city || opts.state || opts.zipCode || opts.country;
630
+ if (hasAddress) {
631
+ const address = {};
632
+ if (opts.address) address.fullAddress = opts.address;
633
+ if (opts.city) address.city = opts.city;
634
+ if (opts.state) address.state = opts.state;
635
+ if (opts.zipCode) address.zipCode = opts.zipCode;
636
+ if (opts.country) address.country = opts.country;
637
+ dto.homeAddressV2 = address;
638
+ }
639
+ if (Object.keys(dto).length === 0) {
640
+ console.log("No fields provided to update. Use --help to see available options.");
641
+ return;
642
+ }
643
+ if (opts.dryRun) {
644
+ outputRecord(cmd, { action: "profile update", fields: dto, note: "Dry run \u2014 profile will not be updated." });
645
+ return;
646
+ }
647
+ const globalOpts = cmd.optsWithGlobals();
648
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
649
+ await client.put(
650
+ "/api/betterness-user/personal-details",
651
+ dto
652
+ );
653
+ console.log("Profile updated successfully.");
654
+ } catch (error) {
655
+ outputError(error);
656
+ }
657
+ });
658
+ }
659
+
660
+ // src/commands/biomarkers.ts
661
+ import { z as z2 } from "zod";
662
+ var biomarkerColumns = [
663
+ { key: "name", label: "Name", width: 25 },
664
+ { key: "value", label: "Value", width: 10, align: "right" },
665
+ { key: "unit", label: "Unit", width: 10 },
666
+ { key: "range", label: "Range", width: 15 },
667
+ { key: "date", label: "Date", width: 12 }
668
+ ];
669
+ function mapBiomarkerRows(results) {
670
+ return results.map((r) => ({
671
+ name: r.biomarker.name,
672
+ value: r.value,
673
+ unit: r.unit,
674
+ range: r.range,
675
+ date: r.dateCollected
676
+ }));
677
+ }
678
+ function registerBiomarkersCommands(program2) {
679
+ const biomarkers = program2.command("biomarkers").description("Biomarker lab results and LOINC codes");
680
+ biomarkers.command("search").description("Search and filter biomarker lab results").option("--name <text>", "Filter by biomarker name").option("--loinc-code <code>", "Filter by LOINC code").option("--start-date <YYYY-MM-DD>", "Start date (ISO-8601)").option("--end-date <YYYY-MM-DD>", "End date (ISO-8601)").option("--categories <list>", "Comma-separated category filter").option("--range <type>", "Range filter: OPTIMAL, AVERAGE, OUT_OF_RANGE, UNKNOWN").option("--limit <n>", "Maximum number of results", "20").action(async (opts, cmd) => {
681
+ try {
682
+ const globalOpts = cmd.optsWithGlobals();
683
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
684
+ const params = { size: opts.limit };
685
+ if (opts.name) params.name = opts.name;
686
+ if (opts.loincCode) params.loincCode = opts.loincCode;
687
+ if (opts.startDate) params.startDate = opts.startDate;
688
+ if (opts.endDate) params.endDate = opts.endDate;
689
+ if (opts.categories) params.categories = opts.categories;
690
+ if (opts.range) params.range = opts.range;
691
+ const results = await client.get(
692
+ "/api/biomarker-result/all",
693
+ params,
694
+ z2.array(biomarkerResultSchema)
695
+ );
696
+ outputList(cmd, mapBiomarkerRows(results), biomarkerColumns);
697
+ } catch (error) {
698
+ outputError(error);
699
+ }
700
+ });
701
+ biomarkers.command("loinc-codes").description("List all available LOINC codes for biomarker identification").action(async (_, cmd) => {
702
+ try {
703
+ const globalOpts = cmd.optsWithGlobals();
704
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
705
+ const loincs = await client.get(
706
+ "/api/health-record/active-loinc",
707
+ void 0,
708
+ z2.array(loincSchema)
709
+ );
710
+ outputList(cmd, loincs.map((l) => ({
711
+ code: l.code,
712
+ name: l.name,
713
+ categories: l.categories?.join(", ") ?? ""
714
+ })), [
715
+ { key: "code", label: "LOINC Code", width: 12 },
716
+ { key: "name", label: "Name", width: 35 },
717
+ { key: "categories", label: "Categories", width: 30 }
718
+ ]);
719
+ } catch (error) {
720
+ outputError(error);
721
+ }
722
+ });
723
+ }
724
+
725
+ // src/commands/biological-age.ts
726
+ import { z as z3 } from "zod";
727
+ function registerBiologicalAgeCommands(program2) {
728
+ const bioAge = program2.command("biological-age").description("Biological age calculations and history");
729
+ bioAge.command("get").description("Get biological age history with biomarker values").option("--limit <n>", "Maximum number of results", "10").action(async (opts, cmd) => {
730
+ try {
731
+ const globalOpts = cmd.optsWithGlobals();
732
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
733
+ const results = await client.get("/api/biological-age", {
734
+ size: opts.limit,
735
+ sort: "date,desc"
736
+ }, z3.array(biologicalAgeResultSchema));
737
+ if (results.length === 0) {
738
+ console.log("No biological age data found.");
739
+ return;
740
+ }
741
+ outputList(cmd, results.map((r) => ({
742
+ currentAge: r.currentAge,
743
+ biologicalAge: r.biologicalAge,
744
+ source: r.source,
745
+ measurementDate: r.measurementDate,
746
+ calculatedAt: r.calculatedAt
747
+ })), [
748
+ { key: "currentAge", label: "Current Age", width: 12, align: "right" },
749
+ { key: "biologicalAge", label: "Bio Age", width: 10, align: "right" },
750
+ { key: "source", label: "Source", width: 15 },
751
+ { key: "measurementDate", label: "Date", width: 12 }
752
+ ]);
753
+ } catch (error) {
754
+ outputError(error);
755
+ }
756
+ });
757
+ }
758
+
759
+ // src/commands/helpers/trends.ts
760
+ function todayIso() {
761
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
762
+ }
763
+ function defaultTimezone() {
764
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
765
+ }
766
+ function dateRange(from, to) {
767
+ const dates = [];
768
+ const current = /* @__PURE__ */ new Date(from + "T00:00:00");
769
+ const end = /* @__PURE__ */ new Date(to + "T00:00:00");
770
+ while (current <= end) {
771
+ dates.push(current.toISOString().slice(0, 10));
772
+ current.setDate(current.getDate() + 1);
773
+ }
774
+ return dates;
775
+ }
776
+ function flattenTrends(data, categoryFilter) {
777
+ const rows = [];
778
+ for (const [provider, categories] of Object.entries(data)) {
779
+ if (!categories || typeof categories !== "object") continue;
780
+ for (const [category, goalTypes] of Object.entries(categories)) {
781
+ if (categoryFilter && category !== categoryFilter) continue;
782
+ if (!goalTypes || typeof goalTypes !== "object") continue;
783
+ for (const [_goalType, goals] of Object.entries(goalTypes)) {
784
+ if (!goals || typeof goals !== "object") continue;
785
+ for (const [goalKey, trend] of Object.entries(goals)) {
786
+ if (!trend || typeof trend !== "object") continue;
787
+ rows.push({ provider, goalKey, ...trend });
788
+ }
789
+ }
790
+ }
791
+ }
792
+ return rows;
793
+ }
794
+ async function fetchTrendsForRange(client, from, to, zoneId, categoryFilter) {
795
+ const dates = dateRange(from, to);
796
+ const allRows = [];
797
+ for (const day of dates) {
798
+ const data = await client.get("/api/goal-entry/trends", {
799
+ day,
800
+ zoneId,
801
+ periodType: "DAILY"
802
+ });
803
+ const rows = flattenTrends(data, categoryFilter);
804
+ for (const row of rows) {
805
+ row.date = day;
806
+ }
807
+ allRows.push(...rows);
808
+ }
809
+ return allRows;
810
+ }
811
+ function printTrendRows(rows) {
812
+ for (const row of rows) {
813
+ console.log(`--- ${row.goalKey} (${row.provider}) ---`);
814
+ const display = { ...row };
815
+ delete display.provider;
816
+ delete display.goalKey;
817
+ const entries = Object.entries(display).filter(([_, v]) => v !== null && v !== void 0);
818
+ if (entries.length === 0) continue;
819
+ const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
820
+ for (const [key, value] of entries) {
821
+ console.log(` ${key.padEnd(maxKeyLen)} ${String(value)}`);
822
+ }
823
+ console.log();
824
+ }
825
+ }
826
+ function registerHealthDataCommand(parent, name, description, categoryFilter) {
827
+ parent.command(name).description(description).option("--from <YYYY-MM-DD>", "Start date", todayIso()).option("--to <YYYY-MM-DD>", "End date").option("--timezone <tz>", "IANA timezone", defaultTimezone()).action(async (opts, cmd) => {
828
+ try {
829
+ const globalOpts = cmd.optsWithGlobals();
830
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
831
+ const to = opts.to ?? opts.from;
832
+ const rows = await fetchTrendsForRange(client, opts.from, to, opts.timezone, categoryFilter);
833
+ if (getOutputFormat(cmd) === "json") {
834
+ console.log(formatJson(rows));
835
+ return;
836
+ }
837
+ if (rows.length === 0) {
838
+ console.log(`No ${name} data found for ${opts.from}${to !== opts.from ? ` to ${to}` : ""}.`);
839
+ return;
840
+ }
841
+ printTrendRows(rows);
842
+ } catch (error) {
843
+ outputError(error);
844
+ }
845
+ });
846
+ }
847
+
848
+ // src/commands/activity.ts
849
+ function registerActivityCommands(program2) {
850
+ const activity = program2.command("activity").description("Activity and workout data from connected wearables");
851
+ registerHealthDataCommand(
852
+ activity,
853
+ "get",
854
+ "Retrieve activity and workout data (steps, distance, calories, VO2 max, workouts)",
855
+ "ACTIVITY"
856
+ );
857
+ }
858
+
859
+ // src/commands/sleep.ts
860
+ import { z as z4 } from "zod";
861
+ var sleepStageEntrySchema = z4.object({
862
+ stage: z4.string(),
863
+ start: z4.string(),
864
+ end: z4.string(),
865
+ durationSeconds: z4.number()
866
+ }).passthrough();
867
+ var sleepNightSchema = z4.object({
868
+ date: z4.string(),
869
+ stages: z4.array(sleepStageEntrySchema)
870
+ }).passthrough();
871
+ var sleepStagesResponseSchema = z4.object({
872
+ source: z4.string(),
873
+ nights: z4.array(sleepNightSchema)
874
+ }).passthrough();
875
+ function formatDuration(totalSeconds) {
876
+ const hours = Math.floor(totalSeconds / 3600);
877
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
878
+ if (hours > 0 && minutes > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
879
+ if (hours > 0) return `${hours}h 00m`;
880
+ return `${minutes}m`;
881
+ }
882
+ function registerSleepCommands(program2) {
883
+ const sleep = program2.command("sleep").description("Sleep data from connected wearables");
884
+ registerHealthDataCommand(
885
+ sleep,
886
+ "get",
887
+ "Retrieve nightly sleep data (time in bed, total sleep, sleep stage breakdown)",
888
+ "SLEEP"
889
+ );
890
+ sleep.command("stages").description("Retrieve minute-by-minute sleep stage transitions (Deep, Core, REM, Awake)").option("--from <YYYY-MM-DD>", "Start date", todayIso()).option("--to <YYYY-MM-DD>", "End date").option("--timezone <tz>", "IANA timezone", defaultTimezone()).action(async (opts, cmd) => {
891
+ try {
892
+ const globalOpts = cmd.optsWithGlobals();
893
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
894
+ const to = opts.to ?? opts.from;
895
+ const result = await client.get(
896
+ "/api/goal-entry/sleep-stages",
897
+ { from: opts.from, to, zoneId: opts.timezone },
898
+ sleepStagesResponseSchema
899
+ );
900
+ if (getOutputFormat(cmd) === "json") {
901
+ console.log(formatJson(result));
902
+ return;
903
+ }
904
+ if (result.nights.length === 0) {
905
+ console.log(`No sleep stage data found for ${opts.from}${to !== opts.from ? ` to ${to}` : ""}.`);
906
+ return;
907
+ }
908
+ console.log(`Source: ${result.source}
909
+ `);
910
+ for (const night of result.nights) {
911
+ console.log(`--- ${night.date} ---`);
912
+ for (const stage of night.stages) {
913
+ console.log(` ${stage.start} - ${stage.end} ${stage.stage.padEnd(7)} ${formatDuration(stage.durationSeconds)}`);
914
+ }
915
+ console.log();
916
+ }
917
+ } catch (error) {
918
+ outputError(error);
919
+ }
920
+ });
921
+ }
922
+
923
+ // src/commands/vitals.ts
924
+ function registerVitalsCommands(program2) {
925
+ const vitals = program2.command("vitals").description("Vital signs from connected wearables");
926
+ registerHealthDataCommand(
927
+ vitals,
928
+ "get",
929
+ "Retrieve vital signs (heart rate, HRV, blood pressure, SpO2, glucose, respiratory rate)",
930
+ "VITALS"
931
+ );
932
+ }
933
+
934
+ // src/commands/body-composition.ts
935
+ function registerBodyCompositionCommands(program2) {
936
+ const bodyComp = program2.command("body-composition").description("Body composition data from connected wearables");
937
+ registerHealthDataCommand(
938
+ bodyComp,
939
+ "get",
940
+ "Retrieve body composition (weight, body fat %, muscle mass, BMI, waist circumference)",
941
+ "BODY"
942
+ );
943
+ }
944
+
945
+ // src/commands/connected-devices.ts
946
+ import { z as z5 } from "zod";
947
+ var availableIntegrationSchema = z5.object({
948
+ key: z5.string(),
949
+ name: z5.string(),
950
+ description: z5.string()
951
+ }).passthrough();
952
+ function registerConnectedDevicesCommands(program2) {
953
+ const devices = program2.command("connected-devices").description("Health device integrations and wearable connections");
954
+ devices.command("list").description("List all connected health devices and wearables").action(async (_, cmd) => {
955
+ try {
956
+ const globalOpts = cmd.optsWithGlobals();
957
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
958
+ const result = await client.get(
959
+ "/api/vital/user/connections",
960
+ void 0,
961
+ connectionsResponseSchema
962
+ );
963
+ const rows = [];
964
+ for (const native of result.nativeConnections) {
965
+ rows.push({
966
+ provider: native.provider,
967
+ type: "native",
968
+ syncEnabled: native.syncEnabled ?? false,
969
+ deviceId: native.deviceId ?? ""
970
+ });
971
+ }
972
+ for (const slug of result.connections) {
973
+ if (!rows.some((r) => r.provider === slug)) {
974
+ rows.push({
975
+ provider: slug,
976
+ type: "cloud",
977
+ syncEnabled: true,
978
+ deviceId: ""
979
+ });
980
+ }
981
+ }
982
+ if (rows.length === 0) {
983
+ console.log("No connected devices found.");
984
+ return;
985
+ }
986
+ outputList(cmd, rows, [
987
+ { key: "provider", label: "Provider", width: 20 },
988
+ { key: "type", label: "Type", width: 10 },
989
+ { key: "syncEnabled", label: "Sync", width: 8 },
990
+ { key: "deviceId", label: "Device ID", width: 25 }
991
+ ]);
992
+ } catch (error) {
993
+ outputError(error);
994
+ }
995
+ });
996
+ devices.command("available").description("List health device integrations the user can connect (not currently active)").action(async (_, cmd) => {
997
+ try {
998
+ const globalOpts = cmd.optsWithGlobals();
999
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1000
+ const available = await client.get(
1001
+ "/api/vital/user/available-integrations",
1002
+ void 0,
1003
+ z5.array(availableIntegrationSchema)
1004
+ );
1005
+ if (available.length === 0) {
1006
+ console.log("All available integrations are already connected.");
1007
+ return;
1008
+ }
1009
+ outputList(cmd, available, [
1010
+ { key: "key", label: "Integration Key", width: 22 },
1011
+ { key: "name", label: "Name", width: 16 },
1012
+ { key: "description", label: "Description", width: 60 }
1013
+ ]);
1014
+ } catch (error) {
1015
+ outputError(error);
1016
+ }
1017
+ });
1018
+ devices.command("link").description("Generate connection link for a web-based health device integration").requiredOption("--integration-key <key>", "Integration provider (GARMIN, OURA, WITHINGS, PELOTON, WAHOO, EIGHT_SLEEP)").action(async (opts, cmd) => {
1019
+ try {
1020
+ const globalOpts = cmd.optsWithGlobals();
1021
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1022
+ const result = await client.post(
1023
+ `/api/vital/user/link-token/${opts.integrationKey}`
1024
+ );
1025
+ outputRecord(cmd, result);
1026
+ } catch (error) {
1027
+ outputError(error);
1028
+ }
1029
+ });
1030
+ devices.command("apple-health-code").description("Generate connection code for Apple HealthKit via Junction app").action(async (_, cmd) => {
1031
+ try {
1032
+ const globalOpts = cmd.optsWithGlobals();
1033
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1034
+ const code = await client.post("/api/vital/user/ios-app-code");
1035
+ outputRecord(cmd, { connectionCode: code });
1036
+ } catch (error) {
1037
+ outputError(error);
1038
+ }
1039
+ });
1040
+ devices.command("disconnect").description("Disconnect a health device integration").requiredOption("--integration-key <key>", "Integration provider to disconnect").option("--dry-run", "Preview the disconnection without executing").action(async (opts, cmd) => {
1041
+ try {
1042
+ if (opts.dryRun) {
1043
+ outputRecord(cmd, { action: "connected-devices disconnect", integrationKey: opts.integrationKey, note: "Dry run \u2014 device will not be disconnected." });
1044
+ return;
1045
+ }
1046
+ const globalOpts = cmd.optsWithGlobals();
1047
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1048
+ await client.delete(`/api/vital/user/connection/${opts.integrationKey}`);
1049
+ console.log(`Disconnected ${opts.integrationKey} successfully.`);
1050
+ } catch (error) {
1051
+ outputError(error);
1052
+ }
1053
+ });
1054
+ }
1055
+
1056
+ // src/commands/lab-tests.ts
1057
+ import { z as z6 } from "zod";
1058
+ function registerLabTestsCommands(program2) {
1059
+ const labTests = program2.command("lab-tests").description("Available lab tests for ordering");
1060
+ labTests.command("list").description("List available lab tests with prices and included markers").option("--query <text>", "Search by name or description").option("--popular", "Only show popular tests").option("--loinc-slug <slug>", "Filter by LOINC slug").action(async (opts, cmd) => {
1061
+ try {
1062
+ const globalOpts = cmd.optsWithGlobals();
1063
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1064
+ const params = {};
1065
+ if (opts.query) params.slug = opts.query;
1066
+ if (opts.popular) params.onlyPopular = true;
1067
+ if (opts.loincSlug) params.loincSlug = opts.loincSlug;
1068
+ const results = await client.get(
1069
+ "/api/lab-test/all",
1070
+ params,
1071
+ z6.array(labTestSchema)
1072
+ );
1073
+ outputList(cmd, results.map((t) => ({
1074
+ name: t.name,
1075
+ lab: t.lab,
1076
+ price: t.price,
1077
+ isPopular: t.isPopular,
1078
+ objectKey: t.objectKey
1079
+ })), [
1080
+ { key: "name", label: "Name", width: 35 },
1081
+ { key: "lab", label: "Lab", width: 15 },
1082
+ { key: "price", label: "Price", width: 10, align: "right" },
1083
+ { key: "isPopular", label: "Popular", width: 8 },
1084
+ { key: "objectKey", label: "Object Key", width: 20 }
1085
+ ]);
1086
+ } catch (error) {
1087
+ outputError(error);
1088
+ }
1089
+ });
1090
+ }
1091
+
1092
+ // src/commands/lab-records.ts
1093
+ function registerLabRecordsCommands(program2) {
1094
+ const labRecords = program2.command("lab-records").description("Lab records \u2014 uploaded results and purchased test orders");
1095
+ labRecords.command("list").description("List lab records (both uploaded results and lab orders)").option("--limit <n>", "Results per page", "20").option("--page <n>", "Page number (zero-based)", "0").action(async (opts, cmd) => {
1096
+ try {
1097
+ const globalOpts = cmd.optsWithGlobals();
1098
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1099
+ const page = await client.get(
1100
+ "/api/health-record/all",
1101
+ { page: opts.page, size: opts.limit },
1102
+ springPageSchema(healthRecordSchema)
1103
+ );
1104
+ outputList(cmd, page.content.map((r) => {
1105
+ const nested = r.result;
1106
+ return {
1107
+ externalId: nested?.externalId ?? r.externalId,
1108
+ source: r.source
1109
+ };
1110
+ }), [
1111
+ { key: "externalId", label: "ID", width: 30 },
1112
+ { key: "source", label: "Source", width: 20 }
1113
+ ]);
1114
+ if (!page.last) {
1115
+ console.error(`Page ${page.number + 1} of ${page.totalPages} (${page.totalElements} total records)`);
1116
+ }
1117
+ } catch (error) {
1118
+ outputError(error);
1119
+ }
1120
+ });
1121
+ labRecords.command("detail").description("Get full detail of a lab record by external ID").requiredOption("--record-id <id>", "Lab record external ID").action(async (opts, cmd) => {
1122
+ try {
1123
+ const globalOpts = cmd.optsWithGlobals();
1124
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1125
+ let result;
1126
+ try {
1127
+ result = await client.get(
1128
+ `/api/lab-result/${opts.recordId}`
1129
+ );
1130
+ } catch {
1131
+ result = await client.get(
1132
+ `/api/lab-order/${opts.recordId}`
1133
+ );
1134
+ }
1135
+ outputRecord(cmd, result);
1136
+ } catch (error) {
1137
+ outputError(error);
1138
+ }
1139
+ });
1140
+ }
1141
+
1142
+ // src/commands/lab-orders.ts
1143
+ function registerLabOrdersCommands(program2) {
1144
+ const labOrders = program2.command("lab-orders").description("Lab order management \u2014 scheduling, appointments, and service centers");
1145
+ labOrders.command("initialize").description("Initialize a lab order for processing (order must be in Paid status)").requiredOption("--order-id <id>", "Lab order external ID").option("--dry-run", "Preview the action without executing").action(async (opts, cmd) => {
1146
+ try {
1147
+ if (opts.dryRun) {
1148
+ outputRecord(cmd, { action: "lab-orders initialize", orderId: opts.orderId, note: "Dry run \u2014 order will not be initialized." });
1149
+ return;
1150
+ }
1151
+ const globalOpts = cmd.optsWithGlobals();
1152
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1153
+ const result = await client.post(
1154
+ "/api/lab-test-order/initialize",
1155
+ { order: opts.orderId }
1156
+ );
1157
+ outputRecord(cmd, result);
1158
+ } catch (error) {
1159
+ outputError(error);
1160
+ }
1161
+ });
1162
+ labOrders.command("service-centers").description("Search lab service centers near a ZIP code").requiredOption("--zip-code <zip>", "ZIP code to search near").requiredOption("--order-id <id>", "Lab order external ID").option("--limit <n>", "Maximum results", "6").option("--offset <n>", "Pagination offset", "0").action(async (opts, cmd) => {
1163
+ try {
1164
+ const globalOpts = cmd.optsWithGlobals();
1165
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1166
+ const results = await client.get(
1167
+ "/api/lab-test-order/all-labs",
1168
+ { zipCode: opts.zipCode, labOrderId: opts.orderId }
1169
+ );
1170
+ if (results.length === 0) {
1171
+ console.log("No service centers found near this ZIP code.");
1172
+ return;
1173
+ }
1174
+ outputList(cmd, results, [
1175
+ { key: "name", label: "Name", width: 30 },
1176
+ { key: "city", label: "City", width: 15 },
1177
+ { key: "state", label: "State", width: 8 },
1178
+ { key: "distance", label: "Distance", width: 10 },
1179
+ { key: "siteCode", label: "Site Code", width: 12 }
1180
+ ]);
1181
+ } catch (error) {
1182
+ outputError(error);
1183
+ }
1184
+ });
1185
+ labOrders.command("slots").description("Get available appointment time slots at a service center").requiredOption("--site-code <code>", "Service center site code").requiredOption("--order-id <id>", "Lab order external ID").requiredOption("--timezone <tz>", "IANA timezone").option("--start-date <YYYY-MM-DD>", "Start date for slot search").option("--range-days <n>", "Number of days to search", "7").action(async (opts, cmd) => {
1186
+ try {
1187
+ const globalOpts = cmd.optsWithGlobals();
1188
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1189
+ const params = {
1190
+ siteCode: opts.siteCode,
1191
+ labOrderId: opts.orderId,
1192
+ timezone: opts.timezone
1193
+ };
1194
+ if (opts.startDate) params.startDate = opts.startDate;
1195
+ if (opts.rangeDays) params.rangeDays = opts.rangeDays;
1196
+ const results = await client.get(
1197
+ "/api/lab-test-order/service-center-slots",
1198
+ params
1199
+ );
1200
+ outputList(cmd, results, [
1201
+ { key: "date", label: "Date", width: 12 },
1202
+ { key: "slots", label: "Available Slots", width: 40 }
1203
+ ]);
1204
+ } catch (error) {
1205
+ outputError(error);
1206
+ }
1207
+ });
1208
+ labOrders.command("book").description("Book a blood draw appointment at a service center").requiredOption("--order-id <id>", "Lab order external ID").requiredOption("--booking-key <key>", "Booking key from slots command").requiredOption("--timezone <tz>", "IANA timezone").option("--dry-run", "Preview the booking without executing").action(async (opts, cmd) => {
1209
+ try {
1210
+ if (opts.dryRun) {
1211
+ outputRecord(cmd, { action: "lab-orders book", orderId: opts.orderId, bookingKey: opts.bookingKey, timezone: opts.timezone, note: "Dry run \u2014 appointment will not be booked." });
1212
+ return;
1213
+ }
1214
+ const globalOpts = cmd.optsWithGlobals();
1215
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1216
+ const result = await client.post(
1217
+ "/api/lab-test-order/book-appointment",
1218
+ {
1219
+ labOrderId: opts.orderId,
1220
+ bookingKey: opts.bookingKey,
1221
+ timeZone: opts.timezone,
1222
+ toReschedule: false
1223
+ }
1224
+ );
1225
+ outputRecord(cmd, result);
1226
+ console.log("Appointment booked successfully.");
1227
+ } catch (error) {
1228
+ outputError(error);
1229
+ }
1230
+ });
1231
+ labOrders.command("reschedule").description("Reschedule an existing blood draw appointment").requiredOption("--order-id <id>", "Lab order external ID").requiredOption("--booking-key <key>", "New booking key from slots command").requiredOption("--timezone <tz>", "IANA timezone").option("--dry-run", "Preview the reschedule without executing").action(async (opts, cmd) => {
1232
+ try {
1233
+ if (opts.dryRun) {
1234
+ outputRecord(cmd, { action: "lab-orders reschedule", orderId: opts.orderId, bookingKey: opts.bookingKey, timezone: opts.timezone, note: "Dry run \u2014 appointment will not be rescheduled." });
1235
+ return;
1236
+ }
1237
+ const globalOpts = cmd.optsWithGlobals();
1238
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1239
+ const result = await client.post(
1240
+ "/api/lab-test-order/book-appointment",
1241
+ {
1242
+ labOrderId: opts.orderId,
1243
+ bookingKey: opts.bookingKey,
1244
+ timeZone: opts.timezone,
1245
+ toReschedule: true
1246
+ }
1247
+ );
1248
+ outputRecord(cmd, result);
1249
+ console.log("Appointment rescheduled successfully.");
1250
+ } catch (error) {
1251
+ outputError(error);
1252
+ }
1253
+ });
1254
+ labOrders.command("cancel").description("Cancel a blood draw appointment (run without --reason-id to see available reasons)").requiredOption("--order-id <id>", "Lab order external ID").option("--reason-id <id>", "Cancellation reason ID").option("--dry-run", "Preview the cancellation without executing").action(async (opts, cmd) => {
1255
+ try {
1256
+ if (opts.dryRun) {
1257
+ outputRecord(cmd, { action: "lab-orders cancel", orderId: opts.orderId, reasonId: opts.reasonId ?? null, note: "Dry run \u2014 appointment will not be cancelled." });
1258
+ return;
1259
+ }
1260
+ const globalOpts = cmd.optsWithGlobals();
1261
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1262
+ const result = await client.post(
1263
+ "/api/appointment/cancel-appointment",
1264
+ {
1265
+ labOrderExternalId: opts.orderId,
1266
+ reasonId: opts.reasonId
1267
+ }
1268
+ );
1269
+ outputRecord(cmd, result);
1270
+ } catch (error) {
1271
+ outputError(error);
1272
+ }
1273
+ });
1274
+ }
1275
+
1276
+ // src/commands/lab-results.ts
1277
+ function registerLabResultsCommands(program2) {
1278
+ const labResults = program2.command("lab-results").description("Lab result management \u2014 approve, update biomarkers, update metadata");
1279
+ labResults.command("update-status").description("Update lab result status (APPROVE, ROLLBACK, or REPROCESS)").requiredOption("--result-id <id>", "Lab result external ID").requiredOption("--action <action>", "Action: APPROVE, ROLLBACK, or REPROCESS").action(async (opts, cmd) => {
1280
+ try {
1281
+ const globalOpts = cmd.optsWithGlobals();
1282
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1283
+ const actionMap = {
1284
+ APPROVE: "approve",
1285
+ ROLLBACK: "rollback",
1286
+ REPROCESS: "reprocess"
1287
+ };
1288
+ const action = actionMap[opts.action.toUpperCase()];
1289
+ if (!action) {
1290
+ console.error(`Invalid action: ${opts.action}. Valid: APPROVE, ROLLBACK, REPROCESS`);
1291
+ process.exit(1);
1292
+ }
1293
+ await client.post(`/api/lab-result/${action}/${opts.resultId}`);
1294
+ console.log(`Lab result ${opts.resultId} \u2014 ${opts.action.toUpperCase()} completed.`);
1295
+ } catch (error) {
1296
+ outputError(error);
1297
+ }
1298
+ });
1299
+ labResults.command("update-biomarker").description("Update or delete a biomarker value within an uploaded lab result").requiredOption("--biomarker-id <id>", "Biomarker external ID").option("--action <action>", "Set to DELETE to remove the biomarker").option("--name <name>", "Biomarker name").option("--result <value>", "Biomarker value (e.g. '5.2', '120')").option("--unit <unit>", "Unit (e.g. 'mg/dL', 'ng/mL')").option("--min-range <n>", "Minimum range value").option("--max-range <n>", "Maximum range value").action(async (opts, cmd) => {
1300
+ try {
1301
+ const globalOpts = cmd.optsWithGlobals();
1302
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1303
+ const body = {};
1304
+ if (opts.name) body.name = opts.name;
1305
+ if (opts.result) body.result = opts.result;
1306
+ if (opts.unit) body.unit = opts.unit;
1307
+ if (opts.minRange) body.minRangeValue = Number(opts.minRange);
1308
+ if (opts.maxRange) body.maxRangeValue = Number(opts.maxRange);
1309
+ const result = await client.put(
1310
+ `/api/mocked-result/${opts.biomarkerId}`,
1311
+ body
1312
+ );
1313
+ outputRecord(cmd, result);
1314
+ } catch (error) {
1315
+ outputError(error);
1316
+ }
1317
+ });
1318
+ labResults.command("update-metadata").description("Update metadata of an uploaded lab result (patient info and test details)").requiredOption("--result-id <id>", "Lab result external ID").option("--patient-name <name>", "Patient name").option("--patient-sex <sex>", "Patient sex: MALE or FEMALE").option("--dob <date>", "Date of birth").option("--lab-name <name>", "Lab name").option("--ordering-physician <name>", "Ordering physician").option("--date-collected <date>", "Date collected (ISO-8601)").option("--fasting", "Mark as fasting test").action(async (opts, cmd) => {
1319
+ try {
1320
+ const globalOpts = cmd.optsWithGlobals();
1321
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1322
+ const patientBody = {};
1323
+ if (opts.patientName) patientBody.patientName = opts.patientName;
1324
+ if (opts.patientSex) patientBody.patientSex = opts.patientSex;
1325
+ if (opts.dob) patientBody.dob = opts.dob;
1326
+ if (Object.keys(patientBody).length > 0) {
1327
+ await client.put(`/api/lab-result/patient/${opts.resultId}`, patientBody);
1328
+ }
1329
+ const testBody = {};
1330
+ if (opts.labName) testBody.labName = opts.labName;
1331
+ if (opts.orderingPhysician) testBody.orderingPhysician = opts.orderingPhysician;
1332
+ if (opts.dateCollected) testBody.dateCollected = opts.dateCollected;
1333
+ if (opts.fasting !== void 0) testBody.fasting = opts.fasting;
1334
+ if (Object.keys(testBody).length > 0) {
1335
+ await client.put(`/api/lab-result/test-detail/${opts.resultId}`, testBody);
1336
+ }
1337
+ console.log(`Lab result ${opts.resultId} metadata updated.`);
1338
+ } catch (error) {
1339
+ outputError(error);
1340
+ }
1341
+ });
1342
+ labResults.command("upload").description("Upload a lab result PDF for processing").requiredOption("--file <path>", "Path to the PDF file").action(async (opts, cmd) => {
1343
+ try {
1344
+ const { existsSync: existsSync2 } = await import("fs");
1345
+ const { resolve } = await import("path");
1346
+ const filePath = resolve(opts.file);
1347
+ if (!existsSync2(filePath)) {
1348
+ console.error(`File not found: ${filePath}`);
1349
+ process.exit(1);
1350
+ }
1351
+ if (!filePath.toLowerCase().endsWith(".pdf")) {
1352
+ console.error("Only PDF files are supported.");
1353
+ process.exit(1);
1354
+ }
1355
+ const globalOpts = cmd.optsWithGlobals();
1356
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1357
+ const result = await client.upload(
1358
+ "/api/lab-result/agent-upload",
1359
+ filePath
1360
+ );
1361
+ if (typeof result === "string") {
1362
+ console.log(result);
1363
+ } else {
1364
+ outputRecord(cmd, result);
1365
+ }
1366
+ } catch (error) {
1367
+ outputError(error);
1368
+ }
1369
+ });
1370
+ }
1371
+
1372
+ // src/commands/purchases.ts
1373
+ import { z as z7 } from "zod";
1374
+ var checkoutResponseSchema = z7.object({
1375
+ checkoutUrl: z7.string(),
1376
+ testName: z7.string(),
1377
+ originalPrice: z7.number().nullable().optional(),
1378
+ finalPrice: z7.number().nullable().optional(),
1379
+ discount: z7.string().nullable().optional(),
1380
+ promotionCode: z7.string().nullable().optional()
1381
+ }).passthrough();
1382
+ function registerPurchasesCommands(program2) {
1383
+ const purchases = program2.command("purchases").description("Lab test purchases and payment methods");
1384
+ purchases.command("payment-methods").description("List saved payment methods (credit/debit cards)").action(async (_, cmd) => {
1385
+ try {
1386
+ const globalOpts = cmd.optsWithGlobals();
1387
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1388
+ const results = await client.get(
1389
+ "/api/payment-methods/all",
1390
+ void 0,
1391
+ z7.array(paymentMethodSchema)
1392
+ );
1393
+ if (results.length === 0) {
1394
+ console.log("No saved payment methods found.");
1395
+ return;
1396
+ }
1397
+ outputList(cmd, results.map((m) => ({
1398
+ brand: m.brand,
1399
+ last4: m.last4,
1400
+ expiresAt: m.expiresAt,
1401
+ active: m.active,
1402
+ externalId: m.externalId
1403
+ })), [
1404
+ { key: "brand", label: "Brand", width: 15 },
1405
+ { key: "last4", label: "Last 4", width: 8 },
1406
+ { key: "expiresAt", label: "Expires", width: 12 },
1407
+ { key: "active", label: "Active", width: 8 },
1408
+ { key: "externalId", label: "ID", width: 20 }
1409
+ ]);
1410
+ } catch (error) {
1411
+ outputError(error);
1412
+ }
1413
+ });
1414
+ purchases.command("buy").description("Purchase a lab test using a saved payment method").requiredOption("--test-key <key>", "Lab test object key").requiredOption("--payment-method-id <id>", "Payment method external ID").option("--promo-code <code>", "Promotion code").option("--dry-run", "Preview what would be purchased without executing").action(async (opts, cmd) => {
1415
+ try {
1416
+ if (opts.dryRun) {
1417
+ outputRecord(cmd, {
1418
+ action: "purchases buy",
1419
+ labTestObjectKey: opts.testKey,
1420
+ paymentMethodExternalId: opts.paymentMethodId,
1421
+ promotionCode: opts.promoCode ?? null,
1422
+ note: "Dry run \u2014 no purchase will be made."
1423
+ });
1424
+ return;
1425
+ }
1426
+ const globalOpts = cmd.optsWithGlobals();
1427
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1428
+ const result = await client.post(
1429
+ "/api/purchase",
1430
+ {
1431
+ labTestObjectKey: opts.testKey,
1432
+ paymentMethodExternalId: opts.paymentMethodId,
1433
+ promotionCode: opts.promoCode
1434
+ }
1435
+ );
1436
+ outputRecord(cmd, result);
1437
+ console.log("Purchase completed successfully.");
1438
+ } catch (error) {
1439
+ outputError(error);
1440
+ }
1441
+ });
1442
+ purchases.command("checkout").description("Generate a Stripe Checkout payment link for a lab test (for users without saved cards)").requiredOption("--test-key <key>", "Lab test object key").requiredOption("--success-url <url>", "URL to redirect after successful payment").requiredOption("--cancel-url <url>", "URL to redirect if checkout is cancelled").option("--promo-code <code>", "Promotion code").option("--dry-run", "Preview the checkout request without executing").action(async (opts, cmd) => {
1443
+ try {
1444
+ if (opts.dryRun) {
1445
+ outputRecord(cmd, {
1446
+ action: "purchases checkout",
1447
+ labTestObjectKey: opts.testKey,
1448
+ successUrl: opts.successUrl,
1449
+ cancelUrl: opts.cancelUrl,
1450
+ promotionCode: opts.promoCode ?? null,
1451
+ note: "Dry run \u2014 no checkout session will be created."
1452
+ });
1453
+ return;
1454
+ }
1455
+ const globalOpts = cmd.optsWithGlobals();
1456
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1457
+ const result = await client.post(
1458
+ "/api/purchase/checkout",
1459
+ {
1460
+ labTestObjectKey: opts.testKey,
1461
+ successUrl: opts.successUrl,
1462
+ cancelUrl: opts.cancelUrl,
1463
+ promotionCode: opts.promoCode
1464
+ },
1465
+ void 0,
1466
+ checkoutResponseSchema
1467
+ );
1468
+ outputRecord(cmd, result);
1469
+ } catch (error) {
1470
+ outputError(error);
1471
+ }
1472
+ });
1473
+ }
1474
+
1475
+ // src/commands/smart-listings.ts
1476
+ import { z as z8 } from "zod";
1477
+ var companySearchResultSchema = z8.object({
1478
+ externalId: z8.string(),
1479
+ name: z8.string().nullable().optional(),
1480
+ description: z8.string().nullable().optional(),
1481
+ city: z8.string().nullable().optional(),
1482
+ state: z8.string().nullable().optional(),
1483
+ avatarUrl: z8.string().nullable().optional(),
1484
+ rating: z8.number().nullable().optional()
1485
+ }).passthrough();
1486
+ function registerSmartListingsCommands(program2) {
1487
+ const smartListings = program2.command("smart-listings").description("Search and browse wellness providers (SmartListings)");
1488
+ smartListings.command("search").description("Search SmartListings by name, description, tags, or location").option("--query <text>", "Text search query").option("--lat <n>", "Latitude for proximity search").option("--lng <n>", "Longitude for proximity search").option("--radius <km>", "Radius in km (1, 2, 5, 10, 20)").option("--following", "Only show followed providers").option("--limit <n>", "Results per page", "10").option("--page <n>", "Page number (zero-based)", "0").action(async (opts, cmd) => {
1489
+ try {
1490
+ const globalOpts = cmd.optsWithGlobals();
1491
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1492
+ const body = {};
1493
+ if (opts.query) body.search = opts.query;
1494
+ if (opts.lat) body.latitude = Number(opts.lat);
1495
+ if (opts.lng) body.longitude = Number(opts.lng);
1496
+ if (opts.radius) body.radiusKm = Number(opts.radius);
1497
+ if (opts.following) body.onlyFollowing = true;
1498
+ const page = await client.post(
1499
+ "/api/company/partners/filtered",
1500
+ body,
1501
+ { page: opts.page, size: opts.limit },
1502
+ simplePageSchema(companySearchResultSchema)
1503
+ );
1504
+ outputList(cmd, page.content.map((c) => ({
1505
+ name: c.name,
1506
+ city: c.city,
1507
+ state: c.state,
1508
+ rating: c.rating,
1509
+ externalId: c.externalId
1510
+ })), [
1511
+ { key: "name", label: "Name", width: 30 },
1512
+ { key: "city", label: "City", width: 15 },
1513
+ { key: "state", label: "State", width: 8 },
1514
+ { key: "rating", label: "Rating", width: 8, align: "right" },
1515
+ { key: "externalId", label: "ID", width: 20 }
1516
+ ]);
1517
+ if (!page.last) {
1518
+ console.error(`Page ${page.number + 1} \u2014 ${page.totalResults} total results`);
1519
+ }
1520
+ } catch (error) {
1521
+ outputError(error);
1522
+ }
1523
+ });
1524
+ smartListings.command("detail").description("Get full details of a SmartListing by ID").requiredOption("--id <externalId>", "SmartListing external ID").action(async (opts, cmd) => {
1525
+ try {
1526
+ const globalOpts = cmd.optsWithGlobals();
1527
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1528
+ const result = await client.get(
1529
+ `/api/company/detail/${opts.id}`
1530
+ );
1531
+ outputRecord(cmd, result);
1532
+ } catch (error) {
1533
+ outputError(error);
1534
+ }
1535
+ });
1536
+ }
1537
+
1538
+ // src/commands/workflow.ts
1539
+ import { z as z9 } from "zod";
1540
+ function isOutOfRange(r) {
1541
+ if (r.value == null) return false;
1542
+ if (r.minRangeValue != null && r.value < r.minRangeValue) return true;
1543
+ if (r.maxRangeValue != null && r.value > r.maxRangeValue) return true;
1544
+ return false;
1545
+ }
1546
+ function rangeLabel(r) {
1547
+ if (r.value == null) return "";
1548
+ if (r.minRangeValue != null && r.value < r.minRangeValue) return "below range";
1549
+ if (r.maxRangeValue != null && r.value > r.maxRangeValue) return "above range";
1550
+ return "";
1551
+ }
1552
+ function deviceNames(devices) {
1553
+ const names = devices.nativeConnections.map((c) => c.provider);
1554
+ for (const slug of devices.connections) {
1555
+ if (!names.includes(slug)) names.push(slug);
1556
+ }
1557
+ return names;
1558
+ }
1559
+ async function fetchDailyBrief(client, biomarkerLimit) {
1560
+ const [profile, bioAgeList, biomarkers, devices] = await Promise.all([
1561
+ client.get("/api/betterness-user/detail", void 0, betternessUserSchema),
1562
+ client.get("/api/biological-age", { size: 1, sort: "date,desc" }, z9.array(biologicalAgeResultSchema)),
1563
+ client.get("/api/biomarker-result/all", { size: biomarkerLimit }, z9.array(biomarkerResultSchema)),
1564
+ client.get("/api/vital/user/connections", void 0, connectionsResponseSchema)
1565
+ ]);
1566
+ const outOfRange = biomarkers.filter(isOutOfRange);
1567
+ const bioAge = bioAgeList.length > 0 ? bioAgeList[0] : null;
1568
+ return {
1569
+ user: {
1570
+ firstName: profile.firstName ?? null,
1571
+ lastName: profile.lastName ?? null,
1572
+ email: profile.email ?? null
1573
+ },
1574
+ biologicalAge: bioAge ? {
1575
+ currentAge: bioAge.currentAge ?? null,
1576
+ biologicalAge: bioAge.biologicalAge ?? null,
1577
+ source: bioAge.source ?? null,
1578
+ measurementDate: bioAge.measurementDate ?? null
1579
+ } : null,
1580
+ biomarkersOutOfRange: outOfRange.map((r) => ({
1581
+ name: r.biomarker.name,
1582
+ value: r.value ?? null,
1583
+ unit: r.unit ?? null,
1584
+ minRange: r.minRangeValue ?? null,
1585
+ maxRange: r.maxRangeValue ?? null,
1586
+ dateCollected: r.dateCollected ?? null
1587
+ })),
1588
+ connectedDevices: deviceNames(devices),
1589
+ recentBiomarkerCount: biomarkers.length
1590
+ };
1591
+ }
1592
+ function printDailyBrief(brief) {
1593
+ const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
1594
+ console.log(`DAILY BRIEF \u2014 ${date}`);
1595
+ console.log(`User: ${[brief.user.firstName, brief.user.lastName].filter(Boolean).join(" ") || "Unknown"}`);
1596
+ console.log();
1597
+ if (brief.biologicalAge) {
1598
+ const ba = brief.biologicalAge;
1599
+ if (ba.biologicalAge != null && ba.currentAge != null) {
1600
+ console.log(`Biological Age: ${ba.biologicalAge} (chronological: ${ba.currentAge})`);
1601
+ } else if (ba.biologicalAge != null) {
1602
+ console.log(`Biological Age: ${ba.biologicalAge}`);
1603
+ } else {
1604
+ console.log("Biological Age: No data available");
1605
+ }
1606
+ if (ba.measurementDate) console.log(` Measured: ${ba.measurementDate}`);
1607
+ console.log();
1608
+ }
1609
+ if (brief.biomarkersOutOfRange.length > 0) {
1610
+ console.log(`Biomarkers out of range (${brief.biomarkersOutOfRange.length}):`);
1611
+ for (const b of brief.biomarkersOutOfRange) {
1612
+ const val = b.value != null ? `${b.value}${b.unit ? " " + b.unit : ""}` : "N/A";
1613
+ let label = "";
1614
+ if (b.value != null && b.minRange != null && b.value < b.minRange) label = "below range";
1615
+ if (b.value != null && b.maxRange != null && b.value > b.maxRange) label = "above range";
1616
+ console.log(` - ${b.name}: ${val}${label ? ` (${label})` : ""}`);
1617
+ }
1618
+ } else {
1619
+ console.log("All biomarkers within range.");
1620
+ }
1621
+ console.log();
1622
+ console.log(`Connected devices: ${brief.connectedDevices.length > 0 ? brief.connectedDevices.join(", ") : "None"}`);
1623
+ console.log(`Recent biomarkers tracked: ${brief.recentBiomarkerCount}`);
1624
+ }
1625
+ async function fetchNextActions(client, biomarkerLimit) {
1626
+ const [biomarkers, devices] = await Promise.all([
1627
+ client.get("/api/biomarker-result/all", { size: biomarkerLimit }, z9.array(biomarkerResultSchema)),
1628
+ client.get("/api/vital/user/connections", void 0, connectionsResponseSchema)
1629
+ ]);
1630
+ const actions = [];
1631
+ const outOfRange = biomarkers.filter(isOutOfRange);
1632
+ for (const r of outOfRange) {
1633
+ const label = rangeLabel(r);
1634
+ actions.push({
1635
+ type: "biomarker",
1636
+ priority: "high",
1637
+ description: `${r.biomarker.name} is ${label}`,
1638
+ detail: `Value: ${r.value}${r.unit ? " " + r.unit : ""} (range: ${r.minRangeValue ?? "?"}-${r.maxRangeValue ?? "?"})${r.dateCollected ? `, tested: ${r.dateCollected}` : ""}`
1639
+ });
1640
+ }
1641
+ const now = Date.now();
1642
+ const ninetyDaysMs = 90 * 24 * 60 * 60 * 1e3;
1643
+ const latestByName = /* @__PURE__ */ new Map();
1644
+ for (const r of biomarkers) {
1645
+ const existing = latestByName.get(r.biomarker.name);
1646
+ if (!existing || r.dateCollected && (!existing.dateCollected || r.dateCollected > existing.dateCollected)) {
1647
+ latestByName.set(r.biomarker.name, r);
1648
+ }
1649
+ }
1650
+ for (const [name, r] of latestByName) {
1651
+ if (!r.dateCollected) continue;
1652
+ const testDate = new Date(r.dateCollected).getTime();
1653
+ if (now - testDate > ninetyDaysMs) {
1654
+ const daysAgo = Math.floor((now - testDate) / (24 * 60 * 60 * 1e3));
1655
+ actions.push({
1656
+ type: "retest",
1657
+ priority: "medium",
1658
+ description: `${name} retest recommended`,
1659
+ detail: `Last tested ${daysAgo} days ago (${r.dateCollected})`
1660
+ });
1661
+ }
1662
+ }
1663
+ const totalDevices = devices.connections.length + devices.nativeConnections.length;
1664
+ if (totalDevices === 0) {
1665
+ actions.push({
1666
+ type: "device",
1667
+ priority: "low",
1668
+ description: "No health devices connected",
1669
+ detail: "Connect a wearable to track activity, sleep, and vitals automatically."
1670
+ });
1671
+ }
1672
+ const priorityOrder = { high: 0, medium: 1, low: 2 };
1673
+ actions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
1674
+ return {
1675
+ actions,
1676
+ summary: { biomarkersOutOfRange: outOfRange.length, totalBiomarkers: biomarkers.length }
1677
+ };
1678
+ }
1679
+ function printNextActions(result) {
1680
+ if (result.actions.length === 0) {
1681
+ console.log("No recommended actions at this time. Everything looks good!");
1682
+ return;
1683
+ }
1684
+ console.log(`RECOMMENDED ACTIONS (${result.actions.length})`);
1685
+ console.log();
1686
+ const grouped = { high: [], medium: [], low: [] };
1687
+ for (const a of result.actions) grouped[a.priority].push(a);
1688
+ const labels = { high: "HIGH PRIORITY", medium: "MEDIUM", low: "LOW" };
1689
+ for (const [priority, actions] of Object.entries(grouped)) {
1690
+ if (actions.length === 0) continue;
1691
+ console.log(`${labels[priority]}:`);
1692
+ for (const a of actions) {
1693
+ console.log(` - [${a.type}] ${a.description}`);
1694
+ if (a.detail) console.log(` ${a.detail}`);
1695
+ }
1696
+ console.log();
1697
+ }
1698
+ console.log(`Summary: ${result.summary.biomarkersOutOfRange} of ${result.summary.totalBiomarkers} biomarkers out of range.`);
1699
+ }
1700
+ function registerWorkflowCommands(program2) {
1701
+ const workflow = program2.command("workflow").description("Composite commands that aggregate data from multiple endpoints");
1702
+ workflow.command("daily-brief").description("Daily health summary \u2014 profile, biological age, biomarkers, devices").option("--biomarker-limit <n>", "Maximum biomarkers to fetch", "20").action(async (opts, cmd) => {
1703
+ try {
1704
+ const globalOpts = cmd.optsWithGlobals();
1705
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1706
+ const brief = await fetchDailyBrief(client, opts.biomarkerLimit);
1707
+ const format = getOutputFormat(cmd);
1708
+ if (format === "json") {
1709
+ console.log(formatJson(brief));
1710
+ } else {
1711
+ printDailyBrief(brief);
1712
+ }
1713
+ } catch (error) {
1714
+ outputError(error);
1715
+ }
1716
+ });
1717
+ workflow.command("next-actions").description("Recommended next actions based on biomarkers and health data").option("--biomarker-limit <n>", "Maximum biomarkers to analyze", "50").action(async (opts, cmd) => {
1718
+ try {
1719
+ const globalOpts = cmd.optsWithGlobals();
1720
+ const client = new ApiClient({ apiKey: globalOpts.apiKey });
1721
+ const result = await fetchNextActions(client, opts.biomarkerLimit);
1722
+ const format = getOutputFormat(cmd);
1723
+ if (format === "json") {
1724
+ console.log(formatJson(result));
1725
+ } else {
1726
+ printNextActions(result);
1727
+ }
1728
+ } catch (error) {
1729
+ outputError(error);
1730
+ }
1731
+ });
1732
+ }
1733
+
1734
+ // src/commands/schema.ts
1735
+ function buildSchema(cmd) {
1736
+ const schema = {
1737
+ name: cmd.name(),
1738
+ description: cmd.description()
1739
+ };
1740
+ const opts = cmd.options.filter((o) => !o.hidden);
1741
+ if (opts.length > 0) {
1742
+ schema.options = opts.map((o) => {
1743
+ const entry = {
1744
+ flags: o.flags,
1745
+ description: o.description
1746
+ };
1747
+ if (o.defaultValue !== void 0) entry.default = o.defaultValue;
1748
+ return entry;
1749
+ });
1750
+ }
1751
+ const subs = cmd.commands;
1752
+ if (subs.length > 0) {
1753
+ schema.subcommands = subs.filter((s) => s.name() !== "help").map(buildSchema);
1754
+ }
1755
+ return schema;
1756
+ }
1757
+ function printSchemaTable(schema, prefix = "") {
1758
+ const fullName = prefix ? `${prefix} ${schema.name}` : schema.name;
1759
+ if (schema.subcommands && schema.subcommands.length > 0) {
1760
+ for (const sub of schema.subcommands) {
1761
+ printSchemaTable(sub, fullName);
1762
+ }
1763
+ return;
1764
+ }
1765
+ console.log(fullName);
1766
+ console.log(` ${schema.description}`);
1767
+ if (schema.options && schema.options.length > 0) {
1768
+ console.log(" Options:");
1769
+ for (const opt of schema.options) {
1770
+ const def = opt.default !== void 0 ? ` (default: ${opt.default})` : "";
1771
+ console.log(` ${opt.flags} ${opt.description}${def}`);
1772
+ }
1773
+ }
1774
+ console.log();
1775
+ }
1776
+ function registerSchemaCommand(program2) {
1777
+ program2.command("schema").description("Discover available commands, options, and response formats").argument("[command-path]", "Command path (e.g. biomarkers, biomarkers.search)").action((commandPath, _, cmd) => {
1778
+ const root = cmd.parent;
1779
+ const format = getOutputFormat(cmd);
1780
+ if (!commandPath) {
1781
+ const allSchemas = root.commands.filter((c) => c.name() !== "schema" && c.name() !== "help").map(buildSchema);
1782
+ if (format === "json") {
1783
+ console.log(formatJson(allSchemas));
1784
+ } else {
1785
+ for (const s of allSchemas) {
1786
+ printSchemaTable(s);
1787
+ }
1788
+ }
1789
+ return;
1790
+ }
1791
+ const parts = commandPath.split(".");
1792
+ let target = root;
1793
+ for (const part of parts) {
1794
+ target = target.commands.find((c) => c.name() === part);
1795
+ if (!target) {
1796
+ console.error(`Unknown command: ${commandPath}`);
1797
+ process.exit(1);
1798
+ }
1799
+ }
1800
+ const schema = buildSchema(target);
1801
+ if (format === "json") {
1802
+ console.log(formatJson(schema));
1803
+ } else {
1804
+ printSchemaTable(schema);
1805
+ }
1806
+ });
1807
+ }
1808
+
1809
+ // src/program.ts
1810
+ function createProgram() {
1811
+ const program2 = new Command();
1812
+ program2.name("betterness").description("Betterness CLI - Agent-first terminal interface for the Betterness platform").version("0.1.0").option("--api-key <key>", "API key (overrides env and stored credentials)").option("--json", "Output as JSON").option("--markdown", "Output as Markdown").option("--quiet", "Suppress output (exit code only)");
1813
+ registerAuthCommands(program2);
1814
+ registerProfileCommands(program2);
1815
+ registerBiomarkersCommands(program2);
1816
+ registerBiologicalAgeCommands(program2);
1817
+ registerActivityCommands(program2);
1818
+ registerSleepCommands(program2);
1819
+ registerVitalsCommands(program2);
1820
+ registerBodyCompositionCommands(program2);
1821
+ registerConnectedDevicesCommands(program2);
1822
+ registerLabTestsCommands(program2);
1823
+ registerLabRecordsCommands(program2);
1824
+ registerLabOrdersCommands(program2);
1825
+ registerLabResultsCommands(program2);
1826
+ registerPurchasesCommands(program2);
1827
+ registerSmartListingsCommands(program2);
1828
+ registerWorkflowCommands(program2);
1829
+ registerSchemaCommand(program2);
1830
+ return program2;
1831
+ }
1832
+
1833
+ // src/index.ts
1834
+ var program = createProgram();
1835
+ program.parse();