@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.
Files changed (89) hide show
  1. package/README.md +115 -13
  2. package/dist/automation/client.d.ts +5 -0
  3. package/dist/automation/client.d.ts.map +1 -0
  4. package/dist/automation/client.js +13 -0
  5. package/dist/automation/client.js.map +1 -0
  6. package/dist/automation/runner.d.ts +3 -0
  7. package/dist/automation/runner.d.ts.map +1 -0
  8. package/dist/automation/runner.js +150 -0
  9. package/dist/automation/runner.js.map +1 -0
  10. package/dist/automation/types.d.ts +75 -0
  11. package/dist/automation/types.d.ts.map +1 -0
  12. package/dist/automation/types.js +2 -0
  13. package/dist/automation/types.js.map +1 -0
  14. package/dist/commands/add.js +3 -3
  15. package/dist/commands/add.js.map +1 -1
  16. package/dist/commands/biometrics.d.ts +10 -0
  17. package/dist/commands/biometrics.d.ts.map +1 -0
  18. package/dist/commands/biometrics.js +81 -0
  19. package/dist/commands/biometrics.js.map +1 -0
  20. package/dist/commands/diary.d.ts +1 -1
  21. package/dist/commands/diary.d.ts.map +1 -1
  22. package/dist/commands/diary.js +3 -3
  23. package/dist/commands/diary.js.map +1 -1
  24. package/dist/commands/export.d.ts +1 -0
  25. package/dist/commands/export.d.ts.map +1 -1
  26. package/dist/commands/export.js +45 -3
  27. package/dist/commands/export.js.map +1 -1
  28. package/dist/commands/log.js +3 -3
  29. package/dist/commands/log.js.map +1 -1
  30. package/dist/commands/login.d.ts.map +1 -1
  31. package/dist/commands/login.js +53 -26
  32. package/dist/commands/login.js.map +1 -1
  33. package/dist/commands/quick-add.js +3 -3
  34. package/dist/commands/quick-add.js.map +1 -1
  35. package/dist/commands/recipes.d.ts +5 -0
  36. package/dist/commands/recipes.d.ts.map +1 -0
  37. package/dist/commands/recipes.js +35 -0
  38. package/dist/commands/recipes.js.map +1 -0
  39. package/dist/commands/weight.d.ts.map +1 -1
  40. package/dist/commands/weight.js +3 -3
  41. package/dist/commands/weight.js.map +1 -1
  42. package/dist/config.d.ts +6 -0
  43. package/dist/config.d.ts.map +1 -1
  44. package/dist/config.js +6 -0
  45. package/dist/config.js.map +1 -1
  46. package/dist/cronometer/export.d.ts +1 -1
  47. package/dist/cronometer/export.d.ts.map +1 -1
  48. package/dist/cronometer/export.js +1 -0
  49. package/dist/cronometer/export.js.map +1 -1
  50. package/dist/cronometer/parse.d.ts +14 -0
  51. package/dist/cronometer/parse.d.ts.map +1 -1
  52. package/dist/cronometer/parse.js +62 -0
  53. package/dist/cronometer/parse.js.map +1 -1
  54. package/dist/index.js +23 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/kernel/add-custom-food.d.ts.map +1 -1
  57. package/dist/kernel/add-custom-food.js +6 -138
  58. package/dist/kernel/add-custom-food.js.map +1 -1
  59. package/dist/kernel/biometrics.d.ts +20 -0
  60. package/dist/kernel/biometrics.d.ts.map +1 -0
  61. package/dist/kernel/biometrics.js +171 -0
  62. package/dist/kernel/biometrics.js.map +1 -0
  63. package/dist/kernel/client.d.ts +4 -56
  64. package/dist/kernel/client.d.ts.map +1 -1
  65. package/dist/kernel/client.js +29 -274
  66. package/dist/kernel/client.js.map +1 -1
  67. package/dist/kernel/food-dialog.d.ts +13 -0
  68. package/dist/kernel/food-dialog.d.ts.map +1 -0
  69. package/dist/kernel/food-dialog.js +294 -0
  70. package/dist/kernel/food-dialog.js.map +1 -0
  71. package/dist/kernel/log-food.d.ts.map +1 -1
  72. package/dist/kernel/log-food.js +2 -186
  73. package/dist/kernel/log-food.js.map +1 -1
  74. package/dist/kernel/login.d.ts +1 -1
  75. package/dist/kernel/login.d.ts.map +1 -1
  76. package/dist/kernel/login.js +34 -4
  77. package/dist/kernel/login.js.map +1 -1
  78. package/dist/kernel/quick-add.d.ts.map +1 -1
  79. package/dist/kernel/quick-add.js +9 -195
  80. package/dist/kernel/quick-add.js.map +1 -1
  81. package/dist/kernel/recipes.d.ts +10 -0
  82. package/dist/kernel/recipes.d.ts.map +1 -0
  83. package/dist/kernel/recipes.js +38 -0
  84. package/dist/kernel/recipes.js.map +1 -0
  85. package/dist/playwright/client.d.ts +6 -0
  86. package/dist/playwright/client.d.ts.map +1 -0
  87. package/dist/playwright/client.js +41 -0
  88. package/dist/playwright/client.js.map +1 -0
  89. package/package.json +3 -2
