databody-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/dist/index.js +1252 -0
  4. package/package.json +57 -0
package/dist/index.js ADDED
@@ -0,0 +1,1252 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/lib/token.ts
7
+ import * as fs from "fs";
8
+
9
+ // src/lib/config.ts
10
+ import * as path from "path";
11
+ import * as os from "os";
12
+ var API_BASE_URL = process.env.DATABODY_API_URL || "http://localhost:3000";
13
+ var CLIENT_ID = "databody-mcp";
14
+ var CALLBACK_PORT = parseInt(
15
+ process.env.DATABODY_CALLBACK_PORT || "8787"
16
+ );
17
+ var TOKEN_FILE = path.join(os.homedir(), ".databody_token.json");
18
+
19
+ // src/lib/token.ts
20
+ function loadToken() {
21
+ try {
22
+ if (fs.existsSync(TOKEN_FILE)) {
23
+ const data = JSON.parse(fs.readFileSync(TOKEN_FILE, "utf-8"));
24
+ if (data.expires_at && Date.now() > data.expires_at) {
25
+ return data.refresh_token ? data : null;
26
+ }
27
+ return data;
28
+ }
29
+ } catch {
30
+ }
31
+ return null;
32
+ }
33
+ function saveToken(token) {
34
+ fs.writeFileSync(TOKEN_FILE, JSON.stringify(token, null, 2), {
35
+ mode: 384
36
+ });
37
+ }
38
+ function clearToken() {
39
+ try {
40
+ fs.unlinkSync(TOKEN_FILE);
41
+ } catch {
42
+ }
43
+ }
44
+
45
+ // src/lib/auth.ts
46
+ import * as http from "http";
47
+ import * as crypto from "crypto";
48
+ function generateCodeVerifier() {
49
+ return crypto.randomBytes(32).toString("base64url");
50
+ }
51
+ function generateCodeChallenge(verifier) {
52
+ return crypto.createHash("sha256").update(verifier).digest("base64url");
53
+ }
54
+ async function refreshAccessToken(refreshToken) {
55
+ try {
56
+ const response = await fetch(`${API_BASE_URL}/oauth/token`, {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
59
+ body: new URLSearchParams({
60
+ grant_type: "refresh_token",
61
+ refresh_token: refreshToken,
62
+ client_id: CLIENT_ID
63
+ })
64
+ });
65
+ if (!response.ok) return null;
66
+ const data = await response.json();
67
+ const token = {
68
+ access_token: data.access_token,
69
+ refresh_token: data.refresh_token,
70
+ expires_at: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
71
+ };
72
+ saveToken(token);
73
+ return token;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+ async function startOAuthFlow() {
79
+ const codeVerifier = generateCodeVerifier();
80
+ const codeChallenge = generateCodeChallenge(codeVerifier);
81
+ const state = crypto.randomBytes(16).toString("hex");
82
+ return new Promise((resolve2, reject) => {
83
+ const server = http.createServer(async (req, res) => {
84
+ const url = new URL(
85
+ req.url || "",
86
+ `http://localhost:${CALLBACK_PORT}`
87
+ );
88
+ if (url.pathname === "/callback") {
89
+ const code = url.searchParams.get("code");
90
+ const returnedState = url.searchParams.get("state");
91
+ const error = url.searchParams.get("error");
92
+ if (error) {
93
+ res.writeHead(400, { "Content-Type": "text/html" });
94
+ res.end(
95
+ `<html><body><h1>Authorization Failed</h1><p>${error}</p></body></html>`
96
+ );
97
+ server.close();
98
+ reject(new Error(`OAuth error: ${error}`));
99
+ return;
100
+ }
101
+ if (returnedState !== state) {
102
+ res.writeHead(400, { "Content-Type": "text/html" });
103
+ res.end("<html><body><h1>Invalid State</h1></body></html>");
104
+ server.close();
105
+ reject(new Error("Invalid OAuth state"));
106
+ return;
107
+ }
108
+ if (!code) {
109
+ res.writeHead(400, { "Content-Type": "text/html" });
110
+ res.end(
111
+ "<html><body><h1>No Code Received</h1></body></html>"
112
+ );
113
+ server.close();
114
+ reject(new Error("No authorization code received"));
115
+ return;
116
+ }
117
+ try {
118
+ const tokenResponse = await fetch(
119
+ `${API_BASE_URL}/oauth/token`,
120
+ {
121
+ method: "POST",
122
+ headers: {
123
+ "Content-Type": "application/x-www-form-urlencoded"
124
+ },
125
+ body: new URLSearchParams({
126
+ grant_type: "authorization_code",
127
+ code,
128
+ redirect_uri: `http://localhost:${CALLBACK_PORT}/callback`,
129
+ client_id: CLIENT_ID,
130
+ code_verifier: codeVerifier
131
+ })
132
+ }
133
+ );
134
+ if (!tokenResponse.ok) {
135
+ const errorText = await tokenResponse.text();
136
+ throw new Error(`Token exchange failed: ${errorText}`);
137
+ }
138
+ const tokenData = await tokenResponse.json();
139
+ const token = {
140
+ access_token: tokenData.access_token,
141
+ refresh_token: tokenData.refresh_token,
142
+ expires_at: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1e3 : void 0
143
+ };
144
+ saveToken(token);
145
+ res.writeHead(200, { "Content-Type": "text/html" });
146
+ res.end(`
147
+ <html>
148
+ <body style="font-family: system-ui; text-align: center; padding: 50px;">
149
+ <h1>Authentication Successful!</h1>
150
+ <p>You can close this window and return to your terminal.</p>
151
+ </body>
152
+ </html>
153
+ `);
154
+ server.close();
155
+ resolve2(token);
156
+ } catch (err) {
157
+ res.writeHead(500, { "Content-Type": "text/html" });
158
+ res.end(
159
+ `<html><body><h1>Token Exchange Failed</h1><p>${err}</p></body></html>`
160
+ );
161
+ server.close();
162
+ reject(err);
163
+ }
164
+ } else {
165
+ res.writeHead(404);
166
+ res.end("Not Found");
167
+ }
168
+ });
169
+ server.listen(CALLBACK_PORT, () => {
170
+ const authUrl = new URL(`${API_BASE_URL}/oauth/authorize`);
171
+ authUrl.searchParams.set("client_id", CLIENT_ID);
172
+ authUrl.searchParams.set(
173
+ "redirect_uri",
174
+ `http://localhost:${CALLBACK_PORT}/callback`
175
+ );
176
+ authUrl.searchParams.set("response_type", "code");
177
+ authUrl.searchParams.set("scope", "read write");
178
+ authUrl.searchParams.set("state", state);
179
+ authUrl.searchParams.set("code_challenge", codeChallenge);
180
+ authUrl.searchParams.set("code_challenge_method", "S256");
181
+ const url = authUrl.toString();
182
+ console.error(`
183
+ Open this URL in your browser to authenticate:
184
+ ${url}
185
+ `);
186
+ console.error("Waiting for authentication...");
187
+ import("child_process").then(({ exec }) => {
188
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
189
+ exec(`${cmd} "${url}"`);
190
+ });
191
+ });
192
+ server.on("error", (err) => {
193
+ reject(new Error(`Failed to start callback server: ${err.message}`));
194
+ });
195
+ setTimeout(() => {
196
+ server.close();
197
+ reject(new Error("Authentication timeout - please try again"));
198
+ }, 5 * 60 * 1e3);
199
+ });
200
+ }
201
+ async function authenticateWithPassword(email, password) {
202
+ const response = await fetch(`${API_BASE_URL}/oauth/token`, {
203
+ method: "POST",
204
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
205
+ body: new URLSearchParams({
206
+ grant_type: "password",
207
+ username: email,
208
+ password,
209
+ client_id: CLIENT_ID,
210
+ scope: "read write"
211
+ })
212
+ });
213
+ if (!response.ok) {
214
+ const error = await response.text();
215
+ throw new Error(`Authentication failed: ${error}`);
216
+ }
217
+ const data = await response.json();
218
+ const token = {
219
+ access_token: data.access_token,
220
+ refresh_token: data.refresh_token,
221
+ expires_at: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
222
+ };
223
+ saveToken(token);
224
+ return token;
225
+ }
226
+
227
+ // src/lib/api.ts
228
+ var AuthError = class extends Error {
229
+ constructor(message) {
230
+ super(message);
231
+ this.name = "AuthError";
232
+ }
233
+ };
234
+ async function getAccessToken() {
235
+ const token = loadToken();
236
+ if (!token) return null;
237
+ if (token.expires_at && Date.now() > token.expires_at) {
238
+ if (token.refresh_token) {
239
+ const refreshed = await refreshAccessToken(token.refresh_token);
240
+ return refreshed?.access_token || null;
241
+ }
242
+ return null;
243
+ }
244
+ return token.access_token;
245
+ }
246
+ async function apiCall(endpoint, method = "GET", body) {
247
+ const accessToken = await getAccessToken();
248
+ if (!accessToken) {
249
+ throw new AuthError("Not authenticated. Run 'databody auth login' first.");
250
+ }
251
+ const url = `${API_BASE_URL}/api/v1${endpoint}`;
252
+ const headers = {
253
+ "Content-Type": "application/json",
254
+ Accept: "application/json",
255
+ Authorization: `Bearer ${accessToken}`
256
+ };
257
+ const response = await fetch(url, {
258
+ method,
259
+ headers,
260
+ body: body ? JSON.stringify(body) : void 0
261
+ });
262
+ if (response.status === 401) {
263
+ const token = loadToken();
264
+ if (token?.refresh_token) {
265
+ const refreshed = await refreshAccessToken(token.refresh_token);
266
+ if (refreshed) {
267
+ headers.Authorization = `Bearer ${refreshed.access_token}`;
268
+ const retryResponse = await fetch(url, {
269
+ method,
270
+ headers,
271
+ body: body ? JSON.stringify(body) : void 0
272
+ });
273
+ if (retryResponse.ok) {
274
+ if (retryResponse.status === 204) return { success: true };
275
+ return retryResponse.json();
276
+ }
277
+ }
278
+ }
279
+ clearToken();
280
+ throw new AuthError(
281
+ "Session expired. Run 'databody auth login' to login again."
282
+ );
283
+ }
284
+ if (!response.ok) {
285
+ const error = await response.text();
286
+ throw new Error(`API error (${response.status}): ${error}`);
287
+ }
288
+ if (response.status === 204) {
289
+ return { success: true };
290
+ }
291
+ return response.json();
292
+ }
293
+
294
+ // src/lib/output.ts
295
+ function output(data, pretty) {
296
+ const json = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
297
+ console.log(json);
298
+ }
299
+ function errorOutput(message) {
300
+ console.error(JSON.stringify({ error: message }));
301
+ }
302
+ function withErrorHandler(fn) {
303
+ return (async (...args) => {
304
+ try {
305
+ await fn(...args);
306
+ } catch (err) {
307
+ errorOutput(err instanceof Error ? err.message : String(err));
308
+ process.exit(err instanceof AuthError ? 2 : 1);
309
+ }
310
+ });
311
+ }
312
+
313
+ // src/commands/auth.ts
314
+ function registerAuthCommands(program2) {
315
+ const auth = program2.command("auth").description("Authentication");
316
+ auth.command("login").description("Authenticate with DataBody").option("--password", "Use email/password instead of browser OAuth").option("-e, --email <email>", "Email address (for password login)").option("-p, --pass <password>", "Password (for password login)").option(
317
+ "--token <access_token>",
318
+ "Directly set an access token (for cross-machine auth)"
319
+ ).option(
320
+ "--refresh-token <refresh_token>",
321
+ "Refresh token (used with --token)"
322
+ ).action(
323
+ withErrorHandler(async (opts) => {
324
+ const pretty = program2.opts().pretty;
325
+ if (opts.token) {
326
+ const token = {
327
+ access_token: opts.token,
328
+ refresh_token: opts.refreshToken
329
+ };
330
+ saveToken(token);
331
+ try {
332
+ const user2 = await apiCall("/users/me");
333
+ output(
334
+ { authenticated: true, message: "Token saved and verified", user: user2 },
335
+ pretty
336
+ );
337
+ } catch {
338
+ clearToken();
339
+ output(
340
+ { authenticated: false, error: "Token is invalid or expired" },
341
+ pretty
342
+ );
343
+ process.exit(1);
344
+ }
345
+ return;
346
+ }
347
+ if (opts.password) {
348
+ if (!opts.email || !opts.pass) {
349
+ output(
350
+ {
351
+ error: "Email and password required. Use: databody auth login --password -e EMAIL -p PASS"
352
+ },
353
+ pretty
354
+ );
355
+ process.exit(1);
356
+ }
357
+ await authenticateWithPassword(opts.email, opts.pass);
358
+ const user2 = await apiCall("/users/me");
359
+ output({ authenticated: true, message: "Logged in", user: user2 }, pretty);
360
+ return;
361
+ }
362
+ await startOAuthFlow();
363
+ const user = await apiCall("/users/me");
364
+ output({ authenticated: true, message: "Logged in", user }, pretty);
365
+ })
366
+ );
367
+ auth.command("logout").description("Clear stored authentication").action(
368
+ withErrorHandler(async () => {
369
+ clearToken();
370
+ output(
371
+ { authenticated: false, message: "Logged out" },
372
+ program2.opts().pretty
373
+ );
374
+ })
375
+ );
376
+ auth.command("status").description("Check authentication status").action(
377
+ withErrorHandler(async () => {
378
+ const pretty = program2.opts().pretty;
379
+ const token = loadToken();
380
+ if (!token) {
381
+ output({ authenticated: false }, pretty);
382
+ return;
383
+ }
384
+ try {
385
+ const user = await apiCall("/users/me");
386
+ output({ authenticated: true, user }, pretty);
387
+ } catch {
388
+ output({ authenticated: false, expired: true }, pretty);
389
+ }
390
+ })
391
+ );
392
+ auth.command("export-token").description("Export auth token for use on another machine").option("--compact", "Output as a single base64 string").action(
393
+ withErrorHandler(async (opts) => {
394
+ const pretty = program2.opts().pretty;
395
+ const token = loadToken();
396
+ if (!token) {
397
+ output({ error: "Not authenticated" }, pretty);
398
+ process.exit(2);
399
+ }
400
+ if (opts.compact) {
401
+ const encoded = Buffer.from(JSON.stringify(token)).toString(
402
+ "base64"
403
+ );
404
+ console.log(encoded);
405
+ } else {
406
+ output(token, pretty);
407
+ }
408
+ })
409
+ );
410
+ auth.command("import-token").description("Import auth token from another machine").argument("[token_data]", "Base64-encoded or JSON token data").option("--stdin", "Read token data from stdin").action(
411
+ withErrorHandler(async (tokenData, opts) => {
412
+ const pretty = program2.opts().pretty;
413
+ let raw;
414
+ if (opts.stdin) {
415
+ raw = await readStdin();
416
+ } else if (tokenData) {
417
+ raw = tokenData;
418
+ } else {
419
+ output(
420
+ {
421
+ error: "Provide token data as argument or use --stdin. Export with: databody auth export-token --compact"
422
+ },
423
+ pretty
424
+ );
425
+ process.exit(1);
426
+ return;
427
+ }
428
+ let token;
429
+ try {
430
+ token = JSON.parse(raw);
431
+ } catch {
432
+ try {
433
+ token = JSON.parse(Buffer.from(raw.trim(), "base64").toString("utf-8"));
434
+ } catch {
435
+ output({ error: "Invalid token data. Expected JSON or base64-encoded JSON." }, pretty);
436
+ process.exit(1);
437
+ return;
438
+ }
439
+ }
440
+ if (!token.access_token) {
441
+ output({ error: "Token data missing access_token field" }, pretty);
442
+ process.exit(1);
443
+ return;
444
+ }
445
+ saveToken(token);
446
+ try {
447
+ const user = await apiCall("/users/me");
448
+ output(
449
+ { authenticated: true, message: "Token imported and verified", user },
450
+ pretty
451
+ );
452
+ } catch {
453
+ clearToken();
454
+ output(
455
+ { authenticated: false, error: "Imported token is invalid or expired" },
456
+ pretty
457
+ );
458
+ process.exit(1);
459
+ }
460
+ })
461
+ );
462
+ }
463
+ function readStdin() {
464
+ return new Promise((resolve2) => {
465
+ let data = "";
466
+ process.stdin.setEncoding("utf-8");
467
+ process.stdin.on("data", (chunk) => data += chunk);
468
+ process.stdin.on("end", () => resolve2(data));
469
+ process.stdin.resume();
470
+ });
471
+ }
472
+
473
+ // src/commands/user.ts
474
+ function registerUserCommands(program2) {
475
+ const user = program2.command("user").description("User profile");
476
+ user.command("profile").description("Get current user profile").action(
477
+ withErrorHandler(async () => {
478
+ const result = await apiCall("/users/me");
479
+ output(result, program2.opts().pretty);
480
+ })
481
+ );
482
+ user.command("update").description("Update profile settings").option("--name <name>", "Display name").option("--height <cm>", "Height in cm").option("--sex <sex>", "Sex (male/female)").option("--birth-date <date>", "Birth date (YYYY-MM-DD)").option(
483
+ "--activity-level <level>",
484
+ "Activity level (sedentary/lightly_active/moderately_active/very_active/extra_active)"
485
+ ).option("--timezone <tz>", "Timezone (e.g. America/New_York)").option("--weight-unit <unit>", "Weight unit (lbs/kg)").option("--height-unit <unit>", "Height unit (in/cm)").action(
486
+ withErrorHandler(async (opts) => {
487
+ const body = {};
488
+ if (opts.name) body.name = opts.name;
489
+ if (opts.height) body.height_cm = parseFloat(opts.height);
490
+ if (opts.sex) body.sex = opts.sex;
491
+ if (opts.birthDate) body.birth_date = opts.birthDate;
492
+ if (opts.activityLevel) body.activity_level = opts.activityLevel;
493
+ if (opts.timezone) body.timezone = opts.timezone;
494
+ if (opts.weightUnit) body.weight_unit = opts.weightUnit;
495
+ if (opts.heightUnit) body.height_unit = opts.heightUnit;
496
+ const result = await apiCall("/users/me", "PATCH", body);
497
+ output(result, program2.opts().pretty);
498
+ })
499
+ );
500
+ user.command("change-password").description("Change password").requiredOption("--current <password>", "Current password").requiredOption("--new <password>", "New password").action(
501
+ withErrorHandler(
502
+ async (opts) => {
503
+ const result = await apiCall("/users/me/change_password", "POST", {
504
+ current_password: opts.current,
505
+ new_password: opts.new,
506
+ new_password_confirmation: opts.new
507
+ });
508
+ output(result, program2.opts().pretty);
509
+ }
510
+ )
511
+ );
512
+ user.command("change-email").description("Change email address").requiredOption("--password <password>", "Current password").requiredOption("--email <email>", "New email address").action(
513
+ withErrorHandler(
514
+ async (opts) => {
515
+ const result = await apiCall("/users/me/change_email", "POST", {
516
+ password: opts.password,
517
+ new_email_address: opts.email
518
+ });
519
+ output(result, program2.opts().pretty);
520
+ }
521
+ )
522
+ );
523
+ }
524
+
525
+ // src/commands/health.ts
526
+ function registerHealthCommands(program2) {
527
+ const health = program2.command("health").description("Health data & summary");
528
+ health.command("summary").description(
529
+ "Today's health summary with macros, goals, workouts, and trends"
530
+ ).action(
531
+ withErrorHandler(async () => {
532
+ const result = await apiCall("/health/summary");
533
+ output(result, program2.opts().pretty);
534
+ })
535
+ );
536
+ health.command("history").description("Historical health stats with trend analysis").option("--days <n>", "Number of days (1-365, default 30)", "30").action(
537
+ withErrorHandler(async (opts) => {
538
+ const result = await apiCall(`/health/history?days=${opts.days}`);
539
+ output(result, program2.opts().pretty);
540
+ })
541
+ );
542
+ }
543
+
544
+ // src/commands/nutrition.ts
545
+ function registerNutritionCommands(program2) {
546
+ const nutrition = program2.command("nutrition").description("Nutrition logging");
547
+ nutrition.command("today").description("Today's nutrition logs with totals and remaining macros").action(
548
+ withErrorHandler(async () => {
549
+ const result = await apiCall("/nutrition/today");
550
+ output(result, program2.opts().pretty);
551
+ })
552
+ );
553
+ nutrition.command("history").description("Nutrition history for date range").option("--start <date>", "Start date (YYYY-MM-DD)").option("--end <date>", "End date (YYYY-MM-DD)").action(
554
+ withErrorHandler(async (opts) => {
555
+ const params = new URLSearchParams();
556
+ if (opts.start) params.set("start_date", opts.start);
557
+ if (opts.end) params.set("end_date", opts.end);
558
+ const qs = params.toString();
559
+ const result = await apiCall(`/nutrition/history${qs ? `?${qs}` : ""}`);
560
+ output(result, program2.opts().pretty);
561
+ })
562
+ );
563
+ nutrition.command("get").description("Get a specific nutrition log").argument("<log_id>", "Nutrition log ID").action(
564
+ withErrorHandler(async (logId) => {
565
+ const result = await apiCall(`/nutrition/${logId}`);
566
+ output(result, program2.opts().pretty);
567
+ })
568
+ );
569
+ nutrition.command("log").description("Log food items").requiredOption(
570
+ "--meal <type>",
571
+ "Meal type (breakfast/lunch/dinner/snack)"
572
+ ).option("--items <json>", "JSON array of food items").option("--logged-at <iso>", "Timestamp (ISO format)").option("--notes <text>", "Notes").option("--stdin", "Read items JSON from stdin").action(
573
+ withErrorHandler(
574
+ async (opts) => {
575
+ let items;
576
+ if (opts.stdin) {
577
+ const raw = await readStdin2();
578
+ items = JSON.parse(raw);
579
+ } else if (opts.items) {
580
+ items = JSON.parse(opts.items);
581
+ } else {
582
+ output(
583
+ { error: "Provide --items JSON or use --stdin" },
584
+ program2.opts().pretty
585
+ );
586
+ process.exit(1);
587
+ return;
588
+ }
589
+ const body = {
590
+ meal_type: opts.meal,
591
+ nutrition_items_attributes: items
592
+ };
593
+ if (opts.loggedAt) body.logged_at = opts.loggedAt;
594
+ if (opts.notes) body.notes = opts.notes;
595
+ const result = await apiCall("/nutrition", "POST", body);
596
+ output(result, program2.opts().pretty);
597
+ }
598
+ )
599
+ );
600
+ nutrition.command("update").description("Update a nutrition log").argument("<log_id>", "Nutrition log ID").option("--meal <type>", "Meal type").option("--logged-at <iso>", "Timestamp").option("--notes <text>", "Notes").action(
601
+ withErrorHandler(
602
+ async (logId, opts) => {
603
+ const body = {};
604
+ if (opts.meal) body.meal_type = opts.meal;
605
+ if (opts.loggedAt) body.logged_at = opts.loggedAt;
606
+ if (opts.notes) body.notes = opts.notes;
607
+ const result = await apiCall(`/nutrition/${logId}`, "PATCH", body);
608
+ output(result, program2.opts().pretty);
609
+ }
610
+ )
611
+ );
612
+ nutrition.command("delete").description("Delete a nutrition log").argument("<log_id>", "Nutrition log ID").action(
613
+ withErrorHandler(async (logId) => {
614
+ const result = await apiCall(`/nutrition/${logId}`, "DELETE");
615
+ output(result, program2.opts().pretty);
616
+ })
617
+ );
618
+ nutrition.command("add-item").description("Add a food item to an existing log").argument("<log_id>", "Nutrition log ID").requiredOption("--name <name>", "Food name").option("--brand <brand>", "Brand").option("--serving-size <size>", "Serving size (e.g. 150g)").option("--serving-quantity <qty>", "Serving quantity").option("--calories <n>", "Calories").option("--protein <n>", "Protein grams").option("--carbs <n>", "Carbs grams").option("--fat <n>", "Fat grams").option("--fiber <n>", "Fiber grams").action(
619
+ withErrorHandler(
620
+ async (logId, opts) => {
621
+ const body = { name: opts.name };
622
+ if (opts.brand) body.brand = opts.brand;
623
+ if (opts.servingSize) body.serving_size = opts.servingSize;
624
+ if (opts.servingQuantity)
625
+ body.serving_quantity = parseFloat(opts.servingQuantity);
626
+ if (opts.calories) body.calories = parseFloat(opts.calories);
627
+ if (opts.protein) body.protein_grams = parseFloat(opts.protein);
628
+ if (opts.carbs) body.carbs_grams = parseFloat(opts.carbs);
629
+ if (opts.fat) body.fat_grams = parseFloat(opts.fat);
630
+ if (opts.fiber) body.fiber_grams = parseFloat(opts.fiber);
631
+ const result = await apiCall(
632
+ `/nutrition/${logId}/items`,
633
+ "POST",
634
+ body
635
+ );
636
+ output(result, program2.opts().pretty);
637
+ }
638
+ )
639
+ );
640
+ nutrition.command("delete-item").description("Delete a food item from a log").argument("<log_id>", "Nutrition log ID").argument("<item_id>", "Nutrition item ID").action(
641
+ withErrorHandler(async (logId, itemId) => {
642
+ const result = await apiCall(
643
+ `/nutrition/${logId}/items/${itemId}`,
644
+ "DELETE"
645
+ );
646
+ output(result, program2.opts().pretty);
647
+ })
648
+ );
649
+ }
650
+ function readStdin2() {
651
+ return new Promise((resolve2) => {
652
+ let data = "";
653
+ process.stdin.setEncoding("utf-8");
654
+ process.stdin.on("data", (chunk) => data += chunk);
655
+ process.stdin.on("end", () => resolve2(data));
656
+ process.stdin.resume();
657
+ });
658
+ }
659
+
660
+ // src/commands/food.ts
661
+ function registerFoodCommands(program2) {
662
+ const food = program2.command("food").description("Food search & favorites");
663
+ food.command("search").description("Search food databases").argument("<query>", "Search query").option(
664
+ "--source <source>",
665
+ "Database (fatsecret/usda/openfoodfacts/all)",
666
+ "all"
667
+ ).option("--page <n>", "Page number", "1").action(
668
+ withErrorHandler(
669
+ async (query, opts) => {
670
+ const params = new URLSearchParams({
671
+ query,
672
+ source: opts.source,
673
+ page: opts.page
674
+ });
675
+ const result = await apiCall(`/foods/search?${params}`);
676
+ output(result, program2.opts().pretty);
677
+ }
678
+ )
679
+ );
680
+ food.command("details").description("Get detailed nutrition for a food").argument("<food_id>", "Food ID (e.g. fatsecret_12345)").action(
681
+ withErrorHandler(async (foodId) => {
682
+ const result = await apiCall(`/foods/details/${foodId}`);
683
+ output(result, program2.opts().pretty);
684
+ })
685
+ );
686
+ food.command("barcode").description("Look up food by barcode").argument("<barcode>", "UPC/EAN barcode").action(
687
+ withErrorHandler(async (barcode) => {
688
+ const result = await apiCall(`/foods/barcode/${barcode}`);
689
+ output(result, program2.opts().pretty);
690
+ })
691
+ );
692
+ food.command("favorites").description("List saved food favorites").action(
693
+ withErrorHandler(async () => {
694
+ const result = await apiCall("/foods/favorites");
695
+ output(result, program2.opts().pretty);
696
+ })
697
+ );
698
+ food.command("add-favorite").description("Save a food to favorites").requiredOption("--name <name>", "Food name").option("--brand <brand>", "Brand").option("--serving-size <size>", "Serving size").option("--calories <n>", "Calories").option("--protein <n>", "Protein grams").option("--carbs <n>", "Carbs grams").option("--fat <n>", "Fat grams").option("--fiber <n>", "Fiber grams").option("--barcode <code>", "Barcode").action(
699
+ withErrorHandler(async (opts) => {
700
+ const body = { name: opts.name };
701
+ if (opts.brand) body.brand = opts.brand;
702
+ if (opts.servingSize) body.serving_size = opts.servingSize;
703
+ if (opts.calories) body.calories = parseFloat(opts.calories);
704
+ if (opts.protein) body.protein_grams = parseFloat(opts.protein);
705
+ if (opts.carbs) body.carbs_grams = parseFloat(opts.carbs);
706
+ if (opts.fat) body.fat_grams = parseFloat(opts.fat);
707
+ if (opts.fiber) body.fiber_grams = parseFloat(opts.fiber);
708
+ if (opts.barcode) body.barcode = opts.barcode;
709
+ const result = await apiCall("/foods/favorites", "POST", body);
710
+ output(result, program2.opts().pretty);
711
+ })
712
+ );
713
+ food.command("remove-favorite").description("Remove a food from favorites").argument("<id>", "Favorite ID").action(
714
+ withErrorHandler(async (id) => {
715
+ const result = await apiCall(`/foods/favorites/${id}`, "DELETE");
716
+ output(result, program2.opts().pretty);
717
+ })
718
+ );
719
+ food.command("recents").description("Get recently logged foods").action(
720
+ withErrorHandler(async () => {
721
+ const result = await apiCall("/foods/recents");
722
+ output(result, program2.opts().pretty);
723
+ })
724
+ );
725
+ }
726
+
727
+ // src/commands/goals.ts
728
+ function registerGoalsCommands(program2) {
729
+ const goals = program2.command("goals").description("Fitness goals");
730
+ goals.command("current").description("Get active goal with macro targets").action(
731
+ withErrorHandler(async () => {
732
+ const result = await apiCall("/goals/current");
733
+ output(result, program2.opts().pretty);
734
+ })
735
+ );
736
+ goals.command("list").description("List all goals").action(
737
+ withErrorHandler(async () => {
738
+ const result = await apiCall("/goals");
739
+ output(result, program2.opts().pretty);
740
+ })
741
+ );
742
+ goals.command("get").description("Get a specific goal").argument("<id>", "Goal ID").action(
743
+ withErrorHandler(async (id) => {
744
+ const result = await apiCall(`/goals/${id}`);
745
+ output(result, program2.opts().pretty);
746
+ })
747
+ );
748
+ goals.command("create").description("Create a new goal").option("--calories <n>", "Daily calorie target").option("--protein <n>", "Daily protein grams").option("--carbs <n>", "Daily carbs grams").option("--fat <n>", "Daily fat grams").option("--strategy <s>", "Strategy (cut/maintain/bulk)").option("--target-bf <n>", "Target body fat percentage").option("--target-date <date>", "Target date (YYYY-MM-DD)").option("--active", "Set as active goal").action(
749
+ withErrorHandler(async (opts) => {
750
+ const body = {};
751
+ if (opts.calories) body.daily_calorie_target = parseFloat(opts.calories);
752
+ if (opts.protein) body.daily_protein_grams = parseFloat(opts.protein);
753
+ if (opts.carbs) body.daily_carbs_grams = parseFloat(opts.carbs);
754
+ if (opts.fat) body.daily_fat_grams = parseFloat(opts.fat);
755
+ if (opts.strategy) body.strategy = opts.strategy;
756
+ if (opts.targetBf)
757
+ body.target_body_fat_percentage = parseFloat(opts.targetBf);
758
+ if (opts.targetDate) body.target_date = opts.targetDate;
759
+ if (opts.active) body.active = true;
760
+ const result = await apiCall("/goals", "POST", body);
761
+ output(result, program2.opts().pretty);
762
+ })
763
+ );
764
+ goals.command("update").description("Update a goal").argument("<id>", "Goal ID").option("--calories <n>", "Daily calorie target").option("--protein <n>", "Daily protein grams").option("--carbs <n>", "Daily carbs grams").option("--fat <n>", "Daily fat grams").option("--strategy <s>", "Strategy (cut/maintain/bulk)").option("--target-bf <n>", "Target body fat percentage").option("--target-date <date>", "Target date (YYYY-MM-DD)").option("--active <bool>", "Active status (true/false)").option("--protein-per-lb <n>", "Protein per lb lean body mass").option("--fat-per-lb <n>", "Fat per lb body weight").option("--weekly-loss <n>", "Weekly weight loss percentage").option("--flex-weekends", "Enable flexible weekends").option("--no-flex-weekends", "Disable flexible weekends").option("--flex-weekends-pct <n>", "Flexible weekends percentage").action(
765
+ withErrorHandler(async (id, opts) => {
766
+ const body = {};
767
+ if (opts.calories) body.daily_calorie_target = parseFloat(opts.calories);
768
+ if (opts.protein) body.daily_protein_grams = parseFloat(opts.protein);
769
+ if (opts.carbs) body.daily_carbs_grams = parseFloat(opts.carbs);
770
+ if (opts.fat) body.daily_fat_grams = parseFloat(opts.fat);
771
+ if (opts.strategy) body.strategy = opts.strategy;
772
+ if (opts.targetBf)
773
+ body.target_body_fat_percentage = parseFloat(opts.targetBf);
774
+ if (opts.targetDate) body.target_date = opts.targetDate;
775
+ if (opts.active !== void 0) body.active = opts.active === "true";
776
+ if (opts.proteinPerLb)
777
+ body.protein_per_lb_lbm = parseFloat(opts.proteinPerLb);
778
+ if (opts.fatPerLb) body.fat_per_lb = parseFloat(opts.fatPerLb);
779
+ if (opts.weeklyLoss)
780
+ body.weekly_weight_loss_percentage = parseFloat(opts.weeklyLoss);
781
+ if (opts.flexWeekends === true) body.flexible_weekends_enabled = true;
782
+ if (opts.flexWeekends === false) body.flexible_weekends_enabled = false;
783
+ if (opts.flexWeekendsPct)
784
+ body.flexible_weekends_percentage = parseFloat(
785
+ opts.flexWeekendsPct
786
+ );
787
+ const result = await apiCall(`/goals/${id}`, "PATCH", body);
788
+ output(result, program2.opts().pretty);
789
+ })
790
+ );
791
+ goals.command("delete").description("Delete a goal").argument("<id>", "Goal ID").action(
792
+ withErrorHandler(async (id) => {
793
+ const result = await apiCall(`/goals/${id}`, "DELETE");
794
+ output(result, program2.opts().pretty);
795
+ })
796
+ );
797
+ goals.command("calculate").description("Recalculate macros based on current health data").argument("<id>", "Goal ID").action(
798
+ withErrorHandler(async (id) => {
799
+ const result = await apiCall(`/goals/${id}/calculate`, "POST");
800
+ output(result, program2.opts().pretty);
801
+ })
802
+ );
803
+ }
804
+
805
+ // src/commands/workouts.ts
806
+ function registerWorkoutsCommands(program2) {
807
+ const workouts = program2.command("workouts").description("Workout tracking");
808
+ workouts.command("recent").description("Recent workouts (last 7 days)").option("--limit <n>", "Number of workouts", "10").action(
809
+ withErrorHandler(async (opts) => {
810
+ const result = await apiCall(`/workouts/recent?limit=${opts.limit}`);
811
+ output(result, program2.opts().pretty);
812
+ })
813
+ );
814
+ workouts.command("list").description("List workouts with filters").option("--start <date>", "Start date (YYYY-MM-DD)").option("--end <date>", "End date (YYYY-MM-DD)").option("--type <type>", "Workout type").option("--limit <n>", "Maximum results", "50").action(
815
+ withErrorHandler(
816
+ async (opts) => {
817
+ const params = new URLSearchParams({ limit: opts.limit });
818
+ if (opts.start) params.set("start_date", opts.start);
819
+ if (opts.end) params.set("end_date", opts.end);
820
+ if (opts.type) params.set("type", opts.type);
821
+ const result = await apiCall(`/workouts?${params}`);
822
+ output(result, program2.opts().pretty);
823
+ }
824
+ )
825
+ );
826
+ workouts.command("get").description("Get a specific workout").argument("<id>", "Workout ID").action(
827
+ withErrorHandler(async (id) => {
828
+ const result = await apiCall(`/workouts/${id}`);
829
+ output(result, program2.opts().pretty);
830
+ })
831
+ );
832
+ workouts.command("create").description("Log a new workout").requiredOption("--type <type>", "Workout type (running, cycling, etc.)").requiredOption("--started-at <iso>", "Start time (ISO format)").option("--duration <min>", "Duration in minutes").option("--ended-at <iso>", "End time (ISO format)").option("--calories <n>", "Calories burned").option("--heart-rate <n>", "Average heart rate").option("--distance <m>", "Distance in meters").action(
833
+ withErrorHandler(async (opts) => {
834
+ const body = {
835
+ workout_type: opts.type,
836
+ started_at: opts.startedAt
837
+ };
838
+ if (opts.duration)
839
+ body.duration_minutes = parseFloat(opts.duration);
840
+ if (opts.endedAt) body.ended_at = opts.endedAt;
841
+ if (opts.calories)
842
+ body.calories_burned = parseFloat(opts.calories);
843
+ if (opts.heartRate)
844
+ body.average_heart_rate = parseFloat(opts.heartRate);
845
+ if (opts.distance)
846
+ body.distance_meters = parseFloat(opts.distance);
847
+ const result = await apiCall("/workouts", "POST", body);
848
+ output(result, program2.opts().pretty);
849
+ })
850
+ );
851
+ workouts.command("update").description("Update a workout").argument("<id>", "Workout ID").option("--type <type>", "Workout type").option("--started-at <iso>", "Start time").option("--duration <min>", "Duration in minutes").option("--ended-at <iso>", "End time").option("--calories <n>", "Calories burned").option("--heart-rate <n>", "Average heart rate").option("--distance <m>", "Distance in meters").action(
852
+ withErrorHandler(async (id, opts) => {
853
+ const body = {};
854
+ if (opts.type) body.workout_type = opts.type;
855
+ if (opts.startedAt) body.started_at = opts.startedAt;
856
+ if (opts.duration)
857
+ body.duration_minutes = parseFloat(opts.duration);
858
+ if (opts.endedAt) body.ended_at = opts.endedAt;
859
+ if (opts.calories)
860
+ body.calories_burned = parseFloat(opts.calories);
861
+ if (opts.heartRate)
862
+ body.average_heart_rate = parseFloat(opts.heartRate);
863
+ if (opts.distance)
864
+ body.distance_meters = parseFloat(opts.distance);
865
+ const result = await apiCall(`/workouts/${id}`, "PATCH", body);
866
+ output(result, program2.opts().pretty);
867
+ })
868
+ );
869
+ workouts.command("delete").description("Delete a workout").argument("<id>", "Workout ID").action(
870
+ withErrorHandler(async (id) => {
871
+ const result = await apiCall(`/workouts/${id}`, "DELETE");
872
+ output(result, program2.opts().pretty);
873
+ })
874
+ );
875
+ }
876
+
877
+ // src/commands/ai.ts
878
+ import * as fs2 from "fs";
879
+ import * as path2 from "path";
880
+ function registerAiCommands(program2) {
881
+ const ai = program2.command("ai").description("AI features & coaching");
882
+ ai.command("chat").description("Chat with the AI nutrition coach").argument("<message>", "Your message").option("--thread <id>", "Thread ID for conversation context").option("--household <id>", "Household ID for family context").action(
883
+ withErrorHandler(
884
+ async (message, opts) => {
885
+ const body = { message };
886
+ if (opts.thread) body.thread_id = parseInt(opts.thread);
887
+ if (opts.household) body.household_id = parseInt(opts.household);
888
+ const result = await apiCall("/ai/chat", "POST", body);
889
+ output(result, program2.opts().pretty);
890
+ }
891
+ )
892
+ );
893
+ ai.command("suggestions").description("Get AI meal suggestions based on remaining macros").option("--preferences <text>", "Meal preferences").option("--household <id>", "Household ID").action(
894
+ withErrorHandler(
895
+ async (opts) => {
896
+ const params = new URLSearchParams();
897
+ if (opts.preferences) params.set("preferences", opts.preferences);
898
+ if (opts.household) params.set("household_id", opts.household);
899
+ const qs = params.toString();
900
+ const result = await apiCall(
901
+ `/ai/suggestions${qs ? `?${qs}` : ""}`
902
+ );
903
+ output(result, program2.opts().pretty);
904
+ }
905
+ )
906
+ );
907
+ ai.command("analyze-photo").description("Analyze a meal photo for nutrition estimation").option("--file <path>", "Path to image file").option("--base64 <data>", "Base64-encoded image data").option("--mime <type>", "MIME type (default: auto-detect from file)").action(
908
+ withErrorHandler(
909
+ async (opts) => {
910
+ let imageData;
911
+ let mimeType;
912
+ if (opts.file) {
913
+ const filePath = path2.resolve(opts.file);
914
+ const buffer = fs2.readFileSync(filePath);
915
+ imageData = buffer.toString("base64");
916
+ mimeType = opts.mime || detectMimeType(filePath) || "image/jpeg";
917
+ const sizeMB = buffer.length / (1024 * 1024);
918
+ if (sizeMB > 20) {
919
+ output(
920
+ { error: `File too large (${sizeMB.toFixed(1)}MB). Max 20MB.` },
921
+ program2.opts().pretty
922
+ );
923
+ process.exit(1);
924
+ }
925
+ } else if (opts.base64) {
926
+ imageData = opts.base64;
927
+ mimeType = opts.mime || "image/jpeg";
928
+ } else {
929
+ output(
930
+ { error: "Provide --file or --base64" },
931
+ program2.opts().pretty
932
+ );
933
+ process.exit(1);
934
+ return;
935
+ }
936
+ const result = await apiCall("/ai/analyze_photo", "POST", {
937
+ image: imageData,
938
+ mime_type: mimeType
939
+ });
940
+ output(result, program2.opts().pretty);
941
+ }
942
+ )
943
+ );
944
+ ai.command("parse").description("Parse a food description into structured nutrition data").argument("<description>", "Food description text").action(
945
+ withErrorHandler(async (description) => {
946
+ const result = await apiCall("/ai/parse_description", "POST", {
947
+ description
948
+ });
949
+ output(result, program2.opts().pretty);
950
+ })
951
+ );
952
+ ai.command("expand").description("Expand a meal into individual ingredients with macros").requiredOption("--meal <name>", "Meal name").requiredOption("--ingredients <list>", "Comma-separated ingredients").option("--calories <n>", "Total calories").option("--protein <n>", "Total protein").option("--carbs <n>", "Total carbs").option("--fat <n>", "Total fat").action(
953
+ withErrorHandler(
954
+ async (opts) => {
955
+ const body = {
956
+ meal_name: opts.meal,
957
+ ingredients: opts.ingredients
958
+ };
959
+ if (opts.calories || opts.protein || opts.carbs || opts.fat) {
960
+ body.total_macros = {
961
+ calories: opts.calories ? parseFloat(opts.calories) : void 0,
962
+ protein: opts.protein ? parseFloat(opts.protein) : void 0,
963
+ carbs: opts.carbs ? parseFloat(opts.carbs) : void 0,
964
+ fat: opts.fat ? parseFloat(opts.fat) : void 0
965
+ };
966
+ }
967
+ const result = await apiCall(
968
+ "/ai/expand_meal_ingredients",
969
+ "POST",
970
+ body
971
+ );
972
+ output(result, program2.opts().pretty);
973
+ }
974
+ )
975
+ );
976
+ ai.command("greeting").description("Get a personalized daily greeting").action(
977
+ withErrorHandler(async () => {
978
+ const result = await apiCall("/ai/daily_greeting");
979
+ output(result, program2.opts().pretty);
980
+ })
981
+ );
982
+ ai.command("token-usage").description("Get AI token usage statistics").action(
983
+ withErrorHandler(async () => {
984
+ const result = await apiCall("/ai/token_usage");
985
+ output(result, program2.opts().pretty);
986
+ })
987
+ );
988
+ ai.command("chat-history").description("Get chat message history").option("--thread <id>", "Thread ID").action(
989
+ withErrorHandler(async (opts) => {
990
+ const params = new URLSearchParams();
991
+ if (opts.thread) params.set("thread_id", opts.thread);
992
+ const qs = params.toString();
993
+ const result = await apiCall(
994
+ `/ai/chat_history${qs ? `?${qs}` : ""}`
995
+ );
996
+ output(result, program2.opts().pretty);
997
+ })
998
+ );
999
+ }
1000
+ function detectMimeType(filePath) {
1001
+ const ext = path2.extname(filePath).toLowerCase();
1002
+ const mimeTypes = {
1003
+ ".jpg": "image/jpeg",
1004
+ ".jpeg": "image/jpeg",
1005
+ ".png": "image/png",
1006
+ ".gif": "image/gif",
1007
+ ".webp": "image/webp",
1008
+ ".heic": "image/heic",
1009
+ ".heif": "image/heif"
1010
+ };
1011
+ return mimeTypes[ext] || null;
1012
+ }
1013
+
1014
+ // src/commands/threads.ts
1015
+ function registerThreadsCommands(program2) {
1016
+ const threads = program2.command("threads").description("Chat threads");
1017
+ threads.command("list").description("List chat threads with previews").action(
1018
+ withErrorHandler(async () => {
1019
+ const result = await apiCall("/chat_threads");
1020
+ output(result, program2.opts().pretty);
1021
+ })
1022
+ );
1023
+ threads.command("get").description("Get a thread with all messages").argument("<id>", "Thread ID").action(
1024
+ withErrorHandler(async (id) => {
1025
+ const result = await apiCall(`/chat_threads/${id}`);
1026
+ output(result, program2.opts().pretty);
1027
+ })
1028
+ );
1029
+ threads.command("create").description("Create a new chat thread").option("--title <title>", "Thread title").action(
1030
+ withErrorHandler(async (opts) => {
1031
+ const body = {};
1032
+ if (opts.title) body.title = opts.title;
1033
+ const result = await apiCall("/chat_threads", "POST", body);
1034
+ output(result, program2.opts().pretty);
1035
+ })
1036
+ );
1037
+ threads.command("delete").description("Delete a chat thread").argument("<id>", "Thread ID").action(
1038
+ withErrorHandler(async (id) => {
1039
+ const result = await apiCall(`/chat_threads/${id}`, "DELETE");
1040
+ output(result, program2.opts().pretty);
1041
+ })
1042
+ );
1043
+ threads.command("generate-title").description("AI-generate a title from thread messages").argument("<id>", "Thread ID").action(
1044
+ withErrorHandler(async (id) => {
1045
+ const result = await apiCall(
1046
+ `/chat_threads/${id}/generate_title`,
1047
+ "POST"
1048
+ );
1049
+ output(result, program2.opts().pretty);
1050
+ })
1051
+ );
1052
+ }
1053
+
1054
+ // src/commands/notes.ts
1055
+ function registerNotesCommands(program2) {
1056
+ const notes = program2.command("notes").description("Personal notes for AI context");
1057
+ notes.command("list").description("List all notes").action(
1058
+ withErrorHandler(async () => {
1059
+ const result = await apiCall("/notes");
1060
+ output(result, program2.opts().pretty);
1061
+ })
1062
+ );
1063
+ notes.command("create").description("Create a note").argument("<content>", "Note content").action(
1064
+ withErrorHandler(async (content) => {
1065
+ const result = await apiCall("/notes", "POST", {
1066
+ content,
1067
+ created_via: "cli"
1068
+ });
1069
+ output(result, program2.opts().pretty);
1070
+ })
1071
+ );
1072
+ notes.command("update").description("Update a note").argument("<id>", "Note ID").argument("<content>", "New content").action(
1073
+ withErrorHandler(async (id, content) => {
1074
+ const result = await apiCall(`/notes/${id}`, "PATCH", { content });
1075
+ output(result, program2.opts().pretty);
1076
+ })
1077
+ );
1078
+ notes.command("delete").description("Delete a note").argument("<id>", "Note ID").action(
1079
+ withErrorHandler(async (id) => {
1080
+ const result = await apiCall(`/notes/${id}`, "DELETE");
1081
+ output(result, program2.opts().pretty);
1082
+ })
1083
+ );
1084
+ }
1085
+
1086
+ // src/commands/households.ts
1087
+ function registerHouseholdsCommands(program2) {
1088
+ const households = program2.command("households").description("Household management");
1089
+ households.command("list").description("List households").action(
1090
+ withErrorHandler(async () => {
1091
+ const result = await apiCall("/households");
1092
+ output(result, program2.opts().pretty);
1093
+ })
1094
+ );
1095
+ households.command("summary").description("Get household details with member health data").argument("<id>", "Household ID").option("--days <n>", "Days of nutrition history (1-30)", "1").action(
1096
+ withErrorHandler(async (id, opts) => {
1097
+ const result = await apiCall(
1098
+ `/households/${id}?include_health=true&days=${opts.days}`
1099
+ );
1100
+ output(result, program2.opts().pretty);
1101
+ })
1102
+ );
1103
+ households.command("create").description("Create a household").argument("<name>", "Household name").action(
1104
+ withErrorHandler(async (name) => {
1105
+ const result = await apiCall("/households", "POST", { name });
1106
+ output(result, program2.opts().pretty);
1107
+ })
1108
+ );
1109
+ households.command("update").description("Update a household").argument("<id>", "Household ID").requiredOption("--name <name>", "New name").action(
1110
+ withErrorHandler(async (id, opts) => {
1111
+ const result = await apiCall(`/households/${id}`, "PATCH", {
1112
+ name: opts.name
1113
+ });
1114
+ output(result, program2.opts().pretty);
1115
+ })
1116
+ );
1117
+ households.command("delete").description("Delete a household (owner only)").argument("<id>", "Household ID").action(
1118
+ withErrorHandler(async (id) => {
1119
+ const result = await apiCall(`/households/${id}`, "DELETE");
1120
+ output(result, program2.opts().pretty);
1121
+ })
1122
+ );
1123
+ households.command("leave").description("Leave a household").argument("<id>", "Household ID").action(
1124
+ withErrorHandler(async (id) => {
1125
+ const result = await apiCall(`/households/${id}/leave`, "DELETE");
1126
+ output(result, program2.opts().pretty);
1127
+ })
1128
+ );
1129
+ households.command("members").description("List household members").argument("<id>", "Household ID").action(
1130
+ withErrorHandler(async (id) => {
1131
+ const result = await apiCall(`/households/${id}/members`);
1132
+ output(result, program2.opts().pretty);
1133
+ })
1134
+ );
1135
+ households.command("remove-member").description("Remove a member from household (owner only)").argument("<id>", "Household ID").argument("<member_id>", "Member ID").action(
1136
+ withErrorHandler(async (id, memberId) => {
1137
+ const result = await apiCall(
1138
+ `/households/${id}/members/${memberId}`,
1139
+ "DELETE"
1140
+ );
1141
+ output(result, program2.opts().pretty);
1142
+ })
1143
+ );
1144
+ }
1145
+
1146
+ // src/commands/invites.ts
1147
+ function registerInvitesCommands(program2) {
1148
+ const invites = program2.command("invites").description("Household invitations");
1149
+ invites.command("list").description("List pending invites for a household (owner view)").argument("<household_id>", "Household ID").action(
1150
+ withErrorHandler(async (householdId) => {
1151
+ const result = await apiCall(
1152
+ `/households/${householdId}/invites`
1153
+ );
1154
+ output(result, program2.opts().pretty);
1155
+ })
1156
+ );
1157
+ invites.command("pending").description("List invites sent to you").action(
1158
+ withErrorHandler(async () => {
1159
+ const result = await apiCall("/household_invites");
1160
+ output(result, program2.opts().pretty);
1161
+ })
1162
+ );
1163
+ invites.command("create").description("Invite someone to a household").argument("<household_id>", "Household ID").requiredOption("--email <email>", "Email address to invite").action(
1164
+ withErrorHandler(
1165
+ async (householdId, opts) => {
1166
+ const result = await apiCall(
1167
+ `/households/${householdId}/invites`,
1168
+ "POST",
1169
+ { email_address: opts.email }
1170
+ );
1171
+ output(result, program2.opts().pretty);
1172
+ }
1173
+ )
1174
+ );
1175
+ invites.command("cancel").description("Cancel a pending invite (owner only)").argument("<household_id>", "Household ID").argument("<invite_id>", "Invite ID").action(
1176
+ withErrorHandler(async (householdId, inviteId) => {
1177
+ const result = await apiCall(
1178
+ `/households/${householdId}/invites/${inviteId}`,
1179
+ "DELETE"
1180
+ );
1181
+ output(result, program2.opts().pretty);
1182
+ })
1183
+ );
1184
+ invites.command("accept").description("Accept an invite").argument("<id>", "Invite ID").action(
1185
+ withErrorHandler(async (id) => {
1186
+ const result = await apiCall(
1187
+ `/household_invites/${id}/accept`,
1188
+ "POST"
1189
+ );
1190
+ output(result, program2.opts().pretty);
1191
+ })
1192
+ );
1193
+ invites.command("decline").description("Decline an invite").argument("<id>", "Invite ID").action(
1194
+ withErrorHandler(async (id) => {
1195
+ const result = await apiCall(
1196
+ `/household_invites/${id}/reject`,
1197
+ "POST"
1198
+ );
1199
+ output(result, program2.opts().pretty);
1200
+ })
1201
+ );
1202
+ }
1203
+
1204
+ // src/index.ts
1205
+ var program = new Command();
1206
+ program.name("databody").description("DataBody CLI - Health & Fitness Tracking").version("1.0.0").option("--pretty", "Pretty-print JSON output");
1207
+ registerAuthCommands(program);
1208
+ registerUserCommands(program);
1209
+ registerHealthCommands(program);
1210
+ registerNutritionCommands(program);
1211
+ registerFoodCommands(program);
1212
+ registerGoalsCommands(program);
1213
+ registerWorkoutsCommands(program);
1214
+ registerAiCommands(program);
1215
+ registerThreadsCommands(program);
1216
+ registerNotesCommands(program);
1217
+ registerHouseholdsCommands(program);
1218
+ registerInvitesCommands(program);
1219
+ program.command("summary").description("Today's health summary (shortcut for: health summary)").action(
1220
+ withErrorHandler(async () => {
1221
+ const result = await apiCall("/health/summary");
1222
+ output(result, program.opts().pretty);
1223
+ })
1224
+ );
1225
+ program.command("today").description("Today's nutrition (shortcut for: nutrition today)").action(
1226
+ withErrorHandler(async () => {
1227
+ const result = await apiCall("/nutrition/today");
1228
+ output(result, program.opts().pretty);
1229
+ })
1230
+ );
1231
+ program.command("chat").description("Chat with AI coach (shortcut for: ai chat)").argument("<message>", "Your message").option("--thread <id>", "Thread ID").option("--household <id>", "Household ID").action(
1232
+ withErrorHandler(
1233
+ async (message, opts) => {
1234
+ const body = { message };
1235
+ if (opts.thread) body.thread_id = parseInt(opts.thread);
1236
+ if (opts.household) body.household_id = parseInt(opts.household);
1237
+ const result = await apiCall("/ai/chat", "POST", body);
1238
+ output(result, program.opts().pretty);
1239
+ }
1240
+ )
1241
+ );
1242
+ program.command("log").description("Log food (shortcut for: nutrition log)").argument("<meal>", "Meal type (breakfast/lunch/dinner/snack)").argument("<items>", "JSON array of food items").action(
1243
+ withErrorHandler(async (meal, items) => {
1244
+ const parsed = JSON.parse(items);
1245
+ const result = await apiCall("/nutrition", "POST", {
1246
+ meal_type: meal,
1247
+ nutrition_items_attributes: parsed
1248
+ });
1249
+ output(result, program.opts().pretty);
1250
+ })
1251
+ );
1252
+ program.parse();