@milldr/crono 0.3.0 → 0.3.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 CHANGED
@@ -8,6 +8,12 @@ Cronometer has no public API, so crono automates the web UI through Kernel.sh br
8
8
 
9
9
  ![crono quick-add demo](demo.gif)
10
10
 
11
+ ## Motivation
12
+
13
+ Cronometer is great for logging food — barcode scanning and manual search cover most meals at home. But when you're eating out or don't have a barcode, the workflow gets clunky: take a photo, ask an AI to estimate the macros, then manually punch those numbers into the app as a quick add.
14
+
15
+ crono closes that loop. Give your AI agent a skill that knows how to call crono, and it goes from estimating macros to actually logging them — no manual step in between. On top of that, your agent can query your diary, pull export data, and answer questions about your nutrition without you ever opening the app.
16
+
11
17
  ## Quickstart
12
18
 
13
19
  ### 1. Install
@@ -49,7 +55,7 @@ You'll be prompted for three things:
49
55
  ### 3. Log a meal
50
56
 
51
57
  ```bash
52
- crono quick-add -p 30 -c 100 -f 20 -m Dinner
58
+ crono quick-add -p 30 -c 100 -f 20 -a 14 -m Dinner -d yesterday
53
59
  ```
54
60
 
55
61
  ```
@@ -58,7 +64,7 @@ crono quick-add -p 30 -c 100 -f 20 -m Dinner
58
64
  ◒ Logging into Cronometer...
59
65
  ◇ Done.
60
66
 
61
- └ Added: 30g protein, 100g carbs, 20g fat → Dinner
67
+ └ Added: 30g protein, 100g carbs, 20g fat, 14g alcohol → Dinner on 2026-02-15
62
68
  ```
63
69
 
64
70
  ## Commands
@@ -86,9 +92,11 @@ crono quick-add [options]
86
92
  | `-p` | `--protein <g>` | Grams of protein |
87
93
  | `-c` | `--carbs <g>` | Grams of carbohydrates |
88
94
  | `-f` | `--fat <g>` | Grams of fat |
95
+ | `-a` | `--alcohol <g>` | Grams of alcohol |
89
96
  | `-m` | `--meal <name>` | Meal category (Breakfast, Lunch, Dinner, Snacks) |
97
+ | `-d` | `--date <date>` | Date (YYYY-MM-DD, yesterday, -1d) |
90
98
 
91
- At least one macro flag (`-p`, `-c`, or `-f`) is required.
99
+ At least one macro flag (`-p`, `-c`, `-f`, or `-a`) is required.
92
100
 
93
101
  **Examples:**
94
102
 
@@ -101,6 +109,15 @@ crono quick-add -p 30 -c 100 -f 20
101
109
 
102
110
  # Log to Dinner category
103
111
  crono quick-add -p 30 -c 50 -f 15 --meal Dinner