@@ -5,6 +5,7 @@
5
5
  * kernel.browsers.playwright.execute(). The code has access to
6
6
  * `page`, `context`, and `browser` from the Playwright environment.
7
7
  */
8
+ import { buildFoodDialogCode } from "./food-dialog.js";
8
9
  /**
9
10
  * Generate Playwright code for logging a food to the Cronometer diary.
10
11
  *
@@ -48,192 +49,7 @@ export function buildLogFoodCode(entry) {
48
49
  return false;
49
50
  }
50
51
 
51
- // Helper: right-click an element from a list of selectors
52
- async function rightClickFirst(selectors, description) {
53
- for (const sel of selectors) {
54
- try {
55
- const el = page.locator(sel);
56
- if (await el.count() > 0) {
57
- await el.first().click({ button: 'right' });
58
- return true;
59
- }
60
- } catch {}
61
- }
62
- return false;
63
- }
64
-
65
- // Right-click meal category with retry (GWT context menus can be flaky)
66
- let menuVisible = false;
67
- for (let attempt = 0; attempt < 3 && !menuVisible; attempt++) {
68
- if (attempt > 0) {
69
- await page.keyboard.press('Escape');
70
- await page.mouse.click(1, 1);
71
- await page.waitForTimeout(1000);
72
- }
73
- const clicked = await rightClickFirst([
74
- 'text="' + mealLabel + '"',
75
- ':has-text("' + mealLabel + '")',
76
- ], 'meal category');
77
- if (!clicked) {
78
- return { success: false, error: 'Could not find meal category "' + mealLabel + '" in diary' };
79
- }
80
- menuVisible = await page.waitForSelector('text="Add Food..."', { timeout: 3000 })
81
- .then(() => true)
82
- .catch(() => page.waitForSelector('text="Add Food"', { timeout: 2000 }).then(() => true).catch(() => false));
83
- }
84
- if (!menuVisible) {
85
- return { success: false, error: 'Context menu did not appear after right-clicking "' + mealLabel + '"' };
86
- }
87
-
88
- // Click "Add Food..." in context menu
89
- const addFoodClicked = await clickFirst([
90
- 'text="Add Food..."',
91
- 'text="Add Food\u2026"',
92
- 'text="Add Food"',
93
- '[role="menuitem"]:has-text("Add Food")',
94
- ], 'Add Food menu item');
95
- if (!addFoodClicked) {
96
- return { success: false, error: 'Could not find "Add Food" in context menu' };
97
- }
98
- await page.waitForTimeout(200);
99
-
100
- // Wait for "Add Food to Diary" dialog
101
- try {
102
- await page.waitForSelector('text="Add Food to Diary"', { timeout: 5000 });
103
- } catch {
104
- return { success: false, error: 'Add Food to Diary dialog did not appear' };
105
- }
106
- await page.waitForTimeout(300);
107
-
108
- // Search for the food
109
- const searchSelectors = [
110
- 'input[placeholder*="Search all foods" i]',
111
- 'input[placeholder*="Search" i]',
112
- 'input[placeholder*="food" i]',
113
- 'input.gwt-TextBox',
114
- 'input[type="text"]',
115
- 'input[type="search"]',
116
- ];
117
- let searched = false;
118
- for (const sel of searchSelectors) {
119
- try {
120
- const el = page.locator(sel);
121
- if (await el.count() > 0) {
122
- await el.first().click();
123
- await page.waitForTimeout(200);
124
- await el.first().fill('');
125
- await page.keyboard.type(foodName, { delay: 50 });
126
- searched = true;
127
- break;
128
- }
129
- } catch {}
130
- }
131
- if (!searched) {
132
- return { success: false, error: 'Could not find food search bar in Add Food dialog' };
133
- }
134
- await page.waitForTimeout(300);
135
-
136
- // Click SEARCH
137
- await clickFirst([
138
- 'text="SEARCH"',
139
- 'button:has-text("SEARCH")',
140
- 'button:has-text("Search")',
141
- ], 'SEARCH button');
142
-
143
- // Wait for results
144
- try {
145
- await page.waitForSelector('td:has-text("' + foodName + '")', { timeout: 8000 });
146
- } catch {
147
- return { success: false, error: 'No food found matching "' + foodName + '"' };
148
- }
149
-
150
- // Select the search result
151
- const resultSelectors = [
152
- 'td:has-text("' + foodName + '")',
153
- 'tr:has-text("' + foodName + '") td',
154
- '.gwt-HTML:has-text("' + foodName + '")',
155
- 'div:has-text("' + foodName + '"):not(:has(input))',
156
- ];
157
- let resultClicked = false;
158
- for (const sel of resultSelectors) {
159
- try {
160
- const el = page.locator(sel);
161
- if (await el.count() > 0) {
162
- await el.first().click();
163
- resultClicked = true;
164
- break;
165
- }
166
- } catch {}
167
- }
168
- if (!resultClicked) {
169
- return { success: false, error: 'No food found matching "' + foodName + '"' };
170
- }
171
- await page.waitForTimeout(200);
172
-
173
- // Wait for the detail panel with serving size
174
- try {
175
- await page.waitForSelector('text="Serving Size"', { timeout: 5000 });
176
- } catch {
177
- return { success: false, error: 'Serving Size panel did not appear for "' + foodName + '"' };
178
- }
179
- await page.waitForTimeout(500);
180
-
181
- // If servings != 1, update the serving size input
182
- if (servingCount !== 1) {
183
- let servingFilled = false;
184
- try {
185
- servingFilled = await page.evaluate((count) => {
186
- const walker = document.createTreeWalker(
187
- document.body,
188
- NodeFilter.SHOW_TEXT,
189
- { acceptNode: (node) =>
190
- node.textContent && node.textContent.trim() === 'Serving Size'
191
- ? NodeFilter.FILTER_ACCEPT
192
- : NodeFilter.FILTER_REJECT
193
- }
194
- );
195
- const textNode = walker.nextNode();
196
- if (!textNode) return false;
197
-
198
- let container = textNode.parentElement;
199
- for (let i = 0; i < 5 && container; i++) {
200
- const input = container.querySelector('input');
201
- if (input) {
202
- input.focus();
203
- input.select();
204
- const nativeSetter = Object.getOwnPropertyDescriptor(
205
- window.HTMLInputElement.prototype, 'value'
206
- ).set;
207
- nativeSetter.call(input, String(count));
208
- input.dispatchEvent(new Event('input', { bubbles: true }));
209
- input.dispatchEvent(new Event('change', { bubbles: true }));
210
- return true;
211
- }
212
- container = container.parentElement;
213
- }
214
- return false;
215
- }, servingCount);
216
- } catch {}
217
-
218
- if (!servingFilled) {
219
- return { success: false, error: 'Could not update serving size for "' + foodName + '"' };
220
- }
221
- await page.waitForTimeout(500);
222
- }
223
-
224
- // Click "ADD TO DIARY"
225
- const addClicked = await clickFirst([
226
- 'button:has-text("ADD TO DIARY")',
227
- 'button:has-text("Add to Diary")',
228
- 'text="ADD TO DIARY"',
229
- 'text="Add to Diary"',
230
- 'button[type="submit"]',
231
- ], 'ADD TO DIARY button');
232
- if (!addClicked) {
233
- return { success: false, error: 'Could not find "Add to Diary" button' };
234
- }
235
- await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 }).catch(() => {});
236
- await page.waitForTimeout(300);
52
+ ${buildFoodDialogCode({ updateServingSize: true })}
237
53
 
238
54
  return { success: true };
239
55
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"log-food.js","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAmB;IAClD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAEvC,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,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,YAAY,GAAG,QAAQ,IAAI,CAAC,CAAC;IAEnC,OAAO;uBACc,QAAQ;wBACP,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;2BACtB,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsNpC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"log-food.js","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAmB;IAClD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAEvC,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,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,YAAY,GAAG,QAAQ,IAAI,CAAC,CAAC;IAEnC,OAAO;uBACc,QAAQ;wBACP,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;2BACtB,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;EA0BrC,mBAAmB,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC;;;GAG/C,CAAC;AACJ,CAAC"}
@@ -7,7 +7,7 @@
7
7
  */
