@milldr/crono 0.1.0 → 0.2.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 (50) hide show
  1. package/README.md +128 -0
  2. package/dist/commands/add.d.ts +9 -0
  3. package/dist/commands/add.d.ts.map +1 -0
  4. package/dist/commands/add.js +65 -0
  5. package/dist/commands/add.js.map +1 -0
  6. package/dist/commands/export.d.ts +8 -0
  7. package/dist/commands/export.d.ts.map +1 -0
  8. package/dist/commands/export.js +142 -0
  9. package/dist/commands/export.js.map +1 -0
  10. package/dist/commands/log.d.ts +6 -0
  11. package/dist/commands/log.d.ts.map +1 -0
  12. package/dist/commands/log.js +40 -0
  13. package/dist/commands/log.js.map +1 -0
  14. package/dist/config.d.ts +2 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/cronometer/auth.d.ts +31 -0
  18. package/dist/cronometer/auth.d.ts.map +1 -0
  19. package/dist/cronometer/auth.js +151 -0
  20. package/dist/cronometer/auth.js.map +1 -0
  21. package/dist/cronometer/export.d.ts +22 -0
  22. package/dist/cronometer/export.d.ts.map +1 -0
  23. package/dist/cronometer/export.js +83 -0
  24. package/dist/cronometer/export.js.map +1 -0
  25. package/dist/cronometer/parse.d.ts +35 -0
  26. package/dist/cronometer/parse.d.ts.map +1 -0
  27. package/dist/cronometer/parse.js +158 -0
  28. package/dist/cronometer/parse.js.map +1 -0
  29. package/dist/index.js +33 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/kernel/add-custom-food.d.ts +22 -0
  32. package/dist/kernel/add-custom-food.d.ts.map +1 -0
  33. package/dist/kernel/add-custom-food.js +314 -0
  34. package/dist/kernel/add-custom-food.js.map +1 -0
  35. package/dist/kernel/client.d.ts +15 -0
  36. package/dist/kernel/client.d.ts.map +1 -1
  37. package/dist/kernel/client.js +92 -1
  38. package/dist/kernel/client.js.map +1 -1
  39. package/dist/kernel/log-food.d.ts +17 -0
  40. package/dist/kernel/log-food.d.ts.map +1 -0
  41. package/dist/kernel/log-food.js +230 -0
  42. package/dist/kernel/log-food.js.map +1 -0
  43. package/dist/kernel/login.d.ts.map +1 -1
  44. package/dist/kernel/login.js +24 -1
  45. package/dist/kernel/login.js.map +1 -1
  46. package/package.json +5 -1
  47. package/dist/debug-nav.d.ts +0 -2
  48. package/dist/debug-nav.d.ts.map +0 -1
  49. package/dist/debug-nav.js +0 -99
  50. package/dist/debug-nav.js.map +0 -1
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Playwright code generator for Cronometer food logging.
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 logging a food to the Cronometer diary.
10
+ *
11
+ * Flow:
12
+ * navigate to #diary → right-click meal → "Add Food" → search food name →
13
+ * select result → set servings → "Add to Diary"
14
+ */
15
+ export function buildLogFoodCode(entry) {
16
+ const { name, meal, servings } = entry;
17
+ const mealLabel = meal
18
+ ? meal.charAt(0).toUpperCase() + meal.slice(1).toLowerCase()
19
+ : "Uncategorized";
20
+ const foodName = JSON.stringify(name);
21
+ const servingCount = servings ?? 1;
22
+ return `
23
+ const foodName = ${foodName};
24
+ const mealLabel = ${JSON.stringify(mealLabel)};
25
+ const servingCount = ${servingCount};
26
+
27
+ // Navigate to diary
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
+ // Helper: find and click an element from a list of selectors
38
+ async function clickFirst(selectors, description) {
39
+ for (const sel of selectors) {
40
+ try {
41
+ const el = page.locator(sel);
42
+ if (await el.count() > 0) {
43
+ await el.first().click();
44
+ return true;
45
+ }
46
+ } catch {}
47
+ }
48
+ return false;
49
+ }
50
+
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 the meal category
66
+ const clicked = await rightClickFirst([
67
+ 'text="' + mealLabel + '"',
68
+ ':has-text("' + mealLabel + '")',
69
+ ], 'meal category');
70
+ if (!clicked) {
71
+ return { success: false, error: 'Could not find meal category "' + mealLabel + '" in diary' };
72
+ }
73
+ await page.waitForSelector('text="Add Food..."', { timeout: 3000 }).catch(() =>
74
+ page.waitForSelector('text="Add Food"', { timeout: 2000 }).catch(() => {})
75
+ );
76
+
77
+ // Click "Add Food..." in context menu
78
+ const addFoodClicked = await clickFirst([
79
+ 'text="Add Food..."',
80
+ 'text="Add Food\u2026"',
81
+ 'text="Add Food"',
82
+ '[role="menuitem"]:has-text("Add Food")',
83
+ ], 'Add Food menu item');
84
+ if (!addFoodClicked) {
85
+ return { success: false, error: 'Could not find "Add Food" in context menu' };
86
+ }
87
+ await page.waitForTimeout(200);
88
+
89
+ // Wait for "Add Food to Diary" dialog
90
+ try {
91
+ await page.waitForSelector('text="Add Food to Diary"', { timeout: 5000 });
92
+ } catch {
93
+ return { success: false, error: 'Add Food to Diary dialog did not appear' };
94
+ }
95
+ await page.waitForTimeout(300);
96
+
97
+ // Search for the food
98
+ const searchSelectors = [
99
+ 'input[placeholder*="Search all foods" i]',
100
+ 'input[placeholder*="Search" i]',
101
+ 'input[placeholder*="food" i]',
102
+ 'input.gwt-TextBox',
103
+ 'input[type="text"]',
104
+ 'input[type="search"]',
105
+ ];
106
+ let searched = false;
107
+ for (const sel of searchSelectors) {
108
+ try {
109
+ const el = page.locator(sel);
110
+ if (await el.count() > 0) {
111
+ await el.first().click();
112
+ await page.waitForTimeout(200);
113
+ await el.first().fill('');
114
+ await page.keyboard.type(foodName, { delay: 50 });
115
+ searched = true;
116
+ break;
117
+ }
118
+ } catch {}
119
+ }
120
+ if (!searched) {
121
+ return { success: false, error: 'Could not find food search bar in Add Food dialog' };
122
+ }
123
+ await page.waitForTimeout(300);
124
+
125
+ // Click SEARCH
126
+ await clickFirst([
127
+ 'text="SEARCH"',
128
+ 'button:has-text("SEARCH")',
129
+ 'button:has-text("Search")',
130
+ ], 'SEARCH button');
131
+
132
+ // Wait for results
133
+ try {
134
+ await page.waitForSelector('td:has-text("' + foodName + '")', { timeout: 8000 });
135
+ } catch {
136
+ return { success: false, error: 'No food found matching "' + foodName + '"' };
137
+ }
138
+
139
+ // Select the search result
140
+ const resultSelectors = [
141
+ 'td:has-text("' + foodName + '")',
142
+ 'tr:has-text("' + foodName + '") td',
143
+ '.gwt-HTML:has-text("' + foodName + '")',
144
+ 'div:has-text("' + foodName + '"):not(:has(input))',
145
+ ];
146
+ let resultClicked = false;
147
+ for (const sel of resultSelectors) {
148
+ try {
149
+ const el = page.locator(sel);
150
+ if (await el.count() > 0) {
151
+ await el.first().click();
152
+ resultClicked = true;
153
+ break;
154
+ }
155
+ } catch {}
156
+ }
157
+ if (!resultClicked) {
158
+ return { success: false, error: 'No food found matching "' + foodName + '"' };
159
+ }
160
+ await page.waitForTimeout(200);
161
+
162
+ // Wait for the detail panel with serving size
163
+ try {
164
+ await page.waitForSelector('text="Serving Size"', { timeout: 5000 });
165
+ } catch {
166
+ return { success: false, error: 'Serving Size panel did not appear for "' + foodName + '"' };
167
+ }
168
+ await page.waitForTimeout(500);
169
+
170
+ // If servings != 1, update the serving size input
171
+ if (servingCount !== 1) {
172
+ let servingFilled = false;
173
+ try {
174
+ servingFilled = await page.evaluate((count) => {
175
+ const walker = document.createTreeWalker(
176
+ document.body,
177
+ NodeFilter.SHOW_TEXT,
178
+ { acceptNode: (node) =>
179
+ node.textContent && node.textContent.trim() === 'Serving Size'
180
+ ? NodeFilter.FILTER_ACCEPT
181
+ : NodeFilter.FILTER_REJECT
182
+ }
183
+ );
184
+ const textNode = walker.nextNode();
185
+ if (!textNode) return false;
186
+
187
+ let container = textNode.parentElement;
188
+ for (let i = 0; i < 5 && container; i++) {
189
+ const input = container.querySelector('input');
190
+ if (input) {
191
+ input.focus();
192
+ input.select();
193
+ const nativeSetter = Object.getOwnPropertyDescriptor(
194
+ window.HTMLInputElement.prototype, 'value'
195
+ ).set;
196
+ nativeSetter.call(input, String(count));
197
+ input.dispatchEvent(new Event('input', { bubbles: true }));
198
+ input.dispatchEvent(new Event('change', { bubbles: true }));
199
+ return true;
200
+ }
201
+ container = container.parentElement;
202
+ }
203
+ return false;
204
+ }, servingCount);
205
+ } catch {}
206
+
207
+ if (!servingFilled) {
208
+ return { success: false, error: 'Could not update serving size for "' + foodName + '"' };
209
+ }
210
+ await page.waitForTimeout(500);
211
+ }
212
+
213
+ // Click "ADD TO DIARY"
214
+ const addClicked = await clickFirst([
215
+ 'button:has-text("ADD TO DIARY")',
216
+ 'button:has-text("Add to Diary")',
217
+ 'text="ADD TO DIARY"',
218
+ 'text="Add to Diary"',
219
+ 'button[type="submit"]',
220
+ ], 'ADD TO DIARY button');
221
+ if (!addClicked) {
222
+ return { success: false, error: 'Could not find "Add to Diary" button' };
223
+ }
224
+ await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 }).catch(() => {});
225
+ await page.waitForTimeout(300);
226
+
227
+ return { success: true };
228
+ `;
229
+ }
230
+ //# sourceMappingURL=log-food.js.map
@@ -0,0 +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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2MpC,CAAC;AACJ,CAAC"}
@@ -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,CAyF7E"}
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"}
@@ -119,7 +119,30 @@ export function buildAutoLoginCode(username, password) {
119
119
 
120
120
  const url = page.url();
121
121
  const loggedIn = !url.includes('/login') && !url.includes('/signin');
122
- return { success: true, loggedIn, url };
122
+
123
+ // If still on login page, check for error messages (rate limit, wrong creds, etc.)
124
+ let loginError = null;
125
+ if (!loggedIn) {
126
+ loginError = await page.evaluate(() => {
127
+ const selectors = [
128
+ '.error-message', '.alert', '.notification',
129
+ '[class*="error"]', '[class*="alert"]',
130
+ '.gwt-HTML',
131
+ ];
132
+ for (const sel of selectors) {
133
+ const els = document.querySelectorAll(sel);
134
+ for (const el of els) {
135
+ const text = el.textContent?.trim();
136
+ if (text && text.length > 5 && text.length < 300 && el.offsetParent !== null) {
137
+ return text;
138
+ }
139
+ }
140
+ }
141
+ return null;
142
+ });
143
+ }
144
+
145
+ return { success: true, loggedIn, url, loginError };
123
146
  `;
124
147
  }
125
148
  //# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCvC,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;;;;;;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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milldr/crono",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for Cronometer automation via Kernel.sh",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,10 @@
26
26
  "automation",
27
27
  "kernel"
28
28
  ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/milldr/crono"
32
+ },
29
33
  "author": "Daniel Miller",
30
34
  "license": "MIT",
31
35
  "engines": {
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=debug-nav.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"debug-nav.d.ts","sourceRoot":"","sources":["../src/debug-nav.ts"],"names":[],"mappings":""}
package/dist/debug-nav.js DELETED
@@ -1,99 +0,0 @@
1
- /**
2
- * Debug script: dump date navigation elements from Cronometer diary.
3
- * Run with: npx tsx src/debug-nav.ts
4
- */
5
- import Kernel from "@onkernel/sdk";
6
- import { getCredential } from "./credentials.js";
7
- import { buildAutoLoginCode } from "./kernel/login.js";
8
- async function main() {
9
- const apiKey = process.env["KERNEL_API_KEY"] ?? getCredential("kernel-api-key");
10
- process.env["KERNEL_API_KEY"] = apiKey;
11
- const kernel = new Kernel();
12
- const browser = await kernel.browsers.create({
13
- headless: true,
14
- stealth: true,
15
- timeout_seconds: 120,
16
- });
17
- try {
18
- const username = getCredential("cronometer-username");
19
- const password = getCredential("cronometer-password");
20
- // Login
21
- await kernel.browsers.playwright.execute(browser.session_id, {
22
- code: buildAutoLoginCode(username, password),
23
- timeout_sec: 60,
24
- });
25
- // Navigate to diary and dump nav info
26
- const result = await kernel.browsers.playwright.execute(browser.session_id, {
27
- code: `
28
- await page.goto('https://cronometer.com/#diary', { waitUntil: 'domcontentloaded', timeout: 15000 });
29
- await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
30
- await page.waitForTimeout(3000);
31
-
32
- const debug = await page.evaluate(() => {
33
- // 1. All images and their src attributes
34
- const imgs = Array.from(document.querySelectorAll('img')).map(img => ({
35
- src: img.getAttribute('src'),
36
- alt: img.getAttribute('alt'),
37
- visible: img.offsetParent !== null,
38
- width: img.offsetWidth,
39
- height: img.offsetHeight,
40
- classes: img.className,
41
- }));
42
-
43
- // 2. All buttons/clickable elements near the top of the page
44
- const buttons = Array.from(document.querySelectorAll('button, [role="button"], .gwt-PushButton')).map(el => ({
45
- text: (el.textContent || '').trim().substring(0, 80),
46
- classes: el.className,
47
- visible: el.offsetParent !== null,
48
- tag: el.tagName,
49
- }));
50
-
51
- // 3. Elements that might be date-related
52
- const dateEls = Array.from(document.querySelectorAll('[class*="date" i], [class*="calendar" i], [class*="nav" i], [class*="arrow" i], [class*="picker" i]')).map(el => ({
53
- tag: el.tagName,
54
- classes: el.className,
55
- text: (el.textContent || '').trim().substring(0, 80),
56
- visible: el.offsetParent !== null,
57
- html: el.outerHTML.substring(0, 200),
58
- }));
59
-
60
- // 4. Look for any element containing a date-like text (month name)
61
- const dateTexts = [];
62
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
63
- let node;
64
- while (node = walker.nextNode()) {
65
- const t = (node.textContent || '').trim();
66
- if (/(?:January|February|March|April|May|June|July|August|September|October|November|December)/i.test(t)) {
67
- dateTexts.push({
68
- text: t.substring(0, 100),
69
- parentTag: node.parentElement?.tagName,
70
- parentClasses: node.parentElement?.className,
71
- });
72
- }
73
- }
74
-
75
- // 5. GWT PushButton elements (Cronometer uses GWT)
76
- const pushButtons = Array.from(document.querySelectorAll('.gwt-PushButton, .gwt-PushButton-up, .gwt-PushButton-down')).map(el => ({
77
- classes: el.className,
78
- html: el.outerHTML.substring(0, 300),
79
- visible: el.offsetParent !== null,
80
- }));
81
-
82
- return { imgs, buttons, dateEls, dateTexts, pushButtons };
83
- });
84
-
85
- return debug;
86
- `,
87
- timeout_sec: 60,
88
- });
89
- console.log(JSON.stringify(result.result, null, 2));
90
- }
91
- finally {
92
- try {
93
- await kernel.browsers.deleteByID(browser.session_id);
94
- }
95
- catch { }
96
- }
97
- }
98
- main().catch(console.error);
99
- //# sourceMappingURL=debug-nav.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"debug-nav.js","sourceRoot":"","sources":["../src/debug-nav.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,MAAM,MAAM,eAAe,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GACV,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACnE,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,GAAG,MAAO,CAAC;IAExC,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;IAC5B,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC3C,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,IAAI;QACb,eAAe,EAAE,GAAG;KACrB,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,aAAa,CAAC,qBAAqB,CAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,aAAa,CAAC,qBAAqB,CAAE,CAAC;QAEvD,QAAQ;QACR,MAAM,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE;YAC3D,IAAI,EAAE,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC;YAC5C,WAAW,EAAE,EAAE;SAChB,CAAC,CAAC;QAEH,sCAAsC;QACtC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CACrD,OAAO,CAAC,UAAU,EAClB;YACE,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SA2DL;YACD,WAAW,EAAE,EAAE;SAChB,CACF,CAAC;QAEF,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACtD,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACZ,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC"}