112
+
113
+ # Log to yesterday
114
+ crono quick-add -p 30 -d yesterday -m Dinner
115
+
116
+ # Log alcohol
117
+ crono quick-add -a 14 -m Dinner
118
+
119
+ # Combine everything
120
+ crono quick-add -p 30 -c 50 -f 10 -a 14 -d -3d -m Dinner
104
121
  ```
105
122
 
106
123
  ### `crono add custom-food`
@@ -12,9 +12,9 @@
12
12
  * navigate to diary → set date via prev/next arrows → read nutrition totals
13
13
  *
14
14
  * Cronometer diary layout (energy summary):
15
- * Each meal category shows a summary like:
16
- * "302 kcal 1 g protein 8 g carbs 0 g fat"
17
- * The daily totals are in the Energy Summary section.
15
+ * The Energy Summary section shows daily totals like:
16
+ * "Energy 1847 kcal", "Protein 168 g", "Carbs 200 g", "Fat 60 g"
17
+ * Per-meal summaries are also present but may include non-food rows.
18
18
  *
19
19
  * Returns { success: true, entries: [{ date, calories, protein, carbs, fat }] }
20
20
  */
@@ -1 +1 @@
1
- {"version":3,"file":"diary.d.ts","sourceRoot":"","sources":["../../src/kernel/diary.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAuItD"}
1
+ {"version":3,"file":"diary.d.ts","sourceRoot":"","sources":["../../src/kernel/diary.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAyItD"}
@@ -12,9 +12,9 @@
12
12
  * navigate to diary → set date via prev/next arrows → read nutrition totals
13
13
  *
14
14
  * Cronometer diary layout (energy summary):
15
- * Each meal category shows a summary like:
16
- * "302 kcal 1 g protein 8 g carbs 0 g fat"
17
- * The daily totals are in the Energy Summary section.
15
+ * The Energy Summary section shows daily totals like:
16
+ * "Energy 1847 kcal", "Protein 168 g", "Carbs 200 g", "Fat 60 g"
17
+ * Per-meal summaries are also present but may include non-food rows.
18
18
  *
19
19
  * Returns { success: true, entries: [{ date, calories, protein, carbs, fat }] }
20
20
  */
@@ -63,26 +63,44 @@ export function buildDiaryCode(dates) {
63
63
  }
64
64
 
65
65
  // Helper: extract nutrition totals from the currently displayed diary page.
66
- // Cronometer shows per-meal summary lines like:
67
- // "302 kcal 1 g protein 8 g carbs • 0 g fat"
68
- // and an Energy Summary section with daily totals.
66
+ // Cronometer shows an Energy Summary section with daily totals
67
+ // ("Energy X kcal", "Protein X g", etc.) and per-meal summary lines
68
+ // ("302 kcal 1 g protein 8 g carbs • 0 g fat").
69
+ //
70
+ // We prefer the Energy Summary totals (Strategy 1) because the
71
+ // per-meal summary regex can match non-food rows (exercise, targets)
72
+ // and inflate the calorie count. See: https://github.com/milldr/crono/issues/20
69
73
  async function extractNutrition() {
70
74
  return await page.evaluate(() => {
71
75
  const bodyText = document.body.innerText;
72
76
 
73
- // Strategy 1: Look for the Energy Summary totals.
74
- // Cronometer shows "Energy X kcal" or "X kcal" in the totals row.
75
- // Also look for individual macros in the totals section.
77
+ // Strategy 1: Look for individual nutrient totals in the Energy Summary.
78
+ // Cronometer displays "Energy 1847 kcal", "Protein 168 g", etc.
79
+ // .match() returns the first occurrence the daily total.
80
+ const calMatch = bodyText.match(/Energy\\s+(\\d+\\.?\\d*)\\s*kcal/i);
81
+ const protMatch = bodyText.match(/Protein\\s+(\\d+\\.?\\d*)\\s*g/i);
82
+ const carbMatch = bodyText.match(/Carbs\\s+(\\d+\\.?\\d*)\\s*g/i);
83
+ const fatMatch = bodyText.match(/Fat\\s+(\\d+\\.?\\d*)\\s*g/i);
84
+
85
+ if (calMatch || protMatch || carbMatch || fatMatch) {
86
+ return {
87
+ calories: calMatch ? Math.round(parseFloat(calMatch[1])) : 0,
88
+ protein: protMatch ? Math.round(parseFloat(protMatch[1])) : 0,
89
+ carbs: carbMatch ? Math.round(parseFloat(carbMatch[1])) : 0,
90
+ fat: fatMatch ? Math.round(parseFloat(fatMatch[1])) : 0,
91
+ };
92
+ }
93
+
94
+ // Strategy 2 (fallback): Sum per-meal summary lines.
95
+ // Each line matches: "N kcal • N g protein • N g carbs • N g fat"
96
+ const mealPattern = /(\\d+\\.?\\d*)\\s*kcal\\s*[•·]\\s*(\\d+\\.?\\d*)\\s*g\\s*protein\\s*[•·]\\s*(\\d+\\.?\\d*)\\s*g\\s*carbs\\s*[•·]\\s*(\\d+\\.?\\d*)\\s*g\\s*fat/gi;
76
97
  let calories = 0;
77
98
  let protein = 0;
78
99
  let carbs = 0;
79
100
  let fat = 0;
80
101
  let found = false;
81
-
82
- // Strategy 2: Sum all meal category summary lines.
83
- // Each line matches: "N kcal • N g protein • N g carbs • N g fat"
84
- const mealPattern = /(\\d+\\.?\\d*)\\s*kcal\\s*[•·]\\s*(\\d+\\.?\\d*)\\s*g\\s*protein\\s*[•·]\\s*(\\d+\\.?\\d*)\\s*g\\s*carbs\\s*[•·]\\s*(\\d+\\.?\\d*)\\s*g\\s*fat/gi;
85
102
  let match;
103
+
86
104
  while ((match = mealPattern.exec(bodyText)) !== null) {
87
105
  calories += parseFloat(match[1]);
88
106
  protein += parseFloat(match[2]);
@@ -100,22 +118,6 @@ export function buildDiaryCode(dates) {
100
118
  };
101
119
  }
102
120
 
103
- // Strategy 3: Look for individual nutrient totals in the page.
104
- // Cronometer may display "Energy 1847 kcal" and "Protein 168 g" etc.
105
- const calMatch = bodyText.match(/Energy\\s+(\\d+\\.?\\d*)\\s*kcal/i);
106
- const protMatch = bodyText.match(/Protein\\s+(\\d+\\.?\\d*)\\s*g/i);
107
- const carbMatch = bodyText.match(/Carbs\\s+(\\d+\\.?\\d*)\\s*g/i);
108
- const fatMatch = bodyText.match(/Fat\\s+(\\d+\\.?\\d*)\\s*g/i);
109
-
110
- if (calMatch || protMatch || carbMatch || fatMatch) {
111
- return {
112
- calories: calMatch ? Math.round(parseFloat(calMatch[1])) : 0,
113
- protein: protMatch ? Math.round(parseFloat(protMatch[1])) : 0,
114
- carbs: carbMatch ? Math.round(parseFloat(carbMatch[1])) : 0,
115
- fat: fatMatch ? Math.round(parseFloat(fatMatch[1])) : 0,
116
- };
117
- }
118
-
119
121
  // No nutrition data found — empty diary day
120
122
  return { calories: 0, protein: 0, carbs: 0, fat: 0 };
121
123
  });
@@ -1 +1 @@
1
- {"version":3,"file":"diary.js","sourceRoot":"","sources":["../../src/kernel/diary.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,KAAe;IAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAExC,OAAO;oBACW,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkI1B,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"diary.js","sourceRoot":"","sources":["../../src/kernel/diary.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,KAAe;IAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAExC,OAAO;oBACW,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoI1B,CAAC;AACJ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"quick-add.d.ts","sourceRoot":"","sources":["../../src/kernel/quick-add.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKrD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAwQ3D"}
1
+ {"version":3,"file":"quick-add.d.ts","sourceRoot":"","sources":["../../src/kernel/quick-add.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKrD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAmR3D"}
@@ -93,7 +93,7 @@ export function buildQuickAddCode(entry) {
93
93
  try {
94
94
  const el = page.locator(sel);
95
95
  if (await el.count() > 0) {
96
- await el.first().click();
96
+ await el.first().click({ timeout: 5000 });
97
97
  return true;
98
98
  }
99
99
  } catch {}
@@ -107,7 +107,7 @@ export function buildQuickAddCode(entry) {
107
107
  try {
108
108
  const el = page.locator(sel);
109
109
  if (await el.count() > 0) {
110
- await el.first().click({ button: 'right' });
110
+ await el.first().click({ button: 'right', timeout: 5000 });
111
111
  return true;
112
112
  }
113
113
  } catch {}
@@ -125,9 +125,12 @@ export function buildQuickAddCode(entry) {
125
125
  if (!clicked) {
126
126
  return { success: false, error: 'Could not find meal category "' + mealLabel + '" in diary' };
127
127
  }
128
- await page.waitForSelector('text="Add Food..."', { timeout: 3000 }).catch(() =>
129
- page.waitForSelector('text="Add Food"', { timeout: 2000 }).catch(() => {})
130
- );
128
+ const menuVisible = await page.waitForSelector('text="Add Food..."', { timeout: 3000 })
129
+ .then(() => true)
130
+ .catch(() => page.waitForSelector('text="Add Food"', { timeout: 2000 }).then(() => true).catch(() => false));
131
+ if (!menuVisible) {
132
+ return { success: false, error: 'Context menu did not appear after right-clicking "' + mealLabel + '"' };
133
+ }
131
134
 
132
135
  // Click "Add Food..." in context menu
133
136
  const addFoodClicked = await clickFirst([
@@ -185,7 +188,11 @@ export function buildQuickAddCode(entry) {
185
188
  'button:has-text("SEARCH")',
186
189
  'button:has-text("Search")',
187
190
  ]);
188
- await page.waitForSelector('td:has-text("' + macro.searchName + '")', { timeout: 8000 }).catch(() => {});
191
+ const resultsAppeared = await page.waitForSelector('td:has-text("' + macro.searchName + '")', { timeout: 8000 })
192
+ .then(() => true).catch(() => false);
193
+ if (!resultsAppeared) {
194
+ return { success: false, error: 'Search results did not appear for "' + macro.searchName + '"' };
195
+ }
189
196
 
190
197
  // Select the search result row (not the search input)
191
198
  // Target table rows/cells containing the macro name
@@ -276,8 +283,12 @@ export function buildQuickAddCode(entry) {
276
283
  if (!addClicked) {
277
284
  return { success: false, error: 'Could not find "Add to Diary" button for "' + macro.name + '"' };
278
285
  }
279
- await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 }).catch(() => {});
280
- await page.waitForTimeout(300);
286
+ const dialogDismissed = await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 })
287
+ .then(() => true).catch(() => false);
288
+ if (!dialogDismissed) {
289
+ return { success: false, error: '"Add Food to Diary" dialog did not close after adding "' + macro.name + '"' };
290
+ }
291
+ await page.waitForTimeout(500);
281
292
  }
282
293
 
283
294
  return { success: true };
@@ -1 +1 @@
1
- {"version":3,"file":"quick-add.js","sourceRoot":"","sources":["../../src/kernel/quick-add.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA2B;IACxD,OAAO,EAAE,oBAAoB;IAC7B,KAAK,EAAE,yBAAyB;IAChC,GAAG,EAAE,gBAAgB;IACrB,OAAO,EAAE,oBAAoB;CAC9B,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAiB;IACjD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;IAE3D,MAAM,SAAS,GAAG,IAAI;QACpB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;QAC5D,CAAC,CAAC,eAAe,CAAC;IAEpB,8BAA8B;IAC9B,MAAM,MAAM,GAA0D,EAAE,CAAC;IACzE,IAAI,OAAO,KAAK,SAAS;QACvB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,SAAS;YACf,UAAU,EAAE,kBAAkB,CAAC,OAAO;YACtC,KAAK,EAAE,OAAO;SACf,CAAC,CAAC;IACL,IAAI,KAAK,KAAK,SAAS;QACrB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,OAAO;YACb,UAAU,EAAE,kBAAkB,CAAC,KAAK;YACpC,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;IACL,IAAI,GAAG,KAAK,SAAS;QACnB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,KAAK;YACX,UAAU,EAAE,kBAAkB,CAAC,GAAG;YAClC,KAAK,EAAE,GAAG;SACX,CAAC,CAAC;IACL,IAAI,OAAO,KAAK,SAAS;QACvB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,SAAS;YACf,UAAU,EAAE,kBAAkB,CAAC,OAAO;YACtC,KAAK,EAAE,OAAO;SACf,CAAC,CAAC;IAEL,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAE1C,OAAO;qBACY,UAAU;wBACP,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;yBACxB,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgOlD,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"quick-add.js","sourceRoot":"","sources":["../../src/kernel/quick-add.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA2B;IACxD,OAAO,EAAE,oBAAoB;IAC7B,KAAK,EAAE,yBAAyB;IAChC,GAAG,EAAE,gBAAgB;IACrB,OAAO,EAAE,oBAAoB;CAC9B,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAiB;IACjD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;IAE3D,MAAM,SAAS,GAAG,IAAI;QACpB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;QAC5D,CAAC,CAAC,eAAe,CAAC;IAEpB,8BAA8B;IAC9B,MAAM,MAAM,GAA0D,EAAE,CAAC;IACzE,IAAI,OAAO,KAAK,SAAS;QACvB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,SAAS;YACf,UAAU,EAAE,kBAAkB,CAAC,OAAO;YACtC,KAAK,EAAE,OAAO;SACf,CAAC,CAAC;IACL,IAAI,KAAK,KAAK,SAAS;QACrB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,OAAO;YACb,UAAU,EAAE,kBAAkB,CAAC,KAAK;YACpC,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;IACL,IAAI,GAAG,KAAK,SAAS;QACnB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,KAAK;YACX,UAAU,EAAE,kBAAkB,CAAC,GAAG;YAClC,KAAK,EAAE,GAAG;SACX,CAAC,CAAC;IACL,IAAI,OAAO,KAAK,SAAS;QACvB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,SAAS;YACf,UAAU,EAAE,kBAAkB,CAAC,OAAO;YACtC,KAAK,EAAE,OAAO;SACf,CAAC,CAAC;IAEL,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAE1C,OAAO;qBACY,UAAU;wBACP,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;yBACxB,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2OlD,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milldr/crono",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "CLI for Cronometer automation via Kernel.sh",
5
5
  "type": "module",
6
6
  "bin": {