8
8
  /**
9
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.
10
+ * Navigates to /#diary and checks if login UI is still presented.
11
11
  */
12
12
  export declare function buildLoginCheckCode(): string;
13
13
  /**
@@ -1 +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,CAgH7E"}
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAc5C;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAKjD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiH7E"}
@@ -7,15 +7,21 @@
7
7
  */
8
8
  /**
9
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.
10
+ * Navigates to /#diary and checks if login UI is still presented.
11
11
  */
12
12
  export function buildLoginCheckCode() {
13
13
  return `
14
14
  await page.goto('https://cronometer.com/#diary', { waitUntil: 'domcontentloaded', timeout: 15000 });
15
15
  await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
16
+ await page.waitForTimeout(1000);
16
17
  const url = page.url();
17
- const isLoggedIn = url.includes('#diary') && !url.includes('/login') && !url.includes('/signin');
18
- return { success: true, loggedIn: isLoggedIn, url };
18
+ ${buildLoginPresentedCheckCode()}
19
+ const diaryPresented = await page.evaluate(() => {
20
+ return !!document.querySelector('i.diary-date-previous, i.diary-date-next') ||
21
+ /Energy\\s+\\d+\\.?\\d*\\s*kcal/i.test(document.body.innerText);
22
+ }).catch(() => false);
23
+ const isLoggedIn = url.includes('#diary') && !url.includes('/login') && !url.includes('/signin') && !loginPresented && diaryPresented;
24
+ return { success: true, loggedIn: isLoggedIn, url, loginPresented, diaryPresented };
19
25
  `;
20
26
  }
