Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,50 @@ crono diary -r 7d --json
# → [{"date":"2026-02-11","calories":1847,"protein":168,"carbs":142,"fat":58}, ...]
```

### `crono biometrics`

Log biometrics to Cronometer (weight, body fat percentage, blood pressure). Defaults to today if no date is specified.

```bash
crono biometrics [options]
```

**Options:**

| Flag | Long | Description |
| ---- | --------------------- | ------------------------------------ |
| `-w` | `--weight <value>` | Weight value |
| `-b` | `--body-fat <value>` | Body fat percentage |
| `-s` | `--systolic <value>` | Systolic blood pressure (mmHg) |
| `-d` | `--diastolic <value>` | Diastolic blood pressure (mmHg) |
| | `--date <date>` | Date (YYYY-MM-DD, yesterday, -1d) |
| `-u` | `--unit <unit>` | Weight unit (kg or lbs), default: kg |

At least one biometric value is required. Blood pressure requires both systolic and diastolic values.

**Examples:**

```bash
# Log weight in kg (default)
crono biometrics --weight 90

# Log weight in lbs
crono biometrics --weight 198 --unit lbs

# Log body fat percentage
crono biometrics --body-fat 19

# Log blood pressure
crono biometrics --systolic 130 --diastolic 76

# Log weight for yesterday
crono biometrics --weight 90 --date yesterday

