@betterness/cli 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,10 +3,6 @@
3
3
  // src/program.ts
4
4
  import { Command } from "commander";
5
5
 
6
- // src/commands/auth.ts
7
- import { createInterface } from "readline/promises";
8
- import { stdin, stdout } from "process";
9
-
10
6
  // src/auth/credentialStore.ts
11
7
  import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
12
8
  import { join } from "path";
@@ -45,6 +41,63 @@ function deleteCredentials() {
45
41
  return true;
46
42
  }
47
43
 
44
+ // src/auth/tokenStore.ts
45
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, unlinkSync as unlinkSync2 } from "fs";
46
+ import { join as join2 } from "path";
47
+ import { homedir as homedir2 } from "os";
48
+ var CONFIG_DIR2 = join2(homedir2(), ".betterness");
49
+ var TOKENS_FILE = join2(CONFIG_DIR2, "tokens.json");
50
+ function ensureConfigDir2() {
51
+ if (!existsSync2(CONFIG_DIR2)) {
52
+ mkdirSync2(CONFIG_DIR2, { recursive: true, mode: 448 });
53
+ }
54
+ }
55
+ function loadTokens() {
56
+ if (!existsSync2(TOKENS_FILE)) {
57
+ return null;
58
+ }
59
+ try {
60
+ const content = readFileSync2(TOKENS_FILE, "utf-8");
61
+ return JSON.parse(content);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ function saveTokens(tokens) {
67
+ ensureConfigDir2();
68
+ const data = {
69
+ ...tokens,
70
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
71
+ };
72
+ writeFileSync2(TOKENS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
73
+ }
74
+ function deleteTokens() {
75
+ if (!existsSync2(TOKENS_FILE)) {
76
+ return false;
77
+ }
78
+ unlinkSync2(TOKENS_FILE);
79
+ return true;
80
+ }
81
+ function isTokenExpired(tokens) {
82
+ return tokens.expiresAt < Date.now() / 1e3 + 60;
83
+ }
84
+
85
+ // src/auth/oauthFlow.ts
86
+ import { createServer } from "http";
87
+ import { URL as URL2 } from "url";
88
+
89
+ // src/auth/pkce.ts
90
+ import { randomBytes, createHash } from "crypto";
91
+ function generateCodeVerifier() {
92
+ return randomBytes(32).toString("base64url");
93
+ }
94
+ function generateCodeChallenge(verifier) {
95
+ return createHash("sha256").update(verifier).digest("base64url");
96
+ }
97
+ function generateState() {
98
+ return randomBytes(16).toString("hex");
99
+ }
100
+
48
101
  // src/types/errors.ts
49
102
  var CliError = class extends Error {
50
103
  code;
@@ -63,8 +116,221 @@ var CliError = class extends Error {
63
116
  }
64
117
  };
65
118
 
119
+ // src/auth/oauthFlow.ts
120
+ var LOGIN_TIMEOUT_MS = 12e4;
121
+ var CALLBACK_PORT = 19847;
122
+ var PAGE_STYLE = `
123
+ body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e;color:#e0e0e0}
124
+ .card{text-align:center;padding:2.5rem 3rem;background:#16213e;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.4)}
125
+ h2{margin:0 0 .5rem;color:#a8dadc}
126
+ p{margin:0;color:#8d99ae}
127
+ .error h2{color:#e63946}
128
+ .error p{color:#8d99ae}
129
+ `;
130
+ var SUCCESS_HTML = `<!DOCTYPE html>
131
+ <html><head><title>Betterness</title><style>${PAGE_STYLE}</style></head>
132
+ <body><div class="card">
133
+ <h2>Authentication successful</h2>
134
+ <p>You can close this tab and return to the terminal.</p>
135
+ </div></body></html>`;
136
+ var ERROR_HTML = `<!DOCTYPE html>
137
+ <html><head><title>Betterness</title><style>${PAGE_STYLE}</style></head>
138
+ <body><div class="card error">
139
+ <h2>Authentication failed</h2>
140
+ <p>Please try again from the terminal.</p>
141
+ </div></body></html>`;
142
+ function getOAuthConfig() {
143
+ const domain = "betterness.us.auth0.com";
144
+ const clientId = "g4lqYHRQb2QMgdRKIlKwoTJl6eu41pWn";
145
+ const audience = "https://api.betterness.ai";
146
+ if (!domain || !clientId || !audience) {
147
+ throw new CliError(
148
+ "OAuth is not configured. AUTH0_DOMAIN, AUTH0_CLIENT_ID, and AUTH0_AUDIENCE must be set at build time.",
149
+ "OAUTH_CONFIG_MISSING"
150
+ );
151
+ }
152
+ return { domain, clientId, audience };
153
+ }
154
+ function decodeIdTokenPayload(idToken) {
155
+ const parts = idToken.split(".");
156
+ if (parts.length !== 3) return {};
157
+ try {
158
+ const payload = Buffer.from(parts[1], "base64url").toString("utf-8");
159
+ return JSON.parse(payload);
160
+ } catch {
161
+ return {};
162
+ }
163
+ }
164
+ async function startOAuthLogin() {
165
+ const { domain, clientId, audience } = getOAuthConfig();
166
+ const codeVerifier = generateCodeVerifier();
167
+ const codeChallenge = generateCodeChallenge(codeVerifier);
168
+ const state = generateState();
169
+ const { code, redirectUri } = await waitForCallback(domain, clientId, audience, codeChallenge, state);
170
+ const tokenResponse = await exchangeCode({
171
+ domain,
172
+ clientId,
173
+ code,
174
+ codeVerifier,
175
+ redirectUri
176
+ });
177
+ const claims = tokenResponse.idToken ? decodeIdTokenPayload(tokenResponse.idToken) : {};
178
+ return {
179
+ ...tokenResponse,
180
+ email: claims.email,
181
+ name: claims.name
182
+ };
183
+ }
184
+ async function waitForCallback(domain, clientId, audience, codeChallenge, state) {
185
+ return new Promise((resolve, reject) => {
186
+ let settled = false;
187
+ const server = createServer((req, res) => {
188
+ const url = new URL2(req.url ?? "/", "http://localhost");
189
+ if (url.pathname !== "/callback") {
190
+ res.writeHead(404);
191
+ res.end("Not found");
192
+ return;
193
+ }
194
+ const code = url.searchParams.get("code");
195
+ const callbackState = url.searchParams.get("state");
196
+ const error = url.searchParams.get("error");
197
+ if (error) {
198
+ const description = url.searchParams.get("error_description") ?? error;
199
+ res.writeHead(400, { "Content-Type": "text/html" });
200
+ res.end(ERROR_HTML);
201
+ settle(false, new CliError(`Auth0 error: ${description}`, "OAUTH_AUTH_ERROR"));
202
+ return;
203
+ }
204
+ if (!code || callbackState !== state) {
205
+ res.writeHead(400, { "Content-Type": "text/html" });
206
+ res.end(ERROR_HTML);
207
+ settle(false, new CliError("Invalid callback: missing code or state mismatch.", "OAUTH_STATE_MISMATCH"));
208
+ return;
209
+ }
210
+ res.writeHead(200, { "Content-Type": "text/html" });
211
+ res.end(SUCCESS_HTML);
212
+ const port = server.address().port;
213
+ settle(true, { code, redirectUri: `http://localhost:${port}/callback` });
214
+ });
215
+ const timeout = setTimeout(() => {
216
+ settle(false, new CliError("Login timed out. Please try again.", "OAUTH_TIMEOUT"));
217
+ }, LOGIN_TIMEOUT_MS);
218
+ function settle(success, value) {
219
+ if (settled) return;
220
+ settled = true;
221
+ clearTimeout(timeout);
222
+ server.close();
223
+ if (success) {
224
+ resolve(value);
225
+ } else {
226
+ reject(value);
227
+ }
228
+ }
229
+ server.on("error", (err) => {
230
+ if (err.code === "EADDRINUSE") {
231
+ settle(false, new CliError(
232
+ `Port ${CALLBACK_PORT} is already in use. Is another login attempt running?`,
233
+ "OAUTH_PORT_IN_USE"
234
+ ));
235
+ } else {
236
+ settle(false, new CliError(`Server error: ${err.message}`, "OAUTH_SERVER_ERROR"));
237
+ }
238
+ });
239
+ server.listen(CALLBACK_PORT, "127.0.0.1", () => {
240
+ const { port } = server.address();
241
+ const redirectUri = `http://localhost:${port}/callback`;
242
+ const authorizeUrl = new URL2(`https://${domain}/authorize`);
243
+ authorizeUrl.searchParams.set("client_id", clientId);
244
+ authorizeUrl.searchParams.set("redirect_uri", redirectUri);
245
+ authorizeUrl.searchParams.set("response_type", "code");
246
+ authorizeUrl.searchParams.set("scope", "openid profile email offline_access");
247
+ authorizeUrl.searchParams.set("audience", audience);
248
+ authorizeUrl.searchParams.set("code_challenge", codeChallenge);
249
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
250
+ authorizeUrl.searchParams.set("state", state);
251
+ console.error(`
252
+ Open this URL to log in:
253
+
254
+ ${authorizeUrl.toString()}
255
+ `);
256
+ console.error("Waiting for authentication... (press Ctrl+C to cancel)\n");
257
+ });
258
+ });
259
+ }
260
+ async function exchangeCode(params) {
261
+ const response = await fetch(`https://${params.domain}/oauth/token`, {
262
+ method: "POST",
263
+ headers: { "Content-Type": "application/json" },
264
+ body: JSON.stringify({
265
+ grant_type: "authorization_code",
266
+ client_id: params.clientId,
267
+ code: params.code,
268
+ code_verifier: params.codeVerifier,
269
+ redirect_uri: params.redirectUri
270
+ })
271
+ });
272
+ if (!response.ok) {
273
+ const body = await response.text();
274
+ throw new CliError(
275
+ `Token exchange failed (${response.status}): ${body}`,
276
+ "OAUTH_TOKEN_EXCHANGE_FAILED"
277
+ );
278
+ }
279
+ const data = await response.json();
280
+ if (!data.access_token) {
281
+ throw new CliError("Token exchange returned no access token.", "OAUTH_TOKEN_EXCHANGE_FAILED");
282
+ }
283
+ if (!data.refresh_token) {
284
+ throw new CliError(
285
+ "Token exchange returned no refresh token. Ensure the Auth0 application has refresh token rotation enabled.",
286
+ "OAUTH_TOKEN_EXCHANGE_FAILED"
287
+ );
288
+ }
289
+ return {
290
+ accessToken: data.access_token,
291
+ refreshToken: data.refresh_token,
292
+ expiresIn: data.expires_in ?? 86400,
293
+ idToken: data.id_token
294
+ };
295
+ }
296
+
297
+ // src/auth/tokenRefresh.ts
298
+ async function refreshAccessToken(refreshToken) {
299
+ const domain = "betterness.us.auth0.com";
300
+ const clientId = "g4lqYHRQb2QMgdRKIlKwoTJl6eu41pWn";
301
+ if (!domain || !clientId) {
302
+ throw new CliError(
303
+ "OAuth is not configured. AUTH0_DOMAIN and AUTH0_CLIENT_ID must be set at build time.",
304
+ "OAUTH_CONFIG_MISSING"
305
+ );
306
+ }
307
+ const response = await fetch(`https://${domain}/oauth/token`, {
308
+ method: "POST",
309
+ headers: { "Content-Type": "application/json" },
310
+ body: JSON.stringify({
311
+ grant_type: "refresh_token",
312
+ client_id: clientId,
313
+ refresh_token: refreshToken
314
+ })
315
+ });
316
+ if (!response.ok) {
317
+ const body = await response.text();
318
+ throw new CliError(
319
+ `Token refresh failed (${response.status}): ${body}`,
320
+ "OAUTH_REFRESH_FAILED"
321
+ );
322
+ }
323
+ const data = await response.json();
324
+ return {
325
+ accessToken: data.access_token,
326
+ refreshToken: data.refresh_token ?? refreshToken,
327
+ expiresIn: data.expires_in,
328
+ idToken: data.id_token
329
+ };
330
+ }
331
+
66
332
  // src/auth/resolve.ts
67
- function resolveCredentials(explicitApiKey) {
333
+ async function resolveCredentials(explicitApiKey) {
68
334
  if (explicitApiKey) {
69
335
  return { apiKey: explicitApiKey };
70
336
  }
@@ -72,12 +338,30 @@ function resolveCredentials(explicitApiKey) {
72
338
  if (envKey) {
73
339
  return { apiKey: envKey };
74
340
  }
341
+ const tokens = loadTokens();
342
+ if (tokens) {
343
+ if (!isTokenExpired(tokens)) {
344
+ return { apiKey: tokens.accessToken };
345
+ }
346
+ try {
347
+ const refreshed = await refreshAccessToken(tokens.refreshToken);
348
+ saveTokens({
349
+ accessToken: refreshed.accessToken,
350
+ refreshToken: refreshed.refreshToken,
351
+ expiresAt: Math.floor(Date.now() / 1e3) + refreshed.expiresIn,
352
+ email: tokens.email,
353
+ name: tokens.name
354
+ });
355
+ return { apiKey: refreshed.accessToken };
356
+ } catch {
357
+ }
358
+ }
75
359
  const stored = loadCredentials();
76
360
  if (stored) {
77
361
  return { apiKey: stored.apiKey };
78
362
  }
79
363
  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",
364
+ "No credentials found. Use one of:\n 1. betterness auth login\n 2. betterness auth login --key <api-key>\n 3. BETTERNESS_API_KEY environment variable",
81
365
  "AUTH_MISSING"
82
366
  );
83
367
  }
@@ -89,9 +373,8 @@ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
89
373
  var ApiClient = class {
90
374
  apiKey;
91
375
  apiUrl;
92
- constructor(options = {}) {
93
- const credentials = resolveCredentials(options.apiKey);
94
- this.apiKey = credentials.apiKey;
376
+ constructor(apiKey) {
377
+ this.apiKey = apiKey;
95
378
  const apiUrl = "https://api.betterness.ai";
96
379
  if (!apiUrl) {
97
380
  throw new CliError(
@@ -125,7 +408,7 @@ var ApiClient = class {
125
408
  headers: {
126
409
  "Authorization": `Bearer ${this.apiKey}`,
127
410
  "Content-Type": "application/json",
128
- "User-Agent": `betterness-cli/${"1.1.2"}`,
411
+ "User-Agent": `betterness-cli/${"1.3.0"}`,
129
412
  "Accept": "application/json"
130
413
  },
131
414
  body: body ? JSON.stringify(body) : void 0,
@@ -173,9 +456,9 @@ var ApiClient = class {
173
456
  return this.unwrap(raw, schema);
174
457
  }
175
458
  async upload(path, filePath, fieldName = "file", schema) {
176
- const { readFileSync: readFileSync2 } = await import("fs");
459
+ const { readFileSync: readFileSync4 } = await import("fs");
177
460
  const { basename, extname } = await import("path");
178
- const buffer = readFileSync2(filePath);
461
+ const buffer = readFileSync4(filePath);
179
462
  const fileName = basename(filePath);
180
463
  const ext = extname(filePath).toLowerCase();
181
464
  const mimeType = ext === ".pdf" ? "application/pdf" : "application/octet-stream";
@@ -189,7 +472,7 @@ var ApiClient = class {
189
472
  method: "POST",
190
473
  headers: {
191
474
  "Authorization": `Bearer ${this.apiKey}`,
192
- "User-Agent": `betterness-cli/${"1.1.2"}`,
475
+ "User-Agent": `betterness-cli/${"1.3.0"}`,
193
476
  "Accept": "application/json"
194
477
  },
195
478
  body: formData,
@@ -240,11 +523,11 @@ var ApiClient = class {
240
523
  switch (response.status) {
241
524
  case 401:
242
525
  code = "AUTH_UNAUTHORIZED";
243
- message = "Invalid or expired API key. Run 'betterness auth login' to re-authenticate.";
526
+ message = "Invalid or expired credentials. Run 'betterness auth login' to re-authenticate.";
244
527
  break;
245
528
  case 403:
246
529
  code = "AUTH_FORBIDDEN";
247
- message = "Access denied. Your API key does not have permission for this operation.";
530
+ message = "Access denied. Your credentials do not have permission for this operation.";
248
531
  break;
249
532
  case 404:
250
533
  code = "NOT_FOUND";
@@ -270,6 +553,10 @@ var ApiClient = class {
270
553
  throw new CliError(message, code);
271
554
  }
272
555
  };
556
+ async function createApiClient(options = {}) {
557
+ const credentials = await resolveCredentials(options.apiKey);
558
+ return new ApiClient(credentials.apiKey);
559
+ }
273
560
 
274
561
  // src/formatters/json.ts
275
562
  function formatJson(data) {
@@ -531,40 +818,44 @@ var connectionsResponseSchema = z.object({
531
818
  syncEnabled: z.boolean().nullable().optional()
532
819
  }).passthrough())
533
820
  }).passthrough();
821
+ var questionSchemaSchema = z.object({
822
+ id: z.string(),
823
+ label: z.string(),
824
+ type: z.string(),
825
+ typeDescription: z.string(),
826
+ example: z.unknown().nullable().optional()
827
+ });
828
+ var sectionSchemaSchema = z.object({
829
+ acronym: z.string(),
830
+ title: z.string(),
831
+ questions: z.array(questionSchemaSchema)
832
+ });
833
+ var healthProfileSchemaSchema = z.object({
834
+ sections: z.array(sectionSchemaSchema)
835
+ });
836
+ var healthProfileResponseSchema = z.object({
837
+ userId: z.string(),
838
+ questionsData: z.record(z.unknown()).nullable().optional()
839
+ }).passthrough();
534
840
 
535
841
  // src/commands/auth.ts
536
842
  function registerAuthCommands(program2) {
537
843
  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) => {
844
+ auth.command("login").description("Authenticate with Betterness (OAuth by default, or --key for API key)").option("--key <apiKey>", "Use an API key instead of OAuth").action(async (opts) => {
539
845
  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);
846
+ if (opts.key) {
847
+ await loginWithApiKey(opts.key);
848
+ } else {
849
+ await loginWithOAuth();
549
850
  }
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
851
  } catch (error) {
562
852
  outputError(error);
563
853
  }
564
854
  });
565
855
  auth.command("logout").description("Remove stored credentials").action(() => {
566
- const deleted = deleteCredentials();
567
- if (deleted) {
856
+ const deletedCreds = deleteCredentials();
857
+ const deletedTokens = deleteTokens();
858
+ if (deletedCreds || deletedTokens) {
568
859
  console.log("Credentials removed.");
569
860
  } else {
570
861
  console.log("No stored credentials found.");
@@ -573,7 +864,7 @@ function registerAuthCommands(program2) {
573
864
  auth.command("whoami").description("Show the currently authenticated user").action(async (_, cmd) => {
574
865
  try {
575
866
  const parentOpts = cmd.optsWithGlobals();
576
- const client = new ApiClient({ apiKey: parentOpts.apiKey });
867
+ const client = await createApiClient({ apiKey: parentOpts.apiKey });
577
868
  const user = await client.get("/api/betterness-user/detail", void 0, betternessUserSchema);
578
869
  outputRecord(cmd, {
579
870
  name: user.fullName ?? user.firstName,
@@ -585,6 +876,54 @@ function registerAuthCommands(program2) {
585
876
  }
586
877
  });
587
878
  }
879
+ async function loginWithApiKey(apiKey) {
880
+ if (!apiKey.trim()) {
881
+ console.error("API key cannot be empty.");
882
+ process.exit(1);
883
+ }
884
+ apiKey = apiKey.trim();
885
+ console.error("Verifying...");
886
+ const client = new ApiClient(apiKey);
887
+ const user = await client.get("/api/betterness-user/detail", void 0, betternessUserSchema);
888
+ deleteTokens();
889
+ saveCredentials({
890
+ apiKey,
891
+ email: user.email ?? void 0,
892
+ name: user.firstName ?? void 0
893
+ });
894
+ console.log(`Logged in as: ${user.firstName ?? "Unknown"} (${user.email ?? "no email"})`);
895
+ console.log("Credentials saved to ~/.betterness/credentials.json");
896
+ }
897
+ async function loginWithOAuth() {
898
+ const result = await startOAuthLogin();
899
+ deleteCredentials();
900
+ const expiresAt = Math.floor(Date.now() / 1e3) + result.expiresIn;
901
+ saveTokens({
902
+ accessToken: result.accessToken,
903
+ refreshToken: result.refreshToken,
904
+ expiresAt,
905
+ email: result.email,
906
+ name: result.name
907
+ });
908
+ console.error("Verifying...");
909
+ let user;
910
+ try {
911
+ const client = new ApiClient(result.accessToken);
912
+ user = await client.get("/api/betterness-user/detail", void 0, betternessUserSchema);
913
+ } catch (error) {
914
+ deleteTokens();
915
+ throw error;
916
+ }
917
+ saveTokens({
918
+ accessToken: result.accessToken,
919
+ refreshToken: result.refreshToken,
920
+ expiresAt,
921
+ email: user.email ?? result.email,
922
+ name: user.firstName ?? result.name
923
+ });
924
+ console.log(`Logged in as: ${user.firstName ?? "Unknown"} (${user.email ?? "no email"})`);
925
+ console.log("Credentials saved to ~/.betterness/tokens.json");
926
+ }
588
927
 
589
928
  // src/commands/profile.ts
590
929
  function registerProfileCommands(program2) {
@@ -592,7 +931,7 @@ function registerProfileCommands(program2) {
592
931
  profile.command("get").description("Retrieve current user profile (name, email, phone, gender, DOB, address)").action(async (_, cmd) => {
593
932
  try {
594
933
  const globalOpts = cmd.optsWithGlobals();
595
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
934
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
596
935
  const user = await client.get(
597
936
  "/api/betterness-user/detail",
598
937
  void 0,
@@ -645,7 +984,7 @@ function registerProfileCommands(program2) {
645
984
  return;
646
985
  }
647
986
  const globalOpts = cmd.optsWithGlobals();
648
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
987
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
649
988
  await client.put(
650
989
  "/api/betterness-user/personal-details",
651
990
  dto
@@ -680,7 +1019,7 @@ function registerBiomarkersCommands(program2) {
680
1019
  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
1020
  try {
682
1021
  const globalOpts = cmd.optsWithGlobals();
683
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1022
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
684
1023
  const params = { size: opts.limit };
685
1024
  if (opts.name) params.name = opts.name;
686
1025
  if (opts.loincCode) params.loincCode = opts.loincCode;
@@ -701,7 +1040,7 @@ function registerBiomarkersCommands(program2) {
701
1040
  biomarkers.command("loinc-codes").description("List all available LOINC codes for biomarker identification").action(async (_, cmd) => {
702
1041
  try {
703
1042
  const globalOpts = cmd.optsWithGlobals();
704
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1043
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
705
1044
  const loincs = await client.get(
706
1045
  "/api/health-record/active-loinc",
707
1046
  void 0,
@@ -729,7 +1068,7 @@ function registerBiologicalAgeCommands(program2) {
729
1068
  bioAge.command("get").description("Get biological age history with biomarker values").option("--limit <n>", "Maximum number of results", "10").action(async (opts, cmd) => {
730
1069
  try {
731
1070
  const globalOpts = cmd.optsWithGlobals();
732
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1071
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
733
1072
  const results = await client.get("/api/biological-age", {
734
1073
  size: opts.limit,
735
1074
  sort: "date,desc"
@@ -763,73 +1102,54 @@ function todayIso() {
763
1102
  function defaultTimezone() {
764
1103
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
765
1104
  }
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) {
1105
+ function flattenAgentTrends(data) {
777
1106
  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
- }
1107
+ for (const [source, goalKeys] of Object.entries(data)) {
1108
+ if (!goalKeys || typeof goalKeys !== "object") continue;
1109
+ for (const [goalKey, timestamps] of Object.entries(goalKeys)) {
1110
+ if (!timestamps || typeof timestamps !== "object") continue;
1111
+ for (const [timestamp, fields] of Object.entries(timestamps)) {
1112
+ if (!fields || typeof fields !== "object") continue;
1113
+ rows.push({ source, goalKey, timestamp, ...fields });
789
1114
  }
790
1115
  }
791
1116
  }
792
1117
  return rows;
793
1118
  }
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
1119
  function printTrendRows(rows) {
1120
+ const groups = /* @__PURE__ */ new Map();
812
1121
  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)}`);
1122
+ const key = `${row.goalKey} (${row.source})`;
1123
+ if (!groups.has(key)) groups.set(key, []);
1124
+ groups.get(key).push(row);
1125
+ }
1126
+ for (const [groupKey, groupRows] of groups) {
1127
+ console.log(`--- ${groupKey} ---`);
1128
+ for (const row of groupRows) {
1129
+ const display = { ...row };
1130
+ delete display.source;
1131
+ delete display.goalKey;
1132
+ const entries = Object.entries(display).filter(([_, v]) => v !== null && v !== void 0);
1133
+ if (entries.length === 0) continue;
1134
+ const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
1135
+ for (const [key, value] of entries) {
1136
+ console.log(` ${key.padEnd(maxKeyLen)} ${String(value)}`);
1137
+ }
1138
+ console.log();
822
1139
  }
823
- console.log();
824
1140
  }
825
1141
  }
826
- function registerHealthDataCommand(parent, name, description, categoryFilter) {
1142
+ function registerHealthDataCommand(parent, name, description, category) {
827
1143
  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
1144
  try {
829
1145
  const globalOpts = cmd.optsWithGlobals();
830
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1146
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
831
1147
  const to = opts.to ?? opts.from;
832
- const rows = await fetchTrendsForRange(client, opts.from, to, opts.timezone, categoryFilter);
1148
+ const data = await client.get(
1149
+ "/api/goal-entry/agent-trends",
1150
+ { from: opts.from, to, zoneId: opts.timezone, category }
1151
+ );
1152
+ const rows = flattenAgentTrends(data);
833
1153
  if (getOutputFormat(cmd) === "json") {
834
1154
  console.log(formatJson(rows));
835
1155
  return;
@@ -890,7 +1210,7 @@ function registerSleepCommands(program2) {
890
1210
  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
1211
  try {
892
1212
  const globalOpts = cmd.optsWithGlobals();
893
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1213
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
894
1214
  const to = opts.to ?? opts.from;
895
1215
  const result = await client.get(
896
1216
  "/api/goal-entry/sleep-stages",
@@ -954,7 +1274,7 @@ function registerConnectedDevicesCommands(program2) {
954
1274
  devices.command("list").description("List all connected health devices and wearables").action(async (_, cmd) => {
955
1275
  try {
956
1276
  const globalOpts = cmd.optsWithGlobals();
957
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1277
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
958
1278
  const result = await client.get(
959
1279
  "/api/vital/user/connections",
960
1280
  void 0,
@@ -996,7 +1316,7 @@ function registerConnectedDevicesCommands(program2) {
996
1316
  devices.command("available").description("List health device integrations the user can connect (not currently active)").action(async (_, cmd) => {
997
1317
  try {
998
1318
  const globalOpts = cmd.optsWithGlobals();
999
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1319
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1000
1320
  const available = await client.get(
1001
1321
  "/api/vital/user/available-integrations",
1002
1322
  void 0,
@@ -1018,7 +1338,7 @@ function registerConnectedDevicesCommands(program2) {
1018
1338
  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
1339
  try {
1020
1340
  const globalOpts = cmd.optsWithGlobals();
1021
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1341
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1022
1342
  const result = await client.post(
1023
1343
  `/api/vital/user/link-token/${opts.integrationKey}`
1024
1344
  );
@@ -1030,7 +1350,7 @@ function registerConnectedDevicesCommands(program2) {
1030
1350
  devices.command("apple-health-code").description("Generate connection code for Apple HealthKit via Junction app").action(async (_, cmd) => {
1031
1351
  try {
1032
1352
  const globalOpts = cmd.optsWithGlobals();
1033
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1353
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1034
1354
  const code = await client.post("/api/vital/user/ios-app-code");
1035
1355
  outputRecord(cmd, { connectionCode: code });
1036
1356
  } catch (error) {
@@ -1044,7 +1364,7 @@ function registerConnectedDevicesCommands(program2) {
1044
1364
  return;
1045
1365
  }
1046
1366
  const globalOpts = cmd.optsWithGlobals();
1047
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1367
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1048
1368
  await client.delete(`/api/vital/user/connection/${opts.integrationKey}`);
1049
1369
  console.log(`Disconnected ${opts.integrationKey} successfully.`);
1050
1370
  } catch (error) {
@@ -1060,7 +1380,7 @@ function registerLabTestsCommands(program2) {
1060
1380
  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
1381
  try {
1062
1382
  const globalOpts = cmd.optsWithGlobals();
1063
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1383
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1064
1384
  const params = {};
1065
1385
  if (opts.query) params.slug = opts.query;
1066
1386
  if (opts.popular) params.onlyPopular = true;
@@ -1095,7 +1415,7 @@ function registerLabRecordsCommands(program2) {
1095
1415
  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
1416
  try {
1097
1417
  const globalOpts = cmd.optsWithGlobals();
1098
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1418
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1099
1419
  const page = await client.get(
1100
1420
  "/api/health-record/all",
1101
1421
  { page: opts.page, size: opts.limit },
@@ -1121,7 +1441,7 @@ function registerLabRecordsCommands(program2) {
1121
1441
  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
1442
  try {
1123
1443
  const globalOpts = cmd.optsWithGlobals();
1124
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1444
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1125
1445
  let result;
1126
1446
  try {
1127
1447
  result = await client.get(
@@ -1149,7 +1469,7 @@ function registerLabOrdersCommands(program2) {
1149
1469
  return;
1150
1470
  }
1151
1471
  const globalOpts = cmd.optsWithGlobals();
1152
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1472
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1153
1473
  const result = await client.post(
1154
1474
  "/api/lab-test-order/initialize",
1155
1475
  { order: opts.orderId }
@@ -1162,7 +1482,7 @@ function registerLabOrdersCommands(program2) {
1162
1482
  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
1483
  try {
1164
1484
  const globalOpts = cmd.optsWithGlobals();
1165
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1485
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1166
1486
  const results = await client.get(
1167
1487
  "/api/lab-test-order/all-labs",
1168
1488
  { zipCode: opts.zipCode, labOrderId: opts.orderId }
@@ -1185,7 +1505,7 @@ function registerLabOrdersCommands(program2) {
1185
1505
  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
1506
  try {
1187
1507
  const globalOpts = cmd.optsWithGlobals();
1188
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1508
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1189
1509
  const params = {
1190
1510
  siteCode: opts.siteCode,
1191
1511
  labOrderId: opts.orderId,
@@ -1212,7 +1532,7 @@ function registerLabOrdersCommands(program2) {
1212
1532
  return;
1213
1533
  }
1214
1534
  const globalOpts = cmd.optsWithGlobals();
1215
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1535
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1216
1536
  const result = await client.post(
1217
1537
  "/api/lab-test-order/book-appointment",
1218
1538
  {
@@ -1235,7 +1555,7 @@ function registerLabOrdersCommands(program2) {
1235
1555
  return;
1236
1556
  }
1237
1557
  const globalOpts = cmd.optsWithGlobals();
1238
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1558
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1239
1559
  const result = await client.post(
1240
1560
  "/api/lab-test-order/book-appointment",
1241
1561
  {
@@ -1258,7 +1578,7 @@ function registerLabOrdersCommands(program2) {
1258
1578
  return;
1259
1579
  }
1260
1580
  const globalOpts = cmd.optsWithGlobals();
1261
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1581
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1262
1582
  const result = await client.post(
1263
1583
  "/api/appointment/cancel-appointment",
1264
1584
  {
@@ -1279,7 +1599,7 @@ function registerLabResultsCommands(program2) {
1279
1599
  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
1600
  try {
1281
1601
  const globalOpts = cmd.optsWithGlobals();
1282
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1602
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1283
1603
  const actionMap = {
1284
1604
  APPROVE: "approve",
1285
1605
  ROLLBACK: "rollback",
@@ -1299,7 +1619,7 @@ function registerLabResultsCommands(program2) {
1299
1619
  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
1620
  try {
1301
1621
  const globalOpts = cmd.optsWithGlobals();
1302
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1622
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1303
1623
  const body = {};
1304
1624
  if (opts.name) body.name = opts.name;
1305
1625
  if (opts.result) body.result = opts.result;
@@ -1318,7 +1638,7 @@ function registerLabResultsCommands(program2) {
1318
1638
  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
1639
  try {
1320
1640
  const globalOpts = cmd.optsWithGlobals();
1321
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1641
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1322
1642
  const patientBody = {};
1323
1643
  if (opts.patientName) patientBody.patientName = opts.patientName;
1324
1644
  if (opts.patientSex) patientBody.patientSex = opts.patientSex;
@@ -1341,10 +1661,10 @@ function registerLabResultsCommands(program2) {
1341
1661
  });
1342
1662
  labResults.command("upload").description("Upload a lab result PDF for processing").requiredOption("--file <path>", "Path to the PDF file").action(async (opts, cmd) => {
1343
1663
  try {
1344
- const { existsSync: existsSync3 } = await import("fs");
1664
+ const { existsSync: existsSync5 } = await import("fs");
1345
1665
  const { resolve } = await import("path");
1346
1666
  const filePath = resolve(opts.file);
1347
- if (!existsSync3(filePath)) {
1667
+ if (!existsSync5(filePath)) {
1348
1668
  console.error(`File not found: ${filePath}`);
1349
1669
  process.exit(1);
1350
1670
  }
@@ -1353,7 +1673,7 @@ function registerLabResultsCommands(program2) {
1353
1673
  process.exit(1);
1354
1674
  }
1355
1675
  const globalOpts = cmd.optsWithGlobals();
1356
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1676
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1357
1677
  const result = await client.upload(
1358
1678
  "/api/lab-result/agent-upload",
1359
1679
  filePath
@@ -1384,7 +1704,7 @@ function registerPurchasesCommands(program2) {
1384
1704
  purchases.command("payment-methods").description("List saved payment methods (credit/debit cards)").action(async (_, cmd) => {
1385
1705
  try {
1386
1706
  const globalOpts = cmd.optsWithGlobals();
1387
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1707
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1388
1708
  const results = await client.get(
1389
1709
  "/api/payment-methods/all",
1390
1710
  void 0,
@@ -1424,7 +1744,7 @@ function registerPurchasesCommands(program2) {
1424
1744
  return;
1425
1745
  }
1426
1746
  const globalOpts = cmd.optsWithGlobals();
1427
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1747
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1428
1748
  const result = await client.post(
1429
1749
  "/api/purchase",
1430
1750
  {
@@ -1453,7 +1773,7 @@ function registerPurchasesCommands(program2) {
1453
1773
  return;
1454
1774
  }
1455
1775
  const globalOpts = cmd.optsWithGlobals();
1456
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1776
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1457
1777
  const result = await client.post(
1458
1778
  "/api/purchase/checkout",
1459
1779
  {
@@ -1488,7 +1808,7 @@ function registerSmartListingsCommands(program2) {
1488
1808
  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
1809
  try {
1490
1810
  const globalOpts = cmd.optsWithGlobals();
1491
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1811
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1492
1812
  const body = {};
1493
1813
  if (opts.query) body.search = opts.query;
1494
1814
  if (opts.lat) body.latitude = Number(opts.lat);
@@ -1524,7 +1844,7 @@ function registerSmartListingsCommands(program2) {
1524
1844
  smartListings.command("detail").description("Get full details of a SmartListing by ID").requiredOption("--id <externalId>", "SmartListing external ID").action(async (opts, cmd) => {
1525
1845
  try {
1526
1846
  const globalOpts = cmd.optsWithGlobals();
1527
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
1847
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1528
1848
  const result = await client.get(
1529
1849
  `/api/company/detail/${opts.id}`
1530
1850
  );
@@ -1702,7 +2022,7 @@ function registerWorkflowCommands(program2) {
1702
2022
  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
2023
  try {
1704
2024
  const globalOpts = cmd.optsWithGlobals();
1705
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
2025
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1706
2026
  const brief = await fetchDailyBrief(client, opts.biomarkerLimit);
1707
2027
  const format = getOutputFormat(cmd);
1708
2028
  if (format === "json") {
@@ -1717,7 +2037,7 @@ function registerWorkflowCommands(program2) {
1717
2037
  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
2038
  try {
1719
2039
  const globalOpts = cmd.optsWithGlobals();
1720
- const client = new ApiClient({ apiKey: globalOpts.apiKey });
2040
+ const client = await createApiClient({ apiKey: globalOpts.apiKey });
1721
2041
  const result = await fetchNextActions(client, opts.biomarkerLimit);
1722
2042
  const format = getOutputFormat(cmd);
1723
2043
  if (format === "json") {
@@ -1807,29 +2127,37 @@ function registerSchemaCommand(program2) {
1807
2127
  }
1808
2128
 
1809
2129
  // src/commands/debug.ts
1810
- import { homedir as homedir2 } from "os";
1811
- import { join as join2 } from "path";
1812
- import { existsSync as existsSync2 } from "fs";
2130
+ import { homedir as homedir3 } from "os";
2131
+ import { join as join3 } from "path";
2132
+ import { existsSync as existsSync3 } from "fs";
1813
2133
  function registerDebugCommands(program2) {
1814
2134
  const debug = program2.command("debug", { hidden: true }).description("Internal debug utilities");
1815
2135
  debug.command("config").description("Show resolved configuration").action((_, cmd) => {
1816
- const credentialsPath = join2(homedir2(), ".betterness", "credentials.json");
2136
+ const credentialsPath = join3(homedir3(), ".betterness", "credentials.json");
2137
+ const tokensPath = join3(homedir3(), ".betterness", "tokens.json");
1817
2138
  const stored = loadCredentials();
2139
+ const tokens = loadTokens();
1818
2140
  const envKey = process.env.BETTERNESS_API_KEY;
1819
2141
  const parentOpts = cmd.optsWithGlobals();
1820
- const authSource = parentOpts.apiKey ? "--api-key flag" : envKey ? "BETTERNESS_API_KEY env" : stored ? "~/.betterness/credentials.json" : "none";
2142
+ const authSource = parentOpts.apiKey ? "--api-key flag" : envKey ? "BETTERNESS_API_KEY env" : tokens && !isTokenExpired(tokens) ? "OAuth tokens (~/.betterness/tokens.json)" : stored ? "API key (~/.betterness/credentials.json)" : "none";
1821
2143
  const config = {
1822
- version: "1.1.2",
2144
+ version: "1.3.0",
1823
2145
  apiUrl: "https://api.betterness.ai",
2146
+ auth0Domain: "betterness.us.auth0.com",
2147
+ auth0ClientId: "g4lqYHRQb2QMgdRKIlKwoTJl6eu41pWn",
2148
+ auth0Audience: "https://api.betterness.ai",
1824
2149
  authSource,
1825
2150
  credentialsFile: credentialsPath,
1826
- credentialsExists: String(existsSync2(credentialsPath))
2151
+ credentialsExists: String(existsSync3(credentialsPath)),
2152
+ tokensFile: tokensPath,
2153
+ tokensExists: String(existsSync3(tokensPath))
1827
2154
  };
1828
- if (stored?.email) {
1829
- config.storedEmail = stored.email;
1830
- }
1831
- if (stored?.savedAt) {
1832
- config.credentialsSavedAt = stored.savedAt;
2155
+ if (stored?.email) config.storedEmail = stored.email;
2156
+ if (stored?.savedAt) config.credentialsSavedAt = stored.savedAt;
2157
+ if (tokens) {
2158
+ config.tokensExpired = String(isTokenExpired(tokens));
2159
+ if (tokens.email) config.oauthEmail = tokens.email;
2160
+ if (tokens.savedAt) config.tokensSavedAt = tokens.savedAt;
1833
2161
  }
1834
2162
  const parentFormat = parentOpts.json ? "json" : parentOpts.markdown ? "markdown" : "table";
1835
2163
  if (parentFormat === "json") {
@@ -1843,10 +2171,506 @@ function registerDebugCommands(program2) {
1843
2171
  });
1844
2172
  }
1845
2173
 
2174
+ // src/commands/health-profile.ts
2175
+ var sectionColumns = [
2176
+ { key: "acronym", label: "Acronym", width: 8 },
2177
+ { key: "title", label: "Title", width: 40 },
2178
+ { key: "questionCount", label: "Questions", width: 10 }
2179
+ ];
2180
+ var questionColumns = [
2181
+ { key: "id", label: "Question ID", width: 55 },
2182
+ { key: "label", label: "Label", width: 40 },
2183
+ { key: "type", label: "Type", width: 15 },
2184
+ { key: "example", label: "Example Value", width: 30 }
2185
+ ];
2186
+ var summaryColumns = [
2187
+ { key: "section", label: "Section", width: 30 },
2188
+ { key: "question", label: "Question", width: 50 },
2189
+ { key: "answer", label: "Answer", width: 40 }
2190
+ ];
2191
+ function registerHealthProfileCommands(program2) {
2192
+ const hp = program2.command("health-profile").description("Health profile questionnaire (read, update, schema)");
2193
+ hp.command("schema").description("List all sections and question IDs available in the health profile").option("--section <acronym>", "Show questions for a specific section only").action(async (opts, cmd) => {
2194
+ try {
2195
+ const globalOpts = cmd.optsWithGlobals();
2196
+ const client = new ApiClient(globalOpts.apiKey);
2197
+ const schema = await client.get(
2198
+ "/api/v1/copilot-config/schema",
2199
+ void 0,
2200
+ healthProfileSchemaSchema
2201
+ );
2202
+ if (opts.section) {
2203
+ const section = schema.sections.find(
2204
+ (s) => s.acronym.toUpperCase() === opts.section.toUpperCase()
2205
+ );
2206
+ if (!section) {
2207
+ console.error(`Section "${opts.section}" not found. Use health-profile schema to list all sections.`);
2208
+ process.exit(1);
2209
+ }
2210
+ const rows = section.questions.map((q) => ({
2211
+ ...q,
2212
+ example: typeof q.example === "object" ? JSON.stringify(q.example) : q.example
2213
+ }));
2214
+ outputList(cmd, rows, questionColumns);
2215
+ } else {
2216
+ outputList(
2217
+ cmd,
2218
+ schema.sections.map((s) => ({
2219
+ acronym: s.acronym,
2220
+ title: s.title,
2221
+ questionCount: s.questions.length
2222
+ })),
2223
+ sectionColumns
2224
+ );
2225
+ }
2226
+ } catch (error) {
2227
+ outputError(error);
2228
+ }
2229
+ });
2230
+ hp.command("get").description("Retrieve the full health profile (all answered questions as a flat map)").action(async (_, cmd) => {
2231
+ try {
2232
+ const globalOpts = cmd.optsWithGlobals();
2233
+ const client = new ApiClient(globalOpts.apiKey);
2234
+ const user = await client.get(
2235
+ "/api/betterness-user/detail"
2236
+ );
2237
+ const profile = await client.get(
2238
+ `/api/v1/copilot-config/${user.externalId}`,
2239
+ void 0,
2240
+ healthProfileResponseSchema
2241
+ );
2242
+ outputRecord(cmd, profile.questionsData ?? {});
2243
+ } catch (error) {
2244
+ outputError(error);
2245
+ }
2246
+ });
2247
+ hp.command("get-section").description("Retrieve health profile answers for a specific section").requiredOption("--section <acronym>", "Section acronym (e.g. HWP, DMH, DA)").action(async (opts, cmd) => {
2248
+ try {
2249
+ const globalOpts = cmd.optsWithGlobals();
2250
+ const client = new ApiClient(globalOpts.apiKey);
2251
+ const user = await client.get(
2252
+ "/api/betterness-user/detail"
2253
+ );
2254
+ const profile = await client.get(
2255
+ `/api/v1/copilot-config/${user.externalId}`,
2256
+ void 0,
2257
+ healthProfileResponseSchema
2258
+ );
2259
+ const prefix = opts.section.toUpperCase() + "_";
2260
+ const filtered = {};
2261
+ for (const [key, value] of Object.entries(profile.questionsData ?? {})) {
2262
+ if (key.startsWith(prefix)) {
2263
+ filtered[key] = value;
2264
+ }
2265
+ }
2266
+ if (Object.keys(filtered).length === 0) {
2267
+ console.log(`No answers found for section "${opts.section}".`);
2268
+ return;
2269
+ }
2270
+ outputRecord(cmd, filtered);
2271
+ } catch (error) {
2272
+ outputError(error);
2273
+ }
2274
+ });
2275
+ hp.command("update").description("Update health profile questions (only provided fields are changed)").requiredOption("--data <json>", "JSON object with question IDs as keys and answers as values").option("--dry-run", "Preview changes without applying").action(async (opts, cmd) => {
2276
+ try {
2277
+ let patchData;
2278
+ try {
2279
+ patchData = JSON.parse(opts.data);
2280
+ } catch {
2281
+ console.error(`Invalid JSON in --data. Example: --data '{"HWP_HAB_do_you_smoke_cigarettes_or_use_tobacco_products": false}'`);
2282
+ process.exit(1);
2283
+ }
2284
+ if (Object.keys(patchData).length === 0) {
2285
+ console.log("No fields provided to update.");
2286
+ return;
2287
+ }
2288
+ if (opts.dryRun) {
2289
+ outputRecord(cmd, {
2290
+ action: "health-profile update",
2291
+ fields: patchData,
2292
+ note: "Dry run \u2014 profile will not be updated."
2293
+ });
2294
+ return;
2295
+ }
2296
+ const globalOpts = cmd.optsWithGlobals();
2297
+ const client = new ApiClient(globalOpts.apiKey);
2298
+ const user = await client.get(
2299
+ "/api/betterness-user/detail"
2300
+ );
2301
+ const raw = await client.request(
2302
+ `/api/v1/copilot-config/${user.externalId}`,
2303
+ {
2304
+ method: "PATCH",
2305
+ body: {
2306
+ patchData,
2307
+ isBotUpdate: false
2308
+ }
2309
+ }
2310
+ );
2311
+ const updatedData = raw?.content?.questionsData ?? {};
2312
+ const updatedFields = {};
2313
+ for (const key of Object.keys(patchData)) {
2314
+ if (key in updatedData) {
2315
+ updatedFields[key] = updatedData[key];
2316
+ }
2317
+ }
2318
+ if (Object.keys(updatedFields).length > 0) {
2319
+ outputRecord(cmd, updatedFields);
2320
+ }
2321
+ console.log("Health profile updated successfully.");
2322
+ } catch (error) {
2323
+ outputError(error);
2324
+ }
2325
+ });
2326
+ hp.command("reset-section").description("Clear all answers in a section").requiredOption("--section <acronym>", "Section acronym (e.g. HWP, DMH, DA)").option("--dry-run", "Preview without applying").action(async (opts, cmd) => {
2327
+ try {
2328
+ if (opts.dryRun) {
2329
+ outputRecord(cmd, {
2330
+ action: "health-profile reset-section",
2331
+ section: opts.section,
2332
+ note: "Dry run \u2014 section will not be reset."
2333
+ });
2334
+ return;
2335
+ }
2336
+ const globalOpts = cmd.optsWithGlobals();
2337
+ const client = new ApiClient(globalOpts.apiKey);
2338
+ const user = await client.get(
2339
+ "/api/betterness-user/detail"
2340
+ );
2341
+ await client.delete(
2342
+ `/api/v1/copilot-config/${user.externalId}/section/${opts.section.toUpperCase()}`
2343
+ );
2344
+ console.log(`Section "${opts.section}" reset successfully.`);
2345
+ } catch (error) {
2346
+ outputError(error);
2347
+ }
2348
+ });
2349
+ hp.command("summary").description("Human-readable summary of answered health profile questions").option("--section <acronym>", "Summarize a specific section only").action(async (opts, cmd) => {
2350
+ try {
2351
+ const globalOpts = cmd.optsWithGlobals();
2352
+ const client = new ApiClient(globalOpts.apiKey);
2353
+ const [schema, user] = await Promise.all([
2354
+ client.get("/api/v1/copilot-config/schema", void 0, healthProfileSchemaSchema),
2355
+ client.get("/api/betterness-user/detail")
2356
+ ]);
2357
+ const profile = await client.get(
2358
+ `/api/v1/copilot-config/${user.externalId}`,
2359
+ void 0,
2360
+ healthProfileResponseSchema
2361
+ );
2362
+ const questionsData = profile.questionsData ?? {};
2363
+ const labelMap = /* @__PURE__ */ new Map();
2364
+ for (const section of schema.sections) {
2365
+ for (const q of section.questions) {
2366
+ labelMap.set(q.id, q.label);
2367
+ }
2368
+ }
2369
+ const sections = opts.section ? schema.sections.filter((s) => s.acronym.toUpperCase() === opts.section.toUpperCase()) : schema.sections;
2370
+ if (sections.length === 0) {
2371
+ console.error(`Section "${opts.section}" not found.`);
2372
+ process.exit(1);
2373
+ }
2374
+ const rows = [];
2375
+ for (const section of sections) {
2376
+ const prefix = section.acronym + "_";
2377
+ for (const [key, value] of Object.entries(questionsData)) {
2378
+ if (key.startsWith(prefix)) {
2379
+ rows.push({
2380
+ section: section.title,
2381
+ question: labelMap.get(key) ?? key,
2382
+ answer: formatAnswerValue(value)
2383
+ });
2384
+ }
2385
+ }
2386
+ }
2387
+ if (rows.length === 0) {
2388
+ console.log("No answered questions found.");
2389
+ return;
2390
+ }
2391
+ outputList(cmd, rows, summaryColumns);
2392
+ } catch (error) {
2393
+ outputError(error);
2394
+ }
2395
+ });
2396
+ }
2397
+ function formatAnswerValue(value) {
2398
+ if (value === null || value === void 0) return null;
2399
+ if (typeof value === "boolean") return value ? "Yes" : "No";
2400
+ if (typeof value === "string" || typeof value === "number") return value;
2401
+ if (Array.isArray(value)) {
2402
+ if (value.length === 0) return "\u2014";
2403
+ if (typeof value[0] === "string" || typeof value[0] === "number") return value.join(", ");
2404
+ return value.map((item) => {
2405
+ const parts = [];
2406
+ if (item.name) parts.push(String(item.name));
2407
+ if (item.relationship) parts.push(String(item.relationship));
2408
+ if (item.sex) parts.push(String(item.sex));
2409
+ if (item.livingStatus && item.livingStatus !== "Living") parts.push(String(item.livingStatus));
2410
+ if (Array.isArray(item.healthConditions) && item.healthConditions.length > 0) {
2411
+ parts.push(`conditions: ${item.healthConditions.join(", ")}`);
2412
+ }
2413
+ return parts.join(", ");
2414
+ }).join(" | ");
2415
+ }
2416
+ if (typeof value === "object") {
2417
+ const obj = value;
2418
+ if ("boolean" in obj && "string" in obj) {
2419
+ const yn = obj.boolean ? "Yes" : "No";
2420
+ return obj.string ? `${yn} \u2014 ${obj.string}` : yn;
2421
+ }
2422
+ if ("boolean" in obj && "arrayString" in obj) {
2423
+ const yn = obj.boolean ? "Yes" : "No";
2424
+ const items = Array.isArray(obj.arrayString) ? obj.arrayString.join(", ") : "";
2425
+ return items ? `${yn} \u2014 ${items}` : yn;
2426
+ }
2427
+ if ("boolean" in obj && "keyValueList" in obj) {
2428
+ const yn = obj.boolean ? "Yes" : "No";
2429
+ const list = Array.isArray(obj.keyValueList) ? obj.keyValueList.map((e) => [e.name, e.value].filter(Boolean).join(": ")).join("; ") : "";
2430
+ return list ? `${yn} \u2014 ${list}` : yn;
2431
+ }
2432
+ if ("boolean" in obj && "supplementsData" in obj) {
2433
+ const yn = obj.boolean ? "Yes" : "No";
2434
+ const supps = obj.supplementsData;
2435
+ if (!supps) return yn;
2436
+ const items = Object.entries(supps).filter(([, v]) => v.isActive).map(([name, v]) => {
2437
+ const doses = (v.dosifications ?? []).map((d) => `${d.name ?? ""} ${d.dosage ?? ""}${d.dosageUnit ?? "mg"}`).join(", ");
2438
+ return doses ? `${name} (${doses})` : name;
2439
+ }).join("; ");
2440
+ return items ? `${yn} \u2014 ${items}` : yn;
2441
+ }
2442
+ const entries = Object.entries(obj);
2443
+ if (entries.length > 0 && entries.every(([, v]) => typeof v === "string" || typeof v === "number")) {
2444
+ return entries.map(([k, v]) => `${k}: ${v}`).join(", ");
2445
+ }
2446
+ }
2447
+ return value;
2448
+ }
2449
+
2450
+ // src/commands/mcp.ts
2451
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3, copyFileSync } from "fs";
2452
+ import { join as join4, dirname } from "path";
2453
+ import { homedir as homedir4, platform } from "os";
2454
+ var CLIENTS = {
2455
+ claude: {
2456
+ name: "Claude Desktop",
2457
+ supportsScope: false,
2458
+ configPath: () => {
2459
+ if (platform() === "darwin") {
2460
+ return join4(homedir4(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
2461
+ }
2462
+ return join4(homedir4(), ".config", "Claude", "claude_desktop_config.json");
2463
+ }
2464
+ },
2465
+ "claude-code": {
2466
+ name: "Claude Code",
2467
+ supportsScope: true,
2468
+ configPath: (scope) => {
2469
+ if (scope === "project") {
2470
+ return join4(process.cwd(), ".mcp.json");
2471
+ }
2472
+ return join4(homedir4(), ".claude", "settings.json");
2473
+ }
2474
+ },
2475
+ cursor: {
2476
+ name: "Cursor",
2477
+ supportsScope: false,
2478
+ configPath: () => join4(process.cwd(), ".cursor", "mcp.json")
2479
+ },
2480
+ windsurf: {
2481
+ name: "Windsurf",
2482
+ supportsScope: false,
2483
+ configPath: () => join4(homedir4(), ".codeium", "windsurf", "mcp_config.json")
2484
+ }
2485
+ };
2486
+ var VALID_CLIENTS = Object.keys(CLIENTS).join(", ");
2487
+ function validateClient(clientArg) {
2488
+ if (!(clientArg in CLIENTS)) {
2489
+ throw new CliError(
2490
+ `Unknown client "${clientArg}". Supported clients: ${VALID_CLIENTS}`,
2491
+ "INVALID_CLIENT"
2492
+ );
2493
+ }
2494
+ return clientArg;
2495
+ }
2496
+ function getMcpUrl() {
2497
+ const base = "https://api.betterness.ai";
2498
+ if (!base) {
2499
+ throw new CliError(
2500
+ "No API URL configured. This is a build error \u2014 BETTERNESS_API_URL should be baked in at build time.",
2501
+ "CONFIG_MISSING"
2502
+ );
2503
+ }
2504
+ return `${base.replace(/\/$/, "")}/mcp`;
2505
+ }
2506
+ function buildMcpEntry(apiKey) {
2507
+ return {
2508
+ command: "npx",
2509
+ args: ["-y", "mcp-remote", getMcpUrl(), "--header", `Authorization: Bearer ${apiKey}`]
2510
+ };
2511
+ }
2512
+ function readConfigFile(filePath) {
2513
+ if (!existsSync4(filePath)) {
2514
+ return {};
2515
+ }
2516
+ try {
2517
+ const content = readFileSync3(filePath, "utf-8");
2518
+ return JSON.parse(content);
2519
+ } catch {
2520
+ throw new CliError(
2521
+ `Failed to parse config file: ${filePath}. Ensure it contains valid JSON.`,
2522
+ "INVALID_CONFIG"
2523
+ );
2524
+ }
2525
+ }
2526
+ function writeConfigFile(filePath, config) {
2527
+ const dir = dirname(filePath);
2528
+ if (!existsSync4(dir)) {
2529
+ mkdirSync3(dir, { recursive: true });
2530
+ }
2531
+ writeFileSync3(filePath, JSON.stringify(config, null, 2) + "\n");
2532
+ }
2533
+ function truncateKey(apiKey) {
2534
+ if (apiKey.length <= 10) return apiKey;
2535
+ return `${apiKey.slice(0, 6)}\u2026${apiKey.slice(-4)}`;
2536
+ }
2537
+ function extractKeyFromConfig(config) {
2538
+ const servers = config.mcpServers;
2539
+ if (!servers?.betterness) return null;
2540
+ const entry = servers.betterness;
2541
+ const args = entry.args;
2542
+ if (!args) return null;
2543
+ const headerIdx = args.indexOf("--header");
2544
+ if (headerIdx === -1 || headerIdx + 1 >= args.length) return null;
2545
+ const headerVal = args[headerIdx + 1];
2546
+ const match = headerVal.match(/Bearer\s+(.+)/);
2547
+ return match ? match[1] : null;
2548
+ }
2549
+ function registerMcpCommands(program2) {
2550
+ const mcp = program2.command("mcp").description("MCP server integration for AI agents (Claude, Cursor, Windsurf)");
2551
+ mcp.command("install").description("Install Betterness MCP integration into an AI client").argument("<client>", `Client to configure (${VALID_CLIENTS})`).option("--scope <scope>", "Config scope: global or project (claude-code only)", "global").option("--dry-run", "Print config without writing").action(async (clientArg, opts, cmd) => {
2552
+ try {
2553
+ const clientId = validateClient(clientArg);
2554
+ const client = CLIENTS[clientId];
2555
+ const scope = opts.scope === "project" ? "project" : "global";
2556
+ if (opts.scope === "project" && !client.supportsScope) {
2557
+ console.error(`Note: --scope is only supported for claude-code. Using default path for ${client.name}.`);
2558
+ }
2559
+ const globalOpts = cmd.optsWithGlobals();
2560
+ const credentials = await resolveCredentials(globalOpts.apiKey);
2561
+ const entry = buildMcpEntry(credentials.apiKey);
2562
+ const configPath = client.configPath(scope);
2563
+ if (opts.dryRun) {
2564
+ const redactedEntry = buildMcpEntry(`${credentials.apiKey.slice(0, 6)}\u2026****`);
2565
+ const preview = { mcpServers: { betterness: redactedEntry } };
2566
+ console.log(`Would write to: ${configPath}
2567
+ `);
2568
+ console.log(JSON.stringify(preview, null, 2));
2569
+ return;
2570
+ }
2571
+ const config = readConfigFile(configPath);
2572
+ let backupPath = null;
2573
+ if (existsSync4(configPath)) {
2574
+ backupPath = `${configPath}.bak`;
2575
+ copyFileSync(configPath, backupPath);
2576
+ }
2577
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
2578
+ config.mcpServers = {};
2579
+ }
2580
+ config.mcpServers.betterness = entry;
2581
+ writeConfigFile(configPath, config);
2582
+ outputRecord(cmd, {
2583
+ client: client.name,
2584
+ configFile: configPath,
2585
+ status: "installed",
2586
+ apiKey: truncateKey(credentials.apiKey),
2587
+ backup: backupPath ?? "none (new file)"
2588
+ });
2589
+ console.log(`
2590
+ Restart ${client.name} to activate. Then try asking:
2591
+ "List my connected devices"`);
2592
+ } catch (error) {
2593
+ outputError(error);
2594
+ }
2595
+ });
2596
+ mcp.command("uninstall").description("Remove Betterness MCP integration from an AI client").argument("<client>", `Client to unconfigure (${VALID_CLIENTS})`).option("--scope <scope>", "Config scope: global or project (claude-code only)", "global").action(async (clientArg, opts, cmd) => {
2597
+ try {
2598
+ const clientId = validateClient(clientArg);
2599
+ const client = CLIENTS[clientId];
2600
+ const scope = opts.scope === "project" ? "project" : "global";
2601
+ const configPath = client.configPath(scope);
2602
+ if (!existsSync4(configPath)) {
2603
+ console.log(`No config file found for ${client.name} at ${configPath}`);
2604
+ return;
2605
+ }
2606
+ const config = readConfigFile(configPath);
2607
+ const servers = config.mcpServers;
2608
+ if (!servers?.betterness) {
2609
+ console.log(`Betterness MCP is not configured for ${client.name}.`);
2610
+ return;
2611
+ }
2612
+ delete servers.betterness;
2613
+ if (Object.keys(servers).length === 0) {
2614
+ delete config.mcpServers;
2615
+ }
2616
+ writeConfigFile(configPath, config);
2617
+ outputRecord(cmd, {
2618
+ client: client.name,
2619
+ configFile: configPath,
2620
+ status: "removed"
2621
+ });
2622
+ } catch (error) {
2623
+ outputError(error);
2624
+ }
2625
+ });
2626
+ mcp.command("status").description("Show MCP integration status across all supported clients").action(async (_, cmd) => {
2627
+ try {
2628
+ const mcpUrl = getMcpUrl();
2629
+ const rows = [];
2630
+ for (const [id, client] of Object.entries(CLIENTS)) {
2631
+ const scopes = client.supportsScope ? ["global", "project"] : ["global"];
2632
+ for (const scope of scopes) {
2633
+ const configPath = client.configPath(scope);
2634
+ const exists = existsSync4(configPath);
2635
+ let configured = false;
2636
+ let apiKey = "";
2637
+ if (exists) {
2638
+ try {
2639
+ const config = readConfigFile(configPath);
2640
+ const key = extractKeyFromConfig(config);
2641
+ if (key) {
2642
+ configured = true;
2643
+ apiKey = truncateKey(key);
2644
+ }
2645
+ } catch {
2646
+ }
2647
+ }
2648
+ rows.push({
2649
+ client: client.supportsScope ? `${client.name} (${scope})` : client.name,
2650
+ status: configured ? "\u2713 configured" : "\u2717 not configured",
2651
+ apiKey: apiKey || "\u2014",
2652
+ configFile: configPath
2653
+ });
2654
+ }
2655
+ }
2656
+ console.log(`MCP Endpoint: ${mcpUrl}
2657
+ `);
2658
+ outputList(cmd, rows, [
2659
+ { key: "client", label: "Client", width: 26 },
2660
+ { key: "status", label: "Status", width: 18 },
2661
+ { key: "apiKey", label: "API Key", width: 16 },
2662
+ { key: "configFile", label: "Config File", width: 55 }
2663
+ ]);
2664
+ } catch (error) {
2665
+ outputError(error);
2666
+ }
2667
+ });
2668
+ }
2669
+
1846
2670
  // src/program.ts
1847
2671
  function createProgram() {
1848
2672
  const program2 = new Command();
1849
- program2.name("betterness").description("Betterness CLI - Agent-first terminal interface for the Betterness platform").version("1.1.2").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)");
2673
+ program2.name("betterness").description("Betterness CLI - Agent-first terminal interface for the Betterness platform").version("1.3.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)");
1850
2674
  registerAuthCommands(program2);
1851
2675
  registerProfileCommands(program2);
1852
2676
  registerBiomarkersCommands(program2);
@@ -1865,6 +2689,8 @@ function createProgram() {
1865
2689
  registerWorkflowCommands(program2);
1866
2690
  registerSchemaCommand(program2);
1867
2691
  registerDebugCommands(program2);
2692
+ registerHealthProfileCommands(program2);
2693
+ registerMcpCommands(program2);
1868
2694
  return program2;
1869
2695
  }
1870
2696