21
27
  /**
@@ -118,7 +124,8 @@ export function buildAutoLoginCode(username, password) {
118
124
  await page.waitForTimeout(500);
119
125
 
120
126
  const url = page.url();
121
- const loggedIn = !url.includes('/login') && !url.includes('/signin');
127
+ ${buildLoginPresentedCheckCode()}
128
+ const loggedIn = !url.includes('/login') && !url.includes('/signin') && !loginPresented;
122
129
 
123
130
  // If still on login page, check for error messages (rate limit, wrong creds, etc.)
124
131
  let loginError = null;
@@ -145,4 +152,27 @@ export function buildAutoLoginCode(username, password) {
145
152
  return { success: true, loggedIn, url, loginError };
146
153
  `;
147
154
  }
155
+ function buildLoginPresentedCheckCode() {
156
+ return `
157
+ const loginPresented = await page.evaluate(() => {
158
+ const visible = (el) => {
159
+ const style = window.getComputedStyle(el);
160
+ return style && style.visibility !== 'hidden' && style.display !== 'none' && el.getClientRects().length > 0;
161
+ };
162
+ const hasVisibleSelector = (selector) =>
163
+ Array.from(document.querySelectorAll(selector)).some((el) => visible(el));
164
+ const hasLoginInput =
165
+ hasVisibleSelector('input[type="email"], input[name="username"], input[name="email"], #email, #username') ||
166
+ hasVisibleSelector('input[type="password"], input[name="password"], #password');
167
+ const hasLoginAction = Array.from(document.querySelectorAll('a, button'))
168
+ .filter((el) => visible(el))
169
+ .some((el) => {
170
+ const text = el.textContent?.replace(/\\s+/g, ' ').trim().toLowerCase() ?? '';
171
+ return ['log in', 'login', 'sign in', 'signin'].includes(text);
172
+ });
173
+
174
+ return hasLoginInput || hasLoginAction;
175
+ }).catch(() => false);
176
+ `;
177
+ }
148
178
  //# sourceMappingURL=login.js.map
@@ -1 +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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDvC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO;;;;;MAKH,4BAA4B,EAAE;;;;;;;GAOjC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAgCpC,4BAA4B,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BjC,CAAC;AACJ,CAAC;AAED,SAAS,4BAA4B;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;GAoBN,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,CA4R3D"}
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;AAG9C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKrD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAiG3D"}
@@ -5,6 +5,7 @@
5
5
  * kernel.browsers.playwright.execute(). The code has access to
6
6
  * `page`, `context`, and `browser` from the Playwright environment.
7
7
  */
