@milldr/crono 0.4.1 → 0.7.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/README.md +115 -13
- package/dist/automation/client.d.ts +5 -0
- package/dist/automation/client.d.ts.map +1 -0
- package/dist/automation/client.js +13 -0
- package/dist/automation/client.js.map +1 -0
- package/dist/automation/runner.d.ts +3 -0
- package/dist/automation/runner.d.ts.map +1 -0
- package/dist/automation/runner.js +150 -0
- package/dist/automation/runner.js.map +1 -0
- package/dist/automation/types.d.ts +75 -0
- package/dist/automation/types.d.ts.map +1 -0
- package/dist/automation/types.js +2 -0
- package/dist/automation/types.js.map +1 -0
- package/dist/commands/add.js +3 -3
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/biometrics.d.ts +10 -0
- package/dist/commands/biometrics.d.ts.map +1 -0
- package/dist/commands/biometrics.js +81 -0
- package/dist/commands/biometrics.js.map +1 -0
- package/dist/commands/diary.d.ts +1 -1
- package/dist/commands/diary.d.ts.map +1 -1
- package/dist/commands/diary.js +3 -3
- package/dist/commands/diary.js.map +1 -1
- package/dist/commands/export.d.ts +1 -0
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +45 -3
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/log.js +3 -3
- package/dist/commands/log.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +53 -26
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/quick-add.js +3 -3
- package/dist/commands/quick-add.js.map +1 -1
- package/dist/commands/recipes.d.ts +5 -0
- package/dist/commands/recipes.d.ts.map +1 -0
- package/dist/commands/recipes.js +35 -0
- package/dist/commands/recipes.js.map +1 -0
- package/dist/commands/weight.d.ts.map +1 -1
- package/dist/commands/weight.js +3 -3
- package/dist/commands/weight.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/cronometer/export.d.ts +1 -1
- package/dist/cronometer/export.d.ts.map +1 -1
- package/dist/cronometer/export.js +1 -0
- package/dist/cronometer/export.js.map +1 -1
- package/dist/cronometer/parse.d.ts +14 -0
- package/dist/cronometer/parse.d.ts.map +1 -1
- package/dist/cronometer/parse.js +62 -0
- package/dist/cronometer/parse.js.map +1 -1
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -1
- package/dist/kernel/add-custom-food.d.ts.map +1 -1
- package/dist/kernel/add-custom-food.js +6 -138
- package/dist/kernel/add-custom-food.js.map +1 -1
- package/dist/kernel/biometrics.d.ts +20 -0
- package/dist/kernel/biometrics.d.ts.map +1 -0
- package/dist/kernel/biometrics.js +171 -0
- package/dist/kernel/biometrics.js.map +1 -0
- package/dist/kernel/client.d.ts +4 -56
- package/dist/kernel/client.d.ts.map +1 -1
- package/dist/kernel/client.js +29 -274
- package/dist/kernel/client.js.map +1 -1
- package/dist/kernel/food-dialog.d.ts +13 -0
- package/dist/kernel/food-dialog.d.ts.map +1 -0
- package/dist/kernel/food-dialog.js +294 -0
- package/dist/kernel/food-dialog.js.map +1 -0
- package/dist/kernel/log-food.d.ts.map +1 -1
- package/dist/kernel/log-food.js +2 -186
- package/dist/kernel/log-food.js.map +1 -1
- package/dist/kernel/login.d.ts +1 -1
- package/dist/kernel/login.d.ts.map +1 -1
- package/dist/kernel/login.js +34 -4
- package/dist/kernel/login.js.map +1 -1
- package/dist/kernel/quick-add.d.ts.map +1 -1
- package/dist/kernel/quick-add.js +9 -195
- package/dist/kernel/quick-add.js.map +1 -1
- package/dist/kernel/recipes.d.ts +10 -0
- package/dist/kernel/recipes.d.ts.map +1 -0
- package/dist/kernel/recipes.js +38 -0
- package/dist/kernel/recipes.js.map +1 -0
- package/dist/playwright/client.d.ts +6 -0
- package/dist/playwright/client.d.ts.map +1 -0
- package/dist/playwright/client.js +41 -0
- package/dist/playwright/client.js.map +1 -0
- package/package.json +3 -2
package/dist/kernel/client.js
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Kernel.sh browser automation
|
|
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.
|
|
2
|
+
* Kernel.sh browser automation runtime.
|
|
9
3
|
*/
|
|
10
|
-
import * as p from "@clack/prompts";
|
|
11
4
|
import Kernel from "@onkernel/sdk";
|
|
5
|
+
import { createAutomationClient } from "../automation/runner.js";
|
|
12
6
|
import { getCredential } from "../credentials.js";
|
|
13
|
-
import { buildAutoLoginCode, buildLoginCheckCode, buildNavigateToLoginCode, } from "./login.js";
|
|
14
|
-
import { buildQuickAddCode } from "./quick-add.js";
|
|
15
|
-
import { buildAddCustomFoodCode } from "./add-custom-food.js";
|
|
16
|
-
import { buildLogFoodCode } from "./log-food.js";
|
|
17
|
-
import { buildDiaryCode } from "./diary.js";
|
|
18
|
-
import { buildWeightCode } from "./weight.js";
|
|
19
7
|
/**
|
|
20
8
|
* Create a Kernel client for Cronometer automation.
|
|
21
9
|
*
|
|
@@ -31,266 +19,33 @@ export async function getKernelClient() {
|
|
|
31
19
|
}
|
|
32
20
|
process.env["KERNEL_API_KEY"] = apiKey;
|
|
33
21
|
const kernel = new Kernel();
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
return createAutomationClient(createKernelRuntimeFactory(kernel));
|
|
23
|
+
}
|
|
24
|
+
function createKernelRuntimeFactory(kernel) {
|
|
25
|
+
return async (hasAutoCredentials) => {
|
|
26
|
+
const browser = await kernel.browsers.create({
|
|
27
|
+
headless: hasAutoCredentials,
|
|
28
|
+
stealth: true,
|
|
29
|
+
timeout_seconds: hasAutoCredentials ? 120 : 300,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
liveViewUrl: browser.browser_live_view_url,
|
|
33
|
+
execute: async (code, timeoutSec = 60) => {
|
|
34
|
+
const response = await kernel.browsers.playwright.execute(browser.session_id, {
|
|
35
|
+
code,
|
|
36
|
+
timeout_sec: timeoutSec,
|
|
37
|
+
});
|
|
38
|
+
return response;
|
|
39
|
+
},
|
|
40
|
+
close: async () => {
|
|
41
|
+
try {
|
|
42
|
+
await kernel.browsers.deleteByID(browser.session_id);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Browser may already be cleaned up by Kernel.
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
40
49
|
};
|
|
41
50
|
}
|
|
42
|
-
/**
|
|
43
|
-
* Execute the quick-add automation on Cronometer.
|
|
44
|
-
* Creates a browser, logs in, performs the quick-add, then tears down.
|
|
45
|
-
*/
|
|
46
|
-
async function addQuickEntry(kernel, entry, onStatus) {
|
|
47
|
-
const username = getCredential("cronometer-username");
|
|
48
|
-
const password = getCredential("cronometer-password");
|
|
49
|
-
const hasAutoCreds = !!(username && password);
|
|
50
|
-
const browser = await kernel.browsers.create({
|
|
51
|
-
headless: hasAutoCreds,
|
|
52
|
-
stealth: true,
|
|
53
|
-
timeout_seconds: hasAutoCreds ? 120 : 300,
|
|
54
|
-
});
|
|
55
|
-
try {
|
|
56
|
-
// Log in to Cronometer
|
|
57
|
-
if (hasAutoCreds) {
|
|
58
|
-
await autoLogin(kernel, browser.session_id, username, password, onStatus);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
await manualLogin(kernel, browser);
|
|
62
|
-
}
|
|
63
|
-
// Execute quick-add
|
|
64
|
-
onStatus?.("Adding quick entry...");
|
|
65
|
-
const result = await kernel.browsers.playwright.execute(browser.session_id, { code: buildQuickAddCode(entry), timeout_sec: 60 });
|
|
66
|
-
if (!result.success) {
|
|
67
|
-
throw new Error(`Automation failed: ${result.error ?? "Unknown error"}`);
|
|
68
|
-
}
|
|
69
|
-
const data = result.result;
|
|
70
|
-
if (!data.success) {
|
|
71
|
-
throw new Error(`Quick add failed: ${data.error ?? "Unknown error"}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
finally {
|
|
75
|
-
try {
|
|
76
|
-
await kernel.browsers.deleteByID(browser.session_id);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// Browser may already be cleaned up by Kernel
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Execute the weight scraping automation on Cronometer.
|
|
85
|
-
* Creates a browser, logs in, reads weight data, then tears down.
|
|
86
|
-
*/
|
|
87
|
-
async function getWeight(kernel, dates, onStatus) {
|
|
88
|
-
const username = getCredential("cronometer-username");
|
|
89
|
-
const password = getCredential("cronometer-password");
|
|
90
|
-
const hasAutoCreds = !!(username && password);
|
|
91
|
-
const browser = await kernel.browsers.create({
|
|
92
|
-
headless: hasAutoCreds,
|
|
93
|
-
stealth: true,
|
|
94
|
-
timeout_seconds: hasAutoCreds ? 120 : 300,
|
|
95
|
-
});
|
|
96
|
-
try {
|
|
97
|
-
if (hasAutoCreds) {
|
|
98
|
-
await autoLogin(kernel, browser.session_id, username, password, onStatus);
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
await manualLogin(kernel, browser);
|
|
102
|
-
}
|
|
103
|
-
onStatus?.("Reading weight data...");
|
|
104
|
-
const result = await kernel.browsers.playwright.execute(browser.session_id, { code: buildWeightCode(dates), timeout_sec: 60 });
|
|
105
|
-
if (!result.success) {
|
|
106
|
-
throw new Error(`Automation failed: ${result.error ?? "Unknown error"}`);
|
|
107
|
-
}
|
|
108
|
-
const data = result.result;
|
|
109
|
-
if (!data.success) {
|
|
110
|
-
throw new Error(`Weight read failed: ${data.error ?? "Unknown error"}`);
|
|
111
|
-
}
|
|
112
|
-
return data.entries ?? [];
|
|
113
|
-
}
|
|
114
|
-
finally {
|
|
115
|
-
try {
|
|
116
|
-
await kernel.browsers.deleteByID(browser.session_id);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// Browser may already be cleaned up by Kernel
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Execute the diary nutrition scraping automation on Cronometer.
|
|
125
|
-
* Creates a browser, logs in, reads nutrition data, then tears down.
|
|
126
|
-
*/
|
|
127
|
-
async function getDiary(kernel, dates, onStatus, scrapeTargets) {
|
|
128
|
-
const username = getCredential("cronometer-username");
|
|
129
|
-
const password = getCredential("cronometer-password");
|
|
130
|
-
const hasAutoCreds = !!(username && password);
|
|
131
|
-
const browser = await kernel.browsers.create({
|
|
132
|
-
headless: hasAutoCreds,
|
|
133
|
-
stealth: true,
|
|
134
|
-
timeout_seconds: hasAutoCreds ? 120 : 300,
|
|
135
|
-
});
|
|
136
|
-
try {
|
|
137
|
-
if (hasAutoCreds) {
|
|
138
|
-
await autoLogin(kernel, browser.session_id, username, password, onStatus);
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
await manualLogin(kernel, browser);
|
|
142
|
-
}
|
|
143
|
-
onStatus?.("Reading diary data...");
|
|
144
|
-
const result = await kernel.browsers.playwright.execute(browser.session_id, { code: buildDiaryCode(dates, scrapeTargets), timeout_sec: 60 });
|
|
145
|
-
if (!result.success) {
|
|
146
|
-
throw new Error(`Automation failed: ${result.error ?? "Unknown error"}`);
|
|
147
|
-
}
|
|
148
|
-
const data = result.result;
|
|
149
|
-
if (!data.success) {
|
|
150
|
-
throw new Error(`Diary read failed: ${data.error ?? "Unknown error"}`);
|
|
151
|
-
}
|
|
152
|
-
return data.entries ?? [];
|
|
153
|
-
}
|
|
154
|
-
finally {
|
|
155
|
-
try {
|
|
156
|
-
await kernel.browsers.deleteByID(browser.session_id);
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// Browser may already be cleaned up by Kernel
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Execute the custom food creation automation on Cronometer.
|
|
165
|
-
* Creates a browser, logs in, creates the food, optionally logs it, then tears down.
|
|
166
|
-
*/
|
|
167
|
-
async function addCustomFood(kernel, entry, onStatus) {
|
|
168
|
-
const username = getCredential("cronometer-username");
|
|
169
|
-
const password = getCredential("cronometer-password");
|
|
170
|
-
const hasAutoCreds = !!(username && password);
|
|
171
|
-
const browser = await kernel.browsers.create({
|
|
172
|
-
headless: hasAutoCreds,
|
|
173
|
-
stealth: true,
|
|
174
|
-
timeout_seconds: hasAutoCreds ? 120 : 300,
|
|
175
|
-
});
|
|
176
|
-
try {
|
|
177
|
-
if (hasAutoCreds) {
|
|
178
|
-
await autoLogin(kernel, browser.session_id, username, password, onStatus);
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
await manualLogin(kernel, browser);
|
|
182
|
-
}
|
|
183
|
-
onStatus?.("Creating custom food...");
|
|
184
|
-
const result = await kernel.browsers.playwright.execute(browser.session_id, { code: buildAddCustomFoodCode(entry), timeout_sec: 120 });
|
|
185
|
-
if (!result.success) {
|
|
186
|
-
throw new Error(`Automation failed: ${result.error ?? "Unknown error"}`);
|
|
187
|
-
}
|
|
188
|
-
const data = result.result;
|
|
189
|
-
if (!data.success) {
|
|
190
|
-
throw new Error(`Custom food creation failed: ${data.error ?? "Unknown error"}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
finally {
|
|
194
|
-
try {
|
|
195
|
-
await kernel.browsers.deleteByID(browser.session_id);
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
// Browser may already be cleaned up by Kernel
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Execute the food logging automation on Cronometer.
|
|
204
|
-
* Creates a browser, logs in, searches for the food, logs it, then tears down.
|
|
205
|
-
*/
|
|
206
|
-
async function logFood(kernel, entry, onStatus) {
|
|
207
|
-
const username = getCredential("cronometer-username");
|
|
208
|
-
const password = getCredential("cronometer-password");
|
|
209
|
-
const hasAutoCreds = !!(username && password);
|
|
210
|
-
const browser = await kernel.browsers.create({
|
|
211
|
-
headless: hasAutoCreds,
|
|
212
|
-
stealth: true,
|
|
213
|
-
timeout_seconds: hasAutoCreds ? 120 : 300,
|
|
214
|
-
});
|
|
215
|
-
try {
|
|
216
|
-
if (hasAutoCreds) {
|
|
217
|
-
await autoLogin(kernel, browser.session_id, username, password, onStatus);
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
await manualLogin(kernel, browser);
|
|
221
|
-
}
|
|
222
|
-
onStatus?.("Logging food to diary...");
|
|
223
|
-
const result = await kernel.browsers.playwright.execute(browser.session_id, { code: buildLogFoodCode(entry), timeout_sec: 60 });
|
|
224
|
-
if (!result.success) {
|
|
225
|
-
throw new Error(`Automation failed: ${result.error ?? "Unknown error"}`);
|
|
226
|
-
}
|
|
227
|
-
const data = result.result;
|
|
228
|
-
if (!data.success) {
|
|
229
|
-
throw new Error(`Food logging failed: ${data.error ?? "Unknown error"}`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
finally {
|
|
233
|
-
try {
|
|
234
|
-
await kernel.browsers.deleteByID(browser.session_id);
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
// Browser may already be cleaned up by Kernel
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Auto-login using stored Cronometer credentials.
|
|
243
|
-
*/
|
|
244
|
-
async function autoLogin(kernel, sessionId, username, password, onStatus) {
|
|
245
|
-
onStatus?.("Logging into Cronometer...");
|
|
246
|
-
const result = await kernel.browsers.playwright.execute(sessionId, {
|
|
247
|
-
code: buildAutoLoginCode(username, password),
|
|
248
|
-
timeout_sec: 60,
|
|
249
|
-
});
|
|
250
|
-
const data = result.result;
|
|
251
|
-
if (!result.success || !data.loggedIn) {
|
|
252
|
-
const pageError = data.loginError?.toLowerCase() ?? "";
|
|
253
|
-
const isRateLimited = pageError.includes("too many") ||
|
|
254
|
-
pageError.includes("rate limit") ||
|
|
255
|
-
pageError.includes("try again later") ||
|
|
256
|
-
pageError.includes("temporarily");
|
|
257
|
-
if (isRateLimited) {
|
|
258
|
-
throw new Error(`Cronometer is rate-limiting login attempts. ${data.loginError}\n` +
|
|
259
|
-
"Please wait a few minutes and try again.");
|
|
260
|
-
}
|
|
261
|
-
throw new Error(`Auto-login failed: ${data.error ?? data.loginError ?? "Login verification failed"}.\n` +
|
|
262
|
-
"Your credentials may be incorrect. Run `crono login` to update them.");
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Manual login: show a live browser view for the user to log in interactively.
|
|
267
|
-
*/
|
|
268
|
-
async function manualLogin(kernel, browser) {
|
|
269
|
-
p.log.warn("No stored Cronometer credentials found.");
|
|
270
|
-
p.log.info("Tip: run `crono login` to save credentials for automatic login.");
|
|
271
|
-
if (browser.browser_live_view_url) {
|
|
272
|
-
p.note(browser.browser_live_view_url, "Browser Live View");
|
|
273
|
-
}
|
|
274
|
-
await kernel.browsers.playwright.execute(browser.session_id, {
|
|
275
|
-
code: buildNavigateToLoginCode(),
|
|
276
|
-
timeout_sec: 30,
|
|
277
|
-
});
|
|
278
|
-
p.log.info("Please log into Cronometer in the browser above.");
|
|
279
|
-
const confirmation = await p.text({
|
|
280
|
-
message: "Press Enter once you've logged in...",
|
|
281
|
-
defaultValue: "",
|
|
282
|
-
});
|
|
283
|
-
if (p.isCancel(confirmation)) {
|
|
284
|
-
throw new Error("Login cancelled by user.");
|
|
285
|
-
}
|
|
286
|
-
const result = await kernel.browsers.playwright.execute(browser.session_id, {
|
|
287
|
-
code: buildLoginCheckCode(),
|
|
288
|
-
timeout_sec: 30,
|
|
289
|
-
});
|
|
290
|
-
const data = result.result;
|
|
291
|
-
if (!result.success || !data.loggedIn) {
|
|
292
|
-
throw new Error("Login verification failed. Make sure you're fully logged in and try again.");
|
|
293
|
-
}
|
|
294
|
-
p.log.step("Logged in.");
|
|
295
|
-
}
|
|
296
51
|
//# sourceMappingURL=client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/kernel/client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/kernel/client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,MAAM,MAAM,eAAe,CAAC;AACnC,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAYjE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAWlD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,MAAM,MAAM,GACV,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACnE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,6BAA6B;YAC3B,uDAAuD;YACvD,uCAAuC,CAC1C,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,GAAG,MAAM,CAAC;IACvC,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;IAE5B,OAAO,sBAAsB,CAAC,0BAA0B,CAAC,MAAM,CAAC,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,0BAA0B,CAAC,MAAc;IAChD,OAAO,KAAK,EAAE,kBAA2B,EAAE,EAAE;QAC3C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC3C,QAAQ,EAAE,kBAAkB;YAC5B,OAAO,EAAE,IAAI;YACb,eAAe,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;SAChD,CAAC,CAAC;QAEH,OAAO;YACL,WAAW,EAAE,OAAO,CAAC,qBAAqB;YAC1C,OAAO,EAAE,KAAK,EACZ,IAAY,EACZ,UAAU,GAAG,EAAE,EAC0B,EAAE;gBAC3C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CACvD,OAAO,CAAC,UAAU,EAClB;oBACE,IAAI;oBACJ,WAAW,EAAE,UAAU;iBACxB,CACF,CAAC;gBACF,OAAO,QAA0C,CAAC;YACpD,CAAC;YACD,KAAK,EAAE,KAAK,IAAI,EAAE;gBAChB,IAAI,CAAC;oBACH,MAAM,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;gBACvD,CAAC;gBAAC,MAAM,CAAC;oBACP,+CAA+C;gBACjD,CAAC;YACH,CAAC;SAC0B,CAAC;IAChC,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface FoodDialogOptions {
|
|
2
|
+
foodNameVar?: string;
|
|
3
|
+
mealLabelVar?: string;
|
|
4
|
+
errorPrefix?: string;
|
|
5
|
+
itemNameVar?: string;
|
|
6
|
+
requireServingSize?: boolean;
|
|
7
|
+
servingCountVar?: string;
|
|
8
|
+
alwaysUpdateServingSize?: boolean;
|
|
9
|
+
updateServingSize?: boolean;
|
|
10
|
+
verifyDialogDismissed?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildFoodDialogCode(options?: FoodDialogOptions): string;
|
|
13
|
+
//# sourceMappingURL=food-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"food-dialog.d.ts","sourceRoot":"","sources":["../../src/kernel/food-dialog.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAqN3E"}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
export function buildFoodDialogCode(options = {}) {
|
|
2
|
+
const foodNameVar = options.foodNameVar ?? "foodName";
|
|
3
|
+
const mealLabelVar = options.mealLabelVar ?? "mealLabel";
|
|
4
|
+
const errorPrefix = options.errorPrefix ?? "";
|
|
5
|
+
const itemNameVar = options.itemNameVar ?? foodNameVar;
|
|
6
|
+
const requireServingSize = options.requireServingSize ?? true;
|
|
7
|
+
const servingCountVar = options.servingCountVar ?? "servingCount";
|
|
8
|
+
const alwaysUpdateServingSize = options.alwaysUpdateServingSize ?? false;
|
|
9
|
+
const updateServingSize = options.updateServingSize ?? false;
|
|
10
|
+
const verifyDialogDismissed = options.verifyDialogDismissed ?? false;
|
|
11
|
+
const mealCategoryError = errorPrefix
|
|
12
|
+
? `${errorPrefix}could not find meal category "`
|
|
13
|
+
: 'Could not find meal category "';
|
|
14
|
+
const contextMenuError = errorPrefix
|
|
15
|
+
? `${errorPrefix}context menu did not appear after right-clicking "`
|
|
16
|
+
: 'Context menu did not appear after right-clicking "';
|
|
17
|
+
const addFoodError = errorPrefix
|
|
18
|
+
? `${errorPrefix}could not find "Add Food" in context menu`
|
|
19
|
+
: 'Could not find "Add Food" in context menu';
|
|
20
|
+
const searchBarError = errorPrefix
|
|
21
|
+
? `${errorPrefix}could not find food search bar in Add Food dialog`
|
|
22
|
+
: "Could not find food search bar in Add Food dialog";
|
|
23
|
+
const noFoodError = errorPrefix
|
|
24
|
+
? `${errorPrefix}no food found matching "`
|
|
25
|
+
: 'No food found matching "';
|
|
26
|
+
const noExactResultError = errorPrefix
|
|
27
|
+
? `${errorPrefix}no exact search result found for "`
|
|
28
|
+
: 'No exact search result found for "';
|
|
29
|
+
const addToDiaryError = errorPrefix
|
|
30
|
+
? `${errorPrefix}could not find "Add to Diary" button`
|
|
31
|
+
: 'Could not find "Add to Diary" button';
|
|
32
|
+
const addToDiaryErrorCode = itemNameVar === foodNameVar
|
|
33
|
+
? `'${addToDiaryError}'`
|
|
34
|
+
: `'${addToDiaryError} for "' + ${itemNameVar} + '"'`;
|
|
35
|
+
return `
|
|
36
|
+
// Helper: right-click an element from a list of selectors
|
|
37
|
+
async function rightClickFirst(selectors, description) {
|
|
38
|
+
for (const sel of selectors) {
|
|
39
|
+
try {
|
|
40
|
+
const el = page.locator(sel);
|
|
41
|
+
if (await el.count() > 0) {
|
|
42
|
+
await el.first().click({ button: 'right', timeout: 5000 });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Right-click meal category with retry (GWT context menus can be flaky
|
|
51
|
+
// and meal labels may not render immediately)
|
|
52
|
+
let menuVisible = false;
|
|
53
|
+
for (let attempt = 0; attempt < 3 && !menuVisible; attempt++) {
|
|
54
|
+
if (attempt > 0) {
|
|
55
|
+
await page.keyboard.press('Escape');
|
|
56
|
+
await page.mouse.click(1, 1);
|
|
57
|
+
await page.waitForTimeout(2000);
|
|
58
|
+
}
|
|
59
|
+
const clicked = await rightClickFirst([
|
|
60
|
+
'td:text("' + ${mealLabelVar} + '")',
|
|
61
|
+
'div:text("' + ${mealLabelVar} + '")',
|
|
62
|
+
'text="' + ${mealLabelVar} + '"',
|
|
63
|
+
], 'meal category');
|
|
64
|
+
if (!clicked) {
|
|
65
|
+
if (attempt < 2) {
|
|
66
|
+
await page.waitForTimeout(2000);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
return { success: false, error: '${mealCategoryError}' + ${mealLabelVar} + '" in diary' };
|
|
70
|
+
}
|
|
71
|
+
menuVisible = await page.waitForSelector('text="Add Food..."', { timeout: 3000 })
|
|
72
|
+
.then(() => true)
|
|
73
|
+
.catch(() => page.waitForSelector('text="Add Food"', { timeout: 2000 }).then(() => true).catch(() => false));
|
|
74
|
+
}
|
|
75
|
+
if (!menuVisible) {
|
|
76
|
+
return { success: false, error: '${contextMenuError}' + ${mealLabelVar} + '"' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Click "Add Food..." in context menu
|
|
80
|
+
const addFoodClicked = await clickFirst([
|
|
81
|
+
'text="Add Food..."',
|
|
82
|
+
'text="Add Food\\u2026"',
|
|
83
|
+
'text="Add Food"',
|
|
84
|
+
'[role="menuitem"]:has-text("Add Food")',
|
|
85
|
+
], 'Add Food menu item');
|
|
86
|
+
if (!addFoodClicked) {
|
|
87
|
+
return { success: false, error: '${addFoodError}' };
|
|
88
|
+
}
|
|
89
|
+
await page.waitForTimeout(200);
|
|
90
|
+
|
|
91
|
+
// Wait for "Add Food to Diary" dialog
|
|
92
|
+
try {
|
|
93
|
+
await page.waitForSelector('text="Add Food to Diary"', { timeout: 5000 });
|
|
94
|
+
} catch {
|
|
95
|
+
return { success: false, error: '${errorPrefix}Add Food to Diary dialog did not appear' };
|
|
96
|
+
}
|
|
97
|
+
await page.waitForTimeout(300);
|
|
98
|
+
|
|
99
|
+
// Search for the food
|
|
100
|
+
const searchSelectors = [
|
|
101
|
+
'input[placeholder*="Search all foods" i]',
|
|
102
|
+
'input[placeholder*="Search" i]',
|
|
103
|
+
'input[placeholder*="food" i]',
|
|
104
|
+
'input.gwt-TextBox',
|
|
105
|
+
'input[type="text"]',
|
|
106
|
+
'input[type="search"]',
|
|
107
|
+
];
|
|
108
|
+
let searched = false;
|
|
109
|
+
for (const sel of searchSelectors) {
|
|
110
|
+
try {
|
|
111
|
+
const el = page.locator(sel);
|
|
112
|
+
if (await el.count() > 0) {
|
|
113
|
+
await el.first().click();
|
|
114
|
+
await page.waitForTimeout(200);
|
|
115
|
+
await el.first().fill('');
|
|
116
|
+
await page.keyboard.type(${foodNameVar}, { delay: 50 });
|
|
117
|
+
searched = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
if (!searched) {
|
|
123
|
+
return { success: false, error: '${searchBarError}' };
|
|
124
|
+
}
|
|
125
|
+
await page.waitForTimeout(300);
|
|
126
|
+
|
|
127
|
+
// Click SEARCH
|
|
128
|
+
await clickFirst([
|
|
129
|
+
'text="SEARCH")',
|
|
130
|
+
'button:has-text("SEARCH")',
|
|
131
|
+
'button:has-text("Search")',
|
|
132
|
+
], 'SEARCH button');
|
|
133
|
+
|
|
134
|
+
// Wait for search results to appear
|
|
135
|
+
const resultsAppeared = await page.waitForSelector('td', { timeout: 8000 })
|
|
136
|
+
.then(() => true)
|
|
137
|
+
.catch(() => false);
|
|
138
|
+
if (!resultsAppeared) {
|
|
139
|
+
return { success: false, error: '${noFoodError}' + ${foodNameVar} + '"' };
|
|
140
|
+
}
|
|
141
|
+
await page.waitForTimeout(2000);
|
|
142
|
+
|
|
143
|
+
// Select the first visible result row whose Description cell exactly
|
|
144
|
+
// matches the requested food. Cronometer renders duplicate result rows, so
|
|
145
|
+
// iterate them and click the first good row. Only consider rows with exactly
|
|
146
|
+
// 2 cells (Description + Source columns in search results), not diary entry
|
|
147
|
+
// rows (8+ cells). We scan ALL <tr> elements because the search results table
|
|
148
|
+
// is distinct from the diary table shown in the background.
|
|
149
|
+
const allRows = page.locator('tr');
|
|
150
|
+
let resultClicked = false;
|
|
151
|
+
const allRowCount = await allRows.count();
|
|
152
|
+
console.log('[crono food-dialog] scanning ' + allRowCount + ' total rows');
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < allRowCount; i++) {
|
|
155
|
+
const row = allRows.nth(i);
|
|
156
|
+
const visible = await row.isVisible().catch(() => false);
|
|
157
|
+
if (!visible) continue;
|
|
158
|
+
|
|
159
|
+
// Only consider rows with exactly 2 cells (search results table)
|
|
160
|
+
const cellCount = await row.locator('td').count();
|
|
161
|
+
if (cellCount !== 2) continue;
|
|
162
|
+
|
|
163
|
+
const description = (await row.locator('td').first().textContent().catch(() => '') || '').trim();
|
|
164
|
+
if (description !== ${foodNameVar}) continue;
|
|
165
|
+
|
|
166
|
+
console.log('[crono food-dialog] found matching search result at row ' + i + ': ' + description);
|
|
167
|
+
// GWT rows don't respond to click(), need to focus and press Enter
|
|
168
|
+
await row.focus();
|
|
169
|
+
await page.waitForTimeout(300);
|
|
170
|
+
await page.keyboard.press('Enter');
|
|
171
|
+
resultClicked = true;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!resultClicked) {
|
|
176
|
+
return { success: false, error: '${noExactResultError}' + ${foodNameVar} + '"' };
|
|
177
|
+
}
|
|
178
|
+
await page.waitForTimeout(1000);
|
|
179
|
+
|
|
180
|
+
const dialogStillOpen = await page.waitForSelector('text="Add Food to Diary"', { timeout: 1000 })
|
|
181
|
+
.then(() => true)
|
|
182
|
+
.catch(() => false);
|
|
183
|
+
if (!dialogStillOpen) {
|
|
184
|
+
return { success: false, error: 'Add Food dialog closed before details loaded after selecting "' + ${foodNameVar} + '"' };
|
|
185
|
+
}
|
|
186
|
+
console.log('[crono food-dialog] selected search result and dialog stayed open: ' + ${foodNameVar});
|
|
187
|
+
|
|
188
|
+
${buildServingSizeCode({ errorPrefix, foodNameVar, itemNameVar, requireServingSize, servingCountVar, alwaysUpdateServingSize, updateServingSize })}
|
|
189
|
+
|
|
190
|
+
// Click "Add to Diary" button - wait for panel to render, then find button using evaluate
|
|
191
|
+
await page.waitForTimeout(2000);
|
|
192
|
+
|
|
193
|
+
// Find and click the button using page.evaluate
|
|
194
|
+
const buttonClicked = await page.evaluate(() => {
|
|
195
|
+
const buttons = document.querySelectorAll('button');
|
|
196
|
+
for (const btn of buttons) {
|
|
197
|
+
if (btn.textContent && btn.textContent.trim() === 'Add to Diary' && btn.offsetParent !== null) {
|
|
198
|
+
btn.click();
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!buttonClicked) {
|
|
206
|
+
return { success: false, error: ${addToDiaryErrorCode} };
|
|
207
|
+
}
|
|
208
|
+
console.log('[crono food-dialog] found and clicked Add to Diary button for: ' + ${itemNameVar});
|
|
209
|
+
console.log('[crono food-dialog] clicked Add to Diary for: ' + ${itemNameVar});
|
|
210
|
+
${buildDialogDismissedCode({ itemNameVar, verifyDialogDismissed })}
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
function buildServingSizeCode(options) {
|
|
214
|
+
const servingCondition = options.alwaysUpdateServingSize
|
|
215
|
+
? "true"
|
|
216
|
+
: `${options.servingCountVar} !== 1`;
|
|
217
|
+
const requireBlock = options.requireServingSize
|
|
218
|
+
? `
|
|
219
|
+
// Wait for the detail panel with serving size
|
|
220
|
+
try {
|
|
221
|
+
await page.waitForSelector('text="Serving Size"', { timeout: 5000 });
|
|
222
|
+
console.log('[crono food-dialog] found Serving Size panel for: ' + ${options.itemNameVar});
|
|
223
|
+
} catch {
|
|
224
|
+
return { success: false, error: '${options.errorPrefix}Serving Size panel did not appear for "' + ${options.itemNameVar} + '"' };
|
|
225
|
+
}
|
|
226
|
+
await page.waitForTimeout(500);
|
|
227
|
+
`
|
|
228
|
+
: "";
|
|
229
|
+
if (!options.updateServingSize) {
|
|
230
|
+
return requireBlock;
|
|
231
|
+
}
|
|
232
|
+
return `${requireBlock}
|
|
233
|
+
// If needed, update the serving size input
|
|
234
|
+
if (${servingCondition}) {
|
|
235
|
+
let servingFilled = false;
|
|
236
|
+
try {
|
|
237
|
+
servingFilled = await page.evaluate((count) => {
|
|
238
|
+
const walker = document.createTreeWalker(
|
|
239
|
+
document.body,
|
|
240
|
+
NodeFilter.SHOW_TEXT,
|
|
241
|
+
{ acceptNode: (node) =>
|
|
242
|
+
node.textContent && node.textContent.trim() === 'Serving Size'
|
|
243
|
+
? NodeFilter.FILTER_ACCEPT
|
|
244
|
+
: NodeFilter.FILTER_REJECT
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
const textNode = walker.nextNode();
|
|
248
|
+
if (!textNode) return false;
|
|
249
|
+
|
|
250
|
+
let container = textNode.parentElement;
|
|
251
|
+
for (let i = 0; i < 5 && container; i++) {
|
|
252
|
+
const input = container.querySelector('input');
|
|
253
|
+
if (input) {
|
|
254
|
+
input.focus();
|
|
255
|
+
input.select();
|
|
256
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
257
|
+
window.HTMLInputElement.prototype, 'value'
|
|
258
|
+
).set;
|
|
259
|
+
nativeSetter.call(input, String(count));
|
|
260
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
261
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
container = container.parentElement;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}, ${options.servingCountVar});
|
|
268
|
+
} catch {}
|
|
269
|
+
|
|
270
|
+
if (!servingFilled) {
|
|
271
|
+
return { success: false, error: '${options.errorPrefix}could not update serving size for "' + ${options.itemNameVar} + '"' };
|
|
272
|
+
}
|
|
273
|
+
console.log('[crono food-dialog] updated serving size for: ' + ${options.itemNameVar});
|
|
274
|
+
await page.waitForTimeout(500);
|
|
275
|
+
}
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
function buildDialogDismissedCode(options) {
|
|
279
|
+
if (!options.verifyDialogDismissed) {
|
|
280
|
+
return `
|
|
281
|
+
await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 }).catch(() => {});
|
|
282
|
+
await page.waitForTimeout(300);
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
return `
|
|
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 "' + ${options.itemNameVar} + '"' };
|
|
290
|
+
}
|
|
291
|
+
await page.waitForTimeout(500);
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=food-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"food-dialog.js","sourceRoot":"","sources":["../../src/kernel/food-dialog.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-food.d.ts","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"log-food.d.ts","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CA2C5D"}
|