@betterness/cli 1.1.1 → 1.2.2
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/README.md +62 -7
- package/dist/index.js +742 -137
- package/package.json +1 -1
- package/CLI_REFERENCE.md +0 -493
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. --
|
|
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(
|
|
93
|
-
|
|
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.
|
|
411
|
+
"User-Agent": `betterness-cli/${"1.2.2"}`,
|
|
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:
|
|
459
|
+
const { readFileSync: readFileSync3 } = await import("fs");
|
|
177
460
|
const { basename, extname } = await import("path");
|
|
178
|
-
const buffer =
|
|
461
|
+
const buffer = readFileSync3(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.
|
|
475
|
+
"User-Agent": `betterness-cli/${"1.2.2"}`,
|
|
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
|
|
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
|
|
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("
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
|
567
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 [
|
|
779
|
-
if (!
|
|
780
|
-
for (const [
|
|
781
|
-
if (
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
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,
|
|
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 =
|
|
1146
|
+
const client = await createApiClient({ apiKey: globalOpts.apiKey });
|
|
831
1147
|
const to = opts.to ?? opts.from;
|
|
832
|
-
const
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
1664
|
+
const { existsSync: existsSync4 } = await import("fs");
|
|
1345
1665
|
const { resolve } = await import("path");
|
|
1346
1666
|
const filePath = resolve(opts.file);
|
|
1347
|
-
if (!
|
|
1667
|
+
if (!existsSync4(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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
1811
|
-
import { join as
|
|
1812
|
-
import { existsSync as
|
|
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 =
|
|
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.
|
|
2144
|
+
version: "1.2.2",
|
|
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(
|
|
2151
|
+
credentialsExists: String(existsSync3(credentialsPath)),
|
|
2152
|
+
tokensFile: tokensPath,
|
|
2153
|
+
tokensExists: String(existsSync3(tokensPath))
|
|
1827
2154
|
};
|
|
1828
|
-
if (stored?.email)
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
config.
|
|
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,286 @@ 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
|
+
|
|
1846
2450
|
// src/program.ts
|
|
1847
2451
|
function createProgram() {
|
|
1848
2452
|
const program2 = new Command();
|
|
1849
|
-
program2.name("betterness").description("Betterness CLI - Agent-first terminal interface for the Betterness platform").version("1.
|
|
2453
|
+
program2.name("betterness").description("Betterness CLI - Agent-first terminal interface for the Betterness platform").version("1.2.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)");
|
|
1850
2454
|
registerAuthCommands(program2);
|
|
1851
2455
|
registerProfileCommands(program2);
|
|
1852
2456
|
registerBiomarkersCommands(program2);
|
|
@@ -1865,6 +2469,7 @@ function createProgram() {
|
|
|
1865
2469
|
registerWorkflowCommands(program2);
|
|
1866
2470
|
registerSchemaCommand(program2);
|
|
1867
2471
|
registerDebugCommands(program2);
|
|
2472
|
+
registerHealthProfileCommands(program2);
|
|
1868
2473
|
return program2;
|
|
1869
2474
|
}
|
|
1870
2475
|
|