8
+ import { buildFoodDialogCode } from "./food-dialog.js";
8
9
  /**
9
10
  * Macro names as they appear in Cronometer's food search.
10
11
  * Each macro is a separate "Quick Add" food item.
@@ -101,203 +102,16 @@ export function buildQuickAddCode(entry) {
101
102
  return false;
102
103
  }
103
104
 
104
- // Helper: right-click an element from a list of selectors
105
- async function rightClickFirst(selectors, description) {
106
- for (const sel of selectors) {
107
- try {
108
- const el = page.locator(sel);
109
- if (await el.count() > 0) {
110
- await el.first().click({ button: 'right', timeout: 5000 });
111
- return true;
112
- }
113
- } catch {}
114
- }
115
- return false;
116
- }
117
-
118
105
  // Add each macro as a separate food entry
119
106
  for (const macro of macros) {
120
- // Right-click meal category with retry (GWT context menus can be flaky)
121
- let menuVisible = false;
122
- for (let attempt = 0; attempt < 3 && !menuVisible; attempt++) {
123
- if (attempt > 0) {
124
- // Dismiss any stale state by pressing Escape and clicking away
125
- await page.keyboard.press('Escape');
126
- await page.mouse.click(1, 1);
127
- await page.waitForTimeout(1000);
128
- }
129
- const clicked = await rightClickFirst([
130
- 'text="' + mealLabel + '"',
131
- ':has-text("' + mealLabel + '")',
132
- ]);
133
- if (!clicked) {
134
- return { success: false, error: 'Could not find meal category "' + mealLabel + '" in diary' };
135
- }
136
- menuVisible = await page.waitForSelector('text="Add Food..."', { timeout: 3000 })
137
- .then(() => true)
138
- .catch(() => page.waitForSelector('text="Add Food"', { timeout: 2000 }).then(() => true).catch(() => false));
139
- }
140
- if (!menuVisible) {
141
- return { success: false, error: 'Context menu did not appear after right-clicking "' + mealLabel + '"' };
142
- }
143
-
144
- // Click "Add Food..." in context menu
145
- const addFoodClicked = await clickFirst([
146
- 'text="Add Food..."',
147
- 'text="Add Food…"',
148
- 'text="Add Food"',
149
- '[role="menuitem"]:has-text("Add Food")',
150
- ]);
151
- if (!addFoodClicked) {
152
- return { success: false, error: 'Could not find "Add Food" in context menu' };
153
- }
154
- await page.waitForTimeout(200);
155
-
156
- // Wait for the "Add Food to Diary" dialog to appear
157
- try {
158
- await page.waitForSelector('text="Add Food to Diary"', { timeout: 5000 });
159
- } catch {
160
- return { success: false, error: 'Add Food to Diary dialog did not appear' };
161
- }
162
- await page.waitForTimeout(300);
163
-
164
- // Click the search bar and type the search term
165
- // GWT apps often need click + keyboard.type instead of fill()
166
- const searchSelectors = [
167
- 'input[placeholder*="Search all foods" i]',
168
- 'input[placeholder*="Search" i]',
169
- 'input[placeholder*="food" i]',
170
- 'input.gwt-TextBox',
171
- 'input[type="text"]',
172
- 'input[type="search"]',
173
- ];
174
- let searched = false;
175
- for (const sel of searchSelectors) {
176
- try {
177
- const el = page.locator(sel);
178
- if (await el.count() > 0) {
179
- await el.first().click();
180
- await page.waitForTimeout(200);
181
- // Clear any existing text, then type via keyboard for GWT compatibility
182
- await el.first().fill('');
183
- await page.keyboard.type(macro.searchName, { delay: 50 });
184
- searched = true;
185
- break;
186
- }
187
- } catch {}
188
- }
189
- if (!searched) {
190
- return { success: false, error: 'Could not find food search bar in Add Food dialog' };
191
- }
192
- await page.waitForTimeout(300);
193
-
194
- // Click the SEARCH button to trigger results
195
- await clickFirst([
196
- 'text="SEARCH"',
197
- 'button:has-text("SEARCH")',
198
- 'button:has-text("Search")',
199
- ]);
200
- const resultsAppeared = await page.waitForSelector('td:has-text("' + macro.searchName + '")', { timeout: 8000 })
201
- .then(() => true).catch(() => false);
202
- if (!resultsAppeared) {
203
- return { success: false, error: 'Search results did not appear for "' + macro.searchName + '"' };
204
- }
205
-
206
- // Select the search result row (not the search input)
207
- // Target table rows/cells containing the macro name
208
- const resultSelectors = [
209
- 'td:has-text("' + macro.searchName + '")',
210
- 'tr:has-text("' + macro.searchName + '") td',
211
- '.gwt-HTML:has-text("' + macro.searchName + '")',
212
- 'div:has-text("' + macro.searchName + '"):not(:has(input))',
213
- ];
214
- let resultClicked = false;
215
- for (const sel of resultSelectors) {
216
- try {
217
- const el = page.locator(sel);
218
- if (await el.count() > 0) {
219
- await el.first().click();
220
- resultClicked = true;
221
- break;
222
- }
223
- } catch {}
224
- }
225
- if (!resultClicked) {
226
- return { success: false, error: 'No search result found for "' + macro.searchName + '"' };
227
- }
228
- await page.waitForTimeout(200);
229
-
230
- // Wait for the detail panel with serving size to appear
231
- try {
232
- await page.waitForSelector('text="Serving Size"', { timeout: 5000 });
233
- } catch {
234
- return { success: false, error: 'Serving Size panel did not appear for "' + macro.name + '"' };
235
- }
236
- await page.waitForTimeout(500);
237
-
238
- // Enter the serving size (grams)
239
- // Find the input by locating the "Serving Size" label's parent and
240
- // then finding the input within it via page.evaluate()
241
- let servingFilled = false;
242
- try {
243
- servingFilled = await page.evaluate((grams) => {
244
- // Walk all elements containing "Serving Size" text
245
- const walker = document.createTreeWalker(
246
- document.body,
247
- NodeFilter.SHOW_TEXT,
248
- { acceptNode: (node) =>
249
- node.textContent && node.textContent.trim() === 'Serving Size'
250
- ? NodeFilter.FILTER_ACCEPT
251
- : NodeFilter.FILTER_REJECT
252
- }
253
- );
254
- const textNode = walker.nextNode();
255
- if (!textNode) return false;
256
-
257
- // Walk up to find a container that also has an input
258
- let container = textNode.parentElement;
259
- for (let i = 0; i < 5 && container; i++) {
260
- const input = container.querySelector('input');
261
- if (input) {
262
- input.focus();
263
- input.select();
264
- // Use native input setter to trigger GWT's change detection
265
- const nativeSetter = Object.getOwnPropertyDescriptor(
266
- window.HTMLInputElement.prototype, 'value'
267
- ).set;
268
- nativeSetter.call(input, String(grams));
269
- input.dispatchEvent(new Event('input', { bubbles: true }));
270
- input.dispatchEvent(new Event('change', { bubbles: true }));
271
- return true;
272
- }
273
- container = container.parentElement;
274
- }
275
- return false;
276
- }, macro.grams);
277
- } catch {}
278
-
279
- if (!servingFilled) {
280
- return { success: false, error: 'Could not find serving size input for "' + macro.name + '"' };
281
- }
282
- await page.waitForTimeout(500);
283
-
284
- // Click "ADD TO DIARY"
285
- const addClicked = await clickFirst([
286
- 'button:has-text("ADD TO DIARY")',
287
- 'button:has-text("Add to Diary")',
288
- 'text="ADD TO DIARY"',
289
- 'text="Add to Diary"',
290
- 'button[type="submit"]',
291
- ]);
292
- if (!addClicked) {
293
- return { success: false, error: 'Could not find "Add to Diary" button for "' + macro.name + '"' };
294
- }
295
- const dialogDismissed = await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 })
296
- .then(() => true).catch(() => false);
297
- if (!dialogDismissed) {
298
- return { success: false, error: '"Add Food to Diary" dialog did not close after adding "' + macro.name + '"' };
299
- }
300
- await page.waitForTimeout(500);
107
+ ${buildFoodDialogCode({
108
+ foodNameVar: "macro.searchName",
109
+ itemNameVar: "macro.name",
110
+ servingCountVar: "macro.grams",
111
+ alwaysUpdateServingSize: true,
112
+ updateServingSize: true,
113
+ verifyDialogDismissed: true,
114
+ })}
301
115
  }
302
116
 
303
117
  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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoPlD,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"quick-add.js","sourceRoot":"","sources":["../../src/kernel/quick-add.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8CnD,mBAAmB,CAAC;QACpB,WAAW,EAAE,kBAAkB;QAC/B,WAAW,EAAE,YAAY;QACzB,eAAe,EAAE,aAAa;QAC9B,uBAAuB,EAAE,IAAI;QAC7B,iBAAiB,EAAE,IAAI;QACvB,qBAAqB,EAAE,IAAI;KAC5B,CAAC;;;;GAIC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Playwright code generator for Cronometer custom recipes scraping.
3
+ *
4
+ * Recipe names live in div.gwt-Label elements inside div[role="button"]
5
+ * rows within the scrollable list on the #custom-recipes page.
6
+ *
7
+ * Returns { success: true, recipes: [{ name }] }
8
+ */
9
+ export declare function buildRecipesCode(): string;
10
+ //# sourceMappingURL=recipes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../../src/kernel/recipes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CA4BzC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Playwright code generator for Cronometer custom recipes scraping.
3
+ *
4
+ * Recipe names live in div.gwt-Label elements inside div[role="button"]
5
+ * rows within the scrollable list on the #custom-recipes page.
6
+ *
7
+ * Returns { success: true, recipes: [{ name }] }
8
+ */
9
+ export function buildRecipesCode() {
10
+ return `
11
+ await page.goto('https://cronometer.com/#custom-recipes', { waitUntil: 'domcontentloaded', timeout: 15000 });
12
+
13
+ const url = page.url();
14
+ if (url.includes('/login') || url.includes('/signin')) {
15
+ return { success: false, error: 'Not logged in. Login may have failed.' };
16
+ }
17
+
18
+ // Wait for the GWT widget to render the recipe list.
19
+ // The search input appears before list items finish rendering, so we wait
20
+ // for an actual recipe row to be present before scraping.
21
+ await page.waitForSelector('input[placeholder*="recipes"]', { timeout: 10000 });
22
+ await page.waitForSelector('div[role="button"] .gwt-Label', { timeout: 10000 }).catch(() => {});
23
+
24
+ const recipes = await page.evaluate(() => {
25
+ // Recipe rows are div[role="button"] elements containing a div.gwt-Label child
26
+ const rows = Array.from(document.querySelectorAll('div[role="button"]'));
27
+ return rows
28
+ .map((row) => {
29
+ const label = row.querySelector('.gwt-Label');
30
+ return label ? { name: label.textContent?.trim() ?? '' } : null;
31
+ })
32
+ .filter((r) => r !== null && r.name.length > 0);
33
+ });
34
+
35
+ return { success: true, recipes };
36
+ `;
37
+ }
38
+ //# sourceMappingURL=recipes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipes.js","sourceRoot":"","sources":["../../src/kernel/recipes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BN,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Local Playwright automation runtime.
3
+ */
4
+ import type { AutomationClient } from "../automation/types.js";
5
+ export declare function getPlaywrightClient(): Promise<AutomationClient>;
6
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/playwright/client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EACV,gBAAgB,EAEjB,MAAM,wBAAwB,CAAC;AAGhC,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAErE"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Local Playwright automation runtime.
3
+ */
4
+ import { mkdirSync } from "fs";
5
+ import { chromium } from "playwright";
6
+ import { createAutomationClient } from "../automation/runner.js";
7
+ import { getPlaywrightProfileDir, loadConfig } from "../config.js";
8
+ export async function getPlaywrightClient() {
9
+ return createAutomationClient(createPlaywrightRuntime);
10
+ }
11
+ async function createPlaywrightRuntime() {
12
+ const context = await createContext();
13
+ const page = context.pages()[0] ?? (await context.newPage());
14
+ return {
15
+ execute: (code) => executeScript(page, context, code),
16
+ close: () => context.close(),
17
+ };
18
+ }
19
+ async function createContext() {
20
+ const config = loadConfig();
21
+ const profileDir = getPlaywrightProfileDir();
22
+ mkdirSync(profileDir, { recursive: true });
23
+ return chromium.launchPersistentContext(profileDir, {
24
+ headless: config.playwrightHeadless ??
25
+ (process.platform === "linux" && !process.env["DISPLAY"]),
26
+ });
27
+ }
28
+ async function executeScript(page, context, code) {
29
+ const browser = context.browser();
30
+ const execute = new Function("page", "context", "browser", `return (async () => { ${code} })();`);
31
+ try {
32
+ return { success: true, result: await execute(page, context, browser) };
33
+ }
34
+ catch (error) {
35
+ return {
36
+ success: false,
37
+ error: error instanceof Error ? error.message : String(error),
38
+ };
39
+ }
40
+ }
41
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/playwright/client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,EAAE,QAAQ,EAAkC,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAKjE,OAAO,EAAE,uBAAuB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAEnE,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,OAAO,sBAAsB,CAAC,uBAAuB,CAAC,CAAC;AACzD,CAAC;AAED,KAAK,UAAU,uBAAuB;IACpC,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAC;IACtC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAE7D,OAAO;QACL,OAAO,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC;QAC7D,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE;KAC7B,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,aAAa;IAC1B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,UAAU,GAAG,uBAAuB,EAAE,CAAC;IAC7C,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE3C,OAAO,QAAQ,CAAC,uBAAuB,CAAC,UAAU,EAAE;QAClD,QAAQ,EACN,MAAM,CAAC,kBAAkB;YACzB,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;KAC5D,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,IAAU,EACV,OAAuB,EACvB,IAAY;IAEZ,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,IAAI,QAAQ,CAC1B,MAAM,EACN,SAAS,EACT,SAAS,EACT,yBAAyB,IAAI,QAAQ,CAKxB,CAAC;IAEhB,IAAI,CAAC;QACH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;IAC1E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;SAC9D,CAAC;IACJ,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milldr/crono",
3
- "version": "0.4.1",
3
+ "version": "0.7.0",
4
4
  "description": "CLI for Cronometer automation via Kernel.sh",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,7 +39,8 @@
39
39
  "@clack/prompts": "^1.0.0",
40
40
  "@napi-rs/keyring": "^1.2.0",
41
41
  "@onkernel/sdk": "^0.33.0",
42
- "commander": "^12.0.0"
42
+ "commander": "^12.0.0",
43
+ "playwright": "^1.60.0"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@types/node": "^20.11.0",