@milldr/crono 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/commands/diary.d.ts +7 -0
- package/dist/commands/diary.d.ts.map +1 -0
- package/dist/commands/diary.js +79 -0
- package/dist/commands/diary.js.map +1 -0
- package/dist/commands/login.d.ts +19 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +160 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/quick-add.d.ts +8 -0
- package/dist/commands/quick-add.d.ts.map +1 -0
- package/dist/commands/quick-add.js +49 -0
- package/dist/commands/quick-add.js.map +1 -0
- package/dist/commands/weight.d.ts +7 -0
- package/dist/commands/weight.d.ts.map +1 -0
- package/dist/commands/weight.js +78 -0
- package/dist/commands/weight.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +43 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +27 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +178 -0
- package/dist/credentials.js.map +1 -0
- package/dist/debug-nav.d.ts +2 -0
- package/dist/debug-nav.d.ts.map +1 -0
- package/dist/debug-nav.js +99 -0
- package/dist/debug-nav.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/client.d.ts +40 -0
- package/dist/kernel/client.d.ts.map +1 -0
- package/dist/kernel/client.js +205 -0
- package/dist/kernel/client.js.map +1 -0
- package/dist/kernel/diary.d.ts +22 -0
- package/dist/kernel/diary.d.ts.map +1 -0
- package/dist/kernel/diary.js +156 -0
- package/dist/kernel/diary.js.map +1 -0
- package/dist/kernel/login.d.ts +24 -0
- package/dist/kernel/login.d.ts.map +1 -0
- package/dist/kernel/login.js +125 -0
- package/dist/kernel/login.js.map +1 -0
- package/dist/kernel/quick-add.d.ts +22 -0
- package/dist/kernel/quick-add.d.ts.map +1 -0
- package/dist/kernel/quick-add.js +260 -0
- package/dist/kernel/quick-add.js.map +1 -0
- package/dist/kernel/weight.d.ts +20 -0
- package/dist/kernel/weight.d.ts.map +1 -0
- package/dist/kernel/weight.js +160 -0
- package/dist/kernel/weight.js.map +1 -0
- package/dist/utils/date.d.ts +31 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +82 -0
- package/dist/utils/date.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel.sh browser automation client for Cronometer.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the SDK to manage browser sessions, handle login,
|
|
5
|
+
* and execute Playwright automation for diary entries.
|
|
6
|
+
*
|
|
7
|
+
* Each operation creates a fresh browser, logs in, performs the
|
|
8
|
+
* action, and tears down.
|
|
9
|
+
*/
|
|
10
|
+
import * as p from "@clack/prompts";
|
|
11
|
+
import Kernel from "@onkernel/sdk";
|
|
12
|
+
import { getCredential } from "../credentials.js";
|
|
13
|
+
import { buildAutoLoginCode, buildLoginCheckCode, buildNavigateToLoginCode, } from "./login.js";
|
|
14
|
+
import { buildQuickAddCode } from "./quick-add.js";
|
|
15
|
+
import { buildDiaryCode } from "./diary.js";
|
|
16
|
+
import { buildWeightCode } from "./weight.js";
|
|
17
|
+
/**
|
|
18
|
+
* Create a Kernel client for Cronometer automation.
|
|
19
|
+
*
|
|
20
|
+
* Resolves the API key from env var or credential store.
|
|
21
|
+
* Each operation creates a fresh browser and logs in.
|
|
22
|
+
*/
|
|
23
|
+
export async function getKernelClient() {
|
|
24
|
+
const apiKey = process.env["KERNEL_API_KEY"] ?? getCredential("kernel-api-key");
|
|
25
|
+
if (!apiKey) {
|
|
26
|
+
throw new Error("Kernel API key not found.\n" +
|
|
27
|
+
"Run `crono login` to configure credentials, or set:\n" +
|
|
28
|
+
" export KERNEL_API_KEY=your-key-here");
|
|
29
|
+
}
|
|
30
|
+
process.env["KERNEL_API_KEY"] = apiKey;
|
|
31
|
+
const kernel = new Kernel();
|
|
32
|
+
return {
|
|
33
|
+
addQuickEntry: (entry, onStatus) => addQuickEntry(kernel, entry, onStatus),
|
|
34
|
+
getWeight: (dates, onStatus) => getWeight(kernel, dates, onStatus),
|
|
35
|
+
getDiary: (dates, onStatus) => getDiary(kernel, dates, onStatus),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Execute the quick-add automation on Cronometer.
|
|
40
|
+
* Creates a browser, logs in, performs the quick-add, then tears down.
|
|
41
|
+
*/
|
|
42
|
+
async function addQuickEntry(kernel, entry, onStatus) {
|
|
43
|
+
const username = getCredential("cronometer-username");
|
|
44
|
+
const password = getCredential("cronometer-password");
|
|
45
|
+
const hasAutoCreds = !!(username && password);
|
|
46
|
+
const browser = await kernel.browsers.create({
|
|
47
|
+
headless: hasAutoCreds,
|
|
48
|
+
stealth: true,
|
|
49
|
+
timeout_seconds: hasAutoCreds ? 120 : 300,
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
// Log in to Cronometer
|
|
53
|
+
if (hasAutoCreds) {
|
|
54
|
+
await autoLogin(kernel, browser.session_id, username, password, onStatus);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
await manualLogin(kernel, browser);
|
|
58
|
+
}
|
|
59
|
+
// Execute quick-add
|
|
60
|
+
onStatus?.("Adding quick entry...");
|
|
61
|
+
const result = await kernel.browsers.playwright.execute(browser.session_id, { code: buildQuickAddCode(entry), timeout_sec: 60 });
|
|
62
|
+
if (!result.success) {
|
|
63
|
+
throw new Error(`Automation failed: ${result.error ?? "Unknown error"}`);
|
|
64
|
+
}
|
|
65
|
+
const data = result.result;
|
|
66
|
+
if (!data.success) {
|
|
67
|
+
throw new Error(`Quick add failed: ${data.error ?? "Unknown error"}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
try {
|
|
72
|
+
await kernel.browsers.deleteByID(browser.session_id);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Browser may already be cleaned up by Kernel
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Execute the weight scraping automation on Cronometer.
|
|
81
|
+
* Creates a browser, logs in, reads weight data, then tears down.
|
|
82
|
+
*/
|
|
83
|
+
async function getWeight(kernel, dates, onStatus) {
|
|
84
|
+
const username = getCredential("cronometer-username");
|
|
85
|
+
const password = getCredential("cronometer-password");
|
|
86
|
+
const hasAutoCreds = !!(username && password);
|
|
87
|
+
const browser = await kernel.browsers.create({
|
|
88
|
+
headless: hasAutoCreds,
|
|
89
|
+
stealth: true,
|
|
90
|
+
timeout_seconds: hasAutoCreds ? 120 : 300,
|
|
91
|
+
});
|
|
92
|
+
try {
|
|
93
|
+
if (hasAutoCreds) {
|
|
94
|
+
await autoLogin(kernel, browser.session_id, username, password, onStatus);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
await manualLogin(kernel, browser);
|
|
98
|
+
}
|
|
99
|
+
onStatus?.("Reading weight data...");
|
|
100
|
+
const result = await kernel.browsers.playwright.execute(browser.session_id, { code: buildWeightCode(dates), timeout_sec: 60 });
|
|
101
|
+
if (!result.success) {
|
|
102
|
+
throw new Error(`Automation failed: ${result.error ?? "Unknown error"}`);
|
|
103
|
+
}
|
|
104
|
+
const data = result.result;
|
|
105
|
+
if (!data.success) {
|
|
106
|
+
throw new Error(`Weight read failed: ${data.error ?? "Unknown error"}`);
|
|
107
|
+
}
|
|
108
|
+
return data.entries ?? [];
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
try {
|
|
112
|
+
await kernel.browsers.deleteByID(browser.session_id);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Browser may already be cleaned up by Kernel
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Execute the diary nutrition scraping automation on Cronometer.
|
|
121
|
+
* Creates a browser, logs in, reads nutrition data, then tears down.
|
|
122
|
+
*/
|
|
123
|
+
async function getDiary(kernel, dates, onStatus) {
|
|
124
|
+
const username = getCredential("cronometer-username");
|
|
125
|
+
const password = getCredential("cronometer-password");
|
|
126
|
+
const hasAutoCreds = !!(username && password);
|
|
127
|
+
const browser = await kernel.browsers.create({
|
|
128
|
+
headless: hasAutoCreds,
|
|
129
|
+
stealth: true,
|
|
130
|
+
timeout_seconds: hasAutoCreds ? 120 : 300,
|
|
131
|
+
});
|
|
132
|
+
try {
|
|
133
|
+
if (hasAutoCreds) {
|
|
134
|
+
await autoLogin(kernel, browser.session_id, username, password, onStatus);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
await manualLogin(kernel, browser);
|
|
138
|
+
}
|
|
139
|
+
onStatus?.("Reading diary data...");
|
|
140
|
+
const result = await kernel.browsers.playwright.execute(browser.session_id, { code: buildDiaryCode(dates), timeout_sec: 60 });
|
|
141
|
+
if (!result.success) {
|
|
142
|
+
throw new Error(`Automation failed: ${result.error ?? "Unknown error"}`);
|
|
143
|
+
}
|
|
144
|
+
const data = result.result;
|
|
145
|
+
if (!data.success) {
|
|
146
|
+
throw new Error(`Diary read failed: ${data.error ?? "Unknown error"}`);
|
|
147
|
+
}
|
|
148
|
+
return data.entries ?? [];
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
try {
|
|
152
|
+
await kernel.browsers.deleteByID(browser.session_id);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Browser may already be cleaned up by Kernel
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Auto-login using stored Cronometer credentials.
|
|
161
|
+
*/
|
|
162
|
+
async function autoLogin(kernel, sessionId, username, password, onStatus) {
|
|
163
|
+
onStatus?.("Logging into Cronometer...");
|
|
164
|
+
const result = await kernel.browsers.playwright.execute(sessionId, {
|
|
165
|
+
code: buildAutoLoginCode(username, password),
|
|
166
|
+
timeout_sec: 60,
|
|
167
|
+
});
|
|
168
|
+
const data = result.result;
|
|
169
|
+
if (!result.success || !data.loggedIn) {
|
|
170
|
+
throw new Error(`Auto-login failed: ${data.error ?? "Login verification failed"}.\n` +
|
|
171
|
+
"Your credentials may be incorrect. Run `crono login` to update them.");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Manual login: show a live browser view for the user to log in interactively.
|
|
176
|
+
*/
|
|
177
|
+
async function manualLogin(kernel, browser) {
|
|
178
|
+
p.log.warn("No stored Cronometer credentials found.");
|
|
179
|
+
p.log.info("Tip: run `crono login` to save credentials for automatic login.");
|
|
180
|
+
if (browser.browser_live_view_url) {
|
|
181
|
+
p.note(browser.browser_live_view_url, "Browser Live View");
|
|
182
|
+
}
|
|
183
|
+
await kernel.browsers.playwright.execute(browser.session_id, {
|
|
184
|
+
code: buildNavigateToLoginCode(),
|
|
185
|
+
timeout_sec: 30,
|
|
186
|
+
});
|
|
187
|
+
p.log.info("Please log into Cronometer in the browser above.");
|
|
188
|
+
const confirmation = await p.text({
|
|
189
|
+
message: "Press Enter once you've logged in...",
|
|
190
|
+
defaultValue: "",
|
|
191
|
+
});
|
|
192
|
+
if (p.isCancel(confirmation)) {
|
|
193
|
+
throw new Error("Login cancelled by user.");
|
|
194
|
+
}
|
|
195
|
+
const result = await kernel.browsers.playwright.execute(browser.session_id, {
|
|
196
|
+
code: buildLoginCheckCode(),
|
|
197
|
+
timeout_sec: 30,
|
|
198
|
+
});
|
|
199
|
+
const data = result.result;
|
|
200
|
+
if (!result.success || !data.loggedIn) {
|
|
201
|
+
throw new Error("Login verification failed. Make sure you're fully logged in and try again.");
|
|
202
|
+
}
|
|
203
|
+
p.log.step("Logged in.");
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/kernel/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright code generator for Cronometer diary nutrition scraping.
|
|
3
|
+
*
|
|
4
|
+
* Returns a code string that executes remotely via
|
|
5
|
+
* kernel.browsers.playwright.execute(). The code has access to
|
|
6
|
+
* `page`, `context`, and `browser` from the Playwright environment.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Generate Playwright code for reading nutrition data from Cronometer.
|
|
10
|
+
*
|
|
11
|
+
* For each date, the flow is:
|
|
12
|
+
* navigate to diary → set date via prev/next arrows → read nutrition totals
|
|
13
|
+
*
|
|
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.
|
|
18
|
+
*
|
|
19
|
+
* Returns { success: true, entries: [{ date, calories, protein, carbs, fat }] }
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildDiaryCode(dates: string[]): string;
|
|
22
|
+
//# sourceMappingURL=diary.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright code generator for Cronometer diary nutrition scraping.
|
|
3
|
+
*
|
|
4
|
+
* Returns a code string that executes remotely via
|
|
5
|
+
* kernel.browsers.playwright.execute(). The code has access to
|
|
6
|
+
* `page`, `context`, and `browser` from the Playwright environment.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Generate Playwright code for reading nutrition data from Cronometer.
|
|
10
|
+
*
|
|
11
|
+
* For each date, the flow is:
|
|
12
|
+
* navigate to diary → set date via prev/next arrows → read nutrition totals
|
|
13
|
+
*
|
|
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.
|
|
18
|
+
*
|
|
19
|
+
* Returns { success: true, entries: [{ date, calories, protein, carbs, fat }] }
|
|
20
|
+
*/
|
|
21
|
+
export function buildDiaryCode(dates) {
|
|
22
|
+
const datesJson = JSON.stringify(dates);
|
|
23
|
+
return `
|
|
24
|
+
const dates = ${datesJson};
|
|
25
|
+
const entries = [];
|
|
26
|
+
|
|
27
|
+
// Navigate to diary — we're already logged in from the same session
|
|
28
|
+
await page.goto('https://cronometer.com/#diary', { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
29
|
+
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
30
|
+
|
|
31
|
+
// Verify we're logged in
|
|
32
|
+
const url = page.url();
|
|
33
|
+
if (url.includes('/login') || url.includes('/signin')) {
|
|
34
|
+
return { success: false, error: 'Not logged in. Login may have failed.' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Wait for the diary to fully render
|
|
38
|
+
await page.waitForTimeout(2000);
|
|
39
|
+
|
|
40
|
+
// Helper: click the previous-day arrow.
|
|
41
|
+
// Cronometer diary uses <i> icon-font elements for date nav:
|
|
42
|
+
// <i class="icon-chevron-left diary-date-previous">
|
|
43
|
+
// <i class="icon-chevron-right diary-date-next">
|
|
44
|
+
async function clickPrevDay() {
|
|
45
|
+
const prev = page.locator('i.diary-date-previous').filter({ visible: true });
|
|
46
|
+
if (await prev.count() > 0) {
|
|
47
|
+
await prev.first().click();
|
|
48
|
+
await page.waitForTimeout(2000);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Helper: click the next-day arrow
|
|
55
|
+
async function clickNextDay() {
|
|
56
|
+
const next = page.locator('i.diary-date-next').filter({ visible: true });
|
|
57
|
+
if (await next.count() > 0) {
|
|
58
|
+
await next.first().click();
|
|
59
|
+
await page.waitForTimeout(2000);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
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.
|
|
69
|
+
async function extractNutrition() {
|
|
70
|
+
return await page.evaluate(() => {
|
|
71
|
+
const bodyText = document.body.innerText;
|
|
72
|
+
|
|
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.
|
|
76
|
+
let calories = 0;
|
|
77
|
+
let protein = 0;
|
|
78
|
+
let carbs = 0;
|
|
79
|
+
let fat = 0;
|
|
80
|
+
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
|
+
let match;
|
|
86
|
+
while ((match = mealPattern.exec(bodyText)) !== null) {
|
|
87
|
+
calories += parseFloat(match[1]);
|
|
88
|
+
protein += parseFloat(match[2]);
|
|
89
|
+
carbs += parseFloat(match[3]);
|
|
90
|
+
fat += parseFloat(match[4]);
|
|
91
|
+
found = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (found) {
|
|
95
|
+
return {
|
|
96
|
+
calories: Math.round(calories),
|
|
97
|
+
protein: Math.round(protein),
|
|
98
|
+
carbs: Math.round(carbs),
|
|
99
|
+
fat: Math.round(fat),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
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
|
+
// No nutrition data found — empty diary day
|
|
120
|
+
return { calories: 0, protein: 0, carbs: 0, fat: 0 };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Diary opens to today. Dates are in descending order (most recent first).
|
|
125
|
+
// Calculate how many days back from today to the first requested date,
|
|
126
|
+
// then step one day back for each subsequent date.
|
|
127
|
+
const today = new Date();
|
|
128
|
+
today.setHours(0, 0, 0, 0);
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < dates.length; i++) {
|
|
131
|
+
const targetDate = dates[i];
|
|
132
|
+
|
|
133
|
+
if (i === 0) {
|
|
134
|
+
// Navigate from today to the first (most recent) requested date
|
|
135
|
+
const target = new Date(targetDate + 'T00:00:00');
|
|
136
|
+
const daysBack = Math.round((today - target) / (1000 * 60 * 60 * 24));
|
|
137
|
+
for (let s = 0; s < daysBack && s < 90; s++) {
|
|
138
|
+
await clickPrevDay();
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
// Step backward one day from previous date
|
|
142
|
+
await clickPrevDay();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const data = await extractNutrition();
|
|
147
|
+
entries.push({ date: targetDate, calories: data.calories, protein: data.protein, carbs: data.carbs, fat: data.fat });
|
|
148
|
+
} catch {
|
|
149
|
+
entries.push({ date: targetDate, calories: 0, protein: 0, carbs: 0, fat: 0 });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { success: true, entries };
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=diary.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright code generators for Cronometer login automation.
|
|
3
|
+
*
|
|
4
|
+
* These functions return code strings that execute remotely via
|
|
5
|
+
* kernel.browsers.playwright.execute(). The code has access to
|
|
6
|
+
* `page`, `context`, and `browser` from the Playwright environment.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Generate code that checks if the user is logged into Cronometer.
|
|
10
|
+
* Navigates to /#diary and checks if we get redirected to a login page.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildLoginCheckCode(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Generate code that navigates to cronometer.com login page.
|
|
15
|
+
* Used during manual login so the user sees the login form in live view.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildNavigateToLoginCode(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Generate Playwright code that automates Cronometer login.
|
|
20
|
+
* Fills email/password, submits, and verifies login succeeded.
|
|
21
|
+
* Credentials are embedded via JSON.stringify for safe escaping.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildAutoLoginCode(username: string, password: string): string;
|
|
24
|
+
//# sourceMappingURL=login.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAQ5C;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAKjD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAyF7E"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright code generators for Cronometer login automation.
|
|
3
|
+
*
|
|
4
|
+
* These functions return code strings that execute remotely via
|
|
5
|
+
* kernel.browsers.playwright.execute(). The code has access to
|
|
6
|
+
* `page`, `context`, and `browser` from the Playwright environment.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Generate code that checks if the user is logged into Cronometer.
|
|
10
|
+
* Navigates to /#diary and checks if we get redirected to a login page.
|
|
11
|
+
*/
|
|
12
|
+
export function buildLoginCheckCode() {
|
|
13
|
+
return `
|
|
14
|
+
await page.goto('https://cronometer.com/#diary', { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
15
|
+
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
16
|
+
const url = page.url();
|
|
17
|
+
const isLoggedIn = url.includes('#diary') && !url.includes('/login') && !url.includes('/signin');
|
|
18
|
+
return { success: true, loggedIn: isLoggedIn, url };
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generate code that navigates to cronometer.com login page.
|
|
23
|
+
* Used during manual login so the user sees the login form in live view.
|
|
24
|
+
*/
|
|
25
|
+
export function buildNavigateToLoginCode() {
|
|
26
|
+
return `
|
|
27
|
+
await page.goto('https://cronometer.com/login/', { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
28
|
+
return { success: true };
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate Playwright code that automates Cronometer login.
|
|
33
|
+
* Fills email/password, submits, and verifies login succeeded.
|
|
34
|
+
* Credentials are embedded via JSON.stringify for safe escaping.
|
|
35
|
+
*/
|
|
36
|
+
export function buildAutoLoginCode(username, password) {
|
|
37
|
+
const safeUser = JSON.stringify(username);
|
|
38
|
+
const safePass = JSON.stringify(password);
|
|
39
|
+
return `
|
|
40
|
+
// Navigate to cronometer.com and click through to the login page
|
|
41
|
+
await page.goto('https://cronometer.com', { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
42
|
+
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
|
|
43
|
+
|
|
44
|
+
// Click the "Log In" link in the top navigation
|
|
45
|
+
const loginLinkSelectors = ['a[href="/login/"]', 'a[href="/login"]', 'a:has-text("Log In")', 'a:has-text("Login")'];
|
|
46
|
+
let clickedLogin = false;
|
|
47
|
+
for (const sel of loginLinkSelectors) {
|
|
48
|
+
try {
|
|
49
|
+
const el = page.locator(sel);
|
|
50
|
+
if (await el.count() > 0) {
|
|
51
|
+
await el.first().click();
|
|
52
|
+
clickedLogin = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
if (!clickedLogin) {
|
|
58
|
+
// Fallback: navigate directly
|
|
59
|
+
await page.goto('https://cronometer.com/login/', { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Wait for login page to load
|
|
63
|
+
await page.waitForSelector('input[type="email"], input[name="username"], input[name="email"], #email, #username', { timeout: 10000 }).catch(() => {});
|
|
64
|
+
|
|
65
|
+
// Fill email — try multiple selectors
|
|
66
|
+
const emailSelectors = ['input[type="email"]', 'input[name="username"]', 'input[name="email"]', '#email', '#username'];
|
|
67
|
+
let emailFilled = false;
|
|
68
|
+
for (const sel of emailSelectors) {
|
|
69
|
+
try {
|
|
70
|
+
const el = page.locator(sel);
|
|
71
|
+
if (await el.count() > 0) {
|
|
72
|
+
await el.first().fill(${safeUser});
|
|
73
|
+
emailFilled = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
if (!emailFilled) {
|
|
79
|
+
return { success: false, loggedIn: false, url: page.url(), error: 'Could not find email input on ' + page.url() };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fill password — try multiple selectors
|
|
83
|
+
const passSelectors = ['input[type="password"]', 'input[name="password"]', '#password'];
|
|
84
|
+
let passFilled = false;
|
|
85
|
+
for (const sel of passSelectors) {
|
|
86
|
+
try {
|
|
87
|
+
const el = page.locator(sel);
|
|
88
|
+
if (await el.count() > 0) {
|
|
89
|
+
await el.first().fill(${safePass});
|
|
90
|
+
passFilled = true;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
if (!passFilled) {
|
|
96
|
+
return { success: false, loggedIn: false, url: page.url(), error: 'Could not find password input on ' + page.url() };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Click the LOG IN button
|
|
100
|
+
const submitSelectors = ['#login-button', 'button:has-text("LOG IN")', 'button:has-text("Log In")', 'button[type="submit"]', 'input[type="submit"]'];
|
|
101
|
+
let submitted = false;
|
|
102
|
+
for (const sel of submitSelectors) {
|
|
103
|
+
try {
|
|
104
|
+
const el = page.locator(sel);
|
|
105
|
+
if (await el.count() > 0) {
|
|
106
|
+
await el.first().click();
|
|
107
|
+
submitted = true;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
if (!submitted) {
|
|
113
|
+
return { success: false, loggedIn: false, url: page.url(), error: 'Could not find submit button on ' + page.url() };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Wait for navigation after login
|
|
117
|
+
await page.waitForURL(u => !u.href.includes('/login') && !u.href.includes('/signin'), { timeout: 15000 }).catch(() => {});
|
|
118
|
+
await page.waitForTimeout(500);
|
|
119
|
+
|
|
120
|
+
const url = page.url();
|
|
121
|
+
const loggedIn = !url.includes('/login') && !url.includes('/signin');
|
|
122
|
+
return { success: true, loggedIn, url };
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO;;;;;;GAMN,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO;;;GAGN,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,QAAgB;IACnE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAE1C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAiCyB,QAAQ;;;;;;;;;;;;;;;;;kCAiBR,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCvC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright code generator for Cronometer quick-add automation.
|
|
3
|
+
*
|
|
4
|
+
* Returns a code string that executes remotely via
|
|
5
|
+
* kernel.browsers.playwright.execute(). The code has access to
|
|
6
|
+
* `page`, `context`, and `browser` from the Playwright environment.
|
|
7
|
+
*/
|
|
8
|
+
import type { MacroEntry } from "./client.js";
|
|
9
|
+
/**
|
|
10
|
+
* Macro names as they appear in Cronometer's food search.
|
|
11
|
+
* Each macro is a separate "Quick Add" food item.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MACRO_SEARCH_NAMES: Record<string, string>;
|
|
14
|
+
/**
|
|
15
|
+
* Generate Playwright code for adding a quick entry to Cronometer.
|
|
16
|
+
*
|
|
17
|
+
* For each macro, the flow is:
|
|
18
|
+
* right-click meal category → "Add Food" → search "Quick Add, <Macro>" →
|
|
19
|
+
* select result → enter serving size (grams) → "Add to Diary"
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildQuickAddCode(entry: MacroEntry): string;
|
|
22
|
+
//# sourceMappingURL=quick-add.d.ts.map
|
|
@@ -0,0 +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,CAIrD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CA+O3D"}
|