# Log multiple biometrics (requires multiple commands)
crono biometrics --weight 90
crono biometrics --body-fat 19
```

### `crono recipes`

List your custom recipes from Cronometer.
Expand Down
21 changes: 21 additions & 0 deletions src/automation/runner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as p from "@clack/prompts";
import { getCredential } from "../credentials.js";
import { buildAddCustomFoodCode } from "../kernel/add-custom-food.js";
import { buildBiometricsCode } from "../kernel/biometrics.js";
import { buildDiaryCode } from "../kernel/diary.js";
import { buildLogFoodCode } from "../kernel/log-food.js";
import { buildRecipesCode } from "../kernel/recipes.js";
Expand All @@ -15,6 +16,7 @@ import type {
AutomationClient,
AutomationRuntime,
AutomationRuntimeFactory,
BiometricEntry,
CustomFoodEntry,
DiaryData,
LogFoodEntry,
Expand Down Expand Up @@ -114,6 +116,25 @@ export function createAutomationClient(
}
return data.recipes ?? [];
}),
logBiometric: (entry: BiometricEntry, onStatus?: (msg: string) => void) =>
withLoggedInRuntime(createRuntime, onStatus, async (runtime) => {
const typeLabel =
entry.type === "weight"
? "weight"
: entry.type === "bodyfat"
? "body fat"
: "blood pressure";
onStatus?.(`Logging ${typeLabel}...`);
const data = await executeAutomation<{
success: boolean;
error?: string;
}>(runtime, buildBiometricsCode(entry), 60);
if (!data.success) {
throw new Error(
`Biometric logging failed: ${data.error ?? "Unknown error"}`
);
}
}),
};
}

Expand Down
13 changes: 13 additions & 0 deletions src/automation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ export interface RecipeData {
name: string;
}

export interface BiometricEntry {
type: "weight" | "bodyfat" | "bloodpressure";
value?: number;
unit?: string;
systolic?: number;
diastolic?: number;
date: string;
}

export interface AutomationClient {
addQuickEntry(
entry: MacroEntry,
Expand All @@ -69,6 +78,10 @@ export interface AutomationClient {
): Promise<void>;
logFood(entry: LogFoodEntry, onStatus?: (msg: string) => void): Promise<void>;
getRecipes(onStatus?: (msg: string) => void): Promise<RecipeData[]>;
logBiometric(
entry: BiometricEntry,
onStatus?: (msg: string) => void
): Promise<void>;
}

export interface PlaywrightExecutionResponse<T = unknown> {
Expand Down
117 changes: 117 additions & 0 deletions src/commands/biometrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as p from "@clack/prompts";
import { getAutomationClient } from "../automation/client.js";
import { formatKernelError } from "../kernel/errors.js";
import { parseDate, todayStr } from "../utils/date.js";

export interface BiometricsOptions {
weight?: number;
bodyFat?: number;
systolic?: number;
diastolic?: number;
date?: string;
unit?: string;
}

export async function biometrics(options: BiometricsOptions): Promise<void> {
// Validate at least one biometric is provided
if (
options.weight === undefined &&
options.bodyFat === undefined &&
(options.systolic === undefined || options.diastolic === undefined)
) {
p.log.error(
"At least one biometric required: --weight, --body-fat, or both --systolic and --diastolic"
);
process.exit(1);
}

// Validate blood pressure requires both values
if (
(options.systolic !== undefined && options.diastolic === undefined) ||
(options.systolic === undefined && options.diastolic !== undefined)
) {
p.log.error("Blood pressure requires both --systolic and --diastolic");
process.exit(1);
}

// Validate weight unit
const unit = options.unit?.toLowerCase() || "kg";
if (unit !== "kg" && unit !== "lbs") {
p.log.error('Weight unit must be "kg" or "lbs"');
process.exit(1);
}

// Resolve date
let targetDate: string;
try {
targetDate = options.date ? parseDate(options.date) : todayStr();
} catch (error) {
p.log.error(String(error instanceof Error ? error.message : error));
process.exit(1);
}

p.intro("🍎 crono biometrics");

const s = p.spinner();
s.start("Connecting...");

try {
const client = await getAutomationClient();

// Log weight if provided
if (options.weight !== undefined) {
await client.logBiometric(
{
type: "weight",
value: options.weight,
unit,
date: targetDate,
},
(msg) => s.message(msg)
);
}

// Log body fat if provided
if (options.bodyFat !== undefined) {
await client.logBiometric(
{
type: "bodyfat",
value: options.bodyFat,
date: targetDate,
},
(msg) => s.message(msg)
);
}

// Log blood pressure if provided
if (options.systolic !== undefined && options.diastolic !== undefined) {
await client.logBiometric(
{
type: "bloodpressure",
systolic: options.systolic,
diastolic: options.diastolic,
date: targetDate,
},
(msg) => s.message(msg)
);
}

s.stop("Done.");

const logged: string[] = [];
if (options.weight !== undefined)
logged.push(`Weight: ${options.weight} ${unit}`);
if (options.bodyFat !== undefined)
logged.push(`Body Fat: ${options.bodyFat}%`);
if (options.systolic !== undefined && options.diastolic !== undefined)
logged.push(
`Blood Pressure: ${options.systolic}/${options.diastolic} mmHg`
);

p.outro(`Logged biometrics for ${targetDate}:\n ${logged.join("\n ")}`);
} catch (error) {
s.stop("Failed.");
p.log.error(`Failed to log biometrics: ${formatKernelError(error)}`);
process.exit(1);
}
}
24 changes: 24 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { diary } from "./commands/diary.js";
import { weight } from "./commands/weight.js";
import { exportCmd } from "./commands/export.js";
import { recipes } from "./commands/recipes.js";
import { biometrics } from "./commands/biometrics.js";

const require = createRequire(import.meta.url);
const { version } = require("../package.json");
Expand Down Expand Up @@ -120,4 +121,27 @@ program
await recipes(options);
});

program
.command("biometrics")
.description(
"Log biometrics to Cronometer (weight, body fat, blood pressure)"
)
.option("-w, --weight <value>", "Weight value", parseFloat)
.option("-b, --body-fat <value>", "Body fat percentage", parseFloat)
.option(
"-s, --systolic <value>",
"Systolic blood pressure (mmHg)",
parseFloat
)
.option(
"-d, --diastolic <value>",
"Diastolic blood pressure (mmHg)",
parseFloat
)
.option("--date <date>", "Date (YYYY-MM-DD, yesterday, -1d)")
.option("-u, --unit <unit>", "Weight unit (kg or lbs)", "kg")
.action(async (options) => {
await biometrics(options);
});

program.parse();
Loading
Loading