copilot-liku-cli 0.0.1

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 (71) hide show
  1. package/ARCHITECTURE.md +411 -0
  2. package/CONFIGURATION.md +302 -0
  3. package/CONTRIBUTING.md +225 -0
  4. package/ELECTRON_README.md +121 -0
  5. package/INSTALLATION.md +350 -0
  6. package/LICENSE.md +1 -0
  7. package/PROJECT_STATUS.md +229 -0
  8. package/QUICKSTART.md +255 -0
  9. package/README.md +167 -0
  10. package/TESTING.md +274 -0
  11. package/package.json +61 -0
  12. package/scripts/start.js +30 -0
  13. package/src/assets/tray-icon.png +0 -0
  14. package/src/cli/commands/agent.js +327 -0
  15. package/src/cli/commands/click.js +108 -0
  16. package/src/cli/commands/drag.js +85 -0
  17. package/src/cli/commands/find.js +109 -0
  18. package/src/cli/commands/keys.js +132 -0
  19. package/src/cli/commands/mouse.js +79 -0
  20. package/src/cli/commands/repl.js +290 -0
  21. package/src/cli/commands/screenshot.js +72 -0
  22. package/src/cli/commands/scroll.js +74 -0
  23. package/src/cli/commands/start.js +67 -0
  24. package/src/cli/commands/type.js +57 -0
  25. package/src/cli/commands/wait.js +84 -0
  26. package/src/cli/commands/window.js +104 -0
  27. package/src/cli/liku.js +249 -0
  28. package/src/cli/util/output.js +174 -0
  29. package/src/main/agents/base-agent.js +410 -0
  30. package/src/main/agents/builder.js +484 -0
  31. package/src/main/agents/index.js +62 -0
  32. package/src/main/agents/orchestrator.js +362 -0
  33. package/src/main/agents/researcher.js +511 -0
  34. package/src/main/agents/state-manager.js +344 -0
  35. package/src/main/agents/supervisor.js +365 -0
  36. package/src/main/agents/verifier.js +452 -0
  37. package/src/main/ai-service.js +1633 -0
  38. package/src/main/index.js +2208 -0
  39. package/src/main/inspect-service.js +467 -0
  40. package/src/main/system-automation.js +1186 -0
  41. package/src/main/ui-automation/config.js +76 -0
  42. package/src/main/ui-automation/core/helpers.js +41 -0
  43. package/src/main/ui-automation/core/index.js +15 -0
  44. package/src/main/ui-automation/core/powershell.js +82 -0
  45. package/src/main/ui-automation/elements/finder.js +274 -0
  46. package/src/main/ui-automation/elements/index.js +14 -0
  47. package/src/main/ui-automation/elements/wait.js +66 -0
  48. package/src/main/ui-automation/index.js +164 -0
  49. package/src/main/ui-automation/interactions/element-click.js +211 -0
  50. package/src/main/ui-automation/interactions/high-level.js +230 -0
  51. package/src/main/ui-automation/interactions/index.js +47 -0
  52. package/src/main/ui-automation/keyboard/index.js +15 -0
  53. package/src/main/ui-automation/keyboard/input.js +179 -0
  54. package/src/main/ui-automation/mouse/click.js +186 -0
  55. package/src/main/ui-automation/mouse/drag.js +88 -0
  56. package/src/main/ui-automation/mouse/index.js +30 -0
  57. package/src/main/ui-automation/mouse/movement.js +51 -0
  58. package/src/main/ui-automation/mouse/scroll.js +116 -0
  59. package/src/main/ui-automation/screenshot.js +183 -0
  60. package/src/main/ui-automation/window/index.js +23 -0
  61. package/src/main/ui-automation/window/manager.js +305 -0
  62. package/src/main/utils/time.js +62 -0
  63. package/src/main/visual-awareness.js +597 -0
  64. package/src/renderer/chat/chat.js +671 -0
  65. package/src/renderer/chat/index.html +725 -0
  66. package/src/renderer/chat/preload.js +112 -0
  67. package/src/renderer/overlay/index.html +648 -0
  68. package/src/renderer/overlay/overlay.js +782 -0
  69. package/src/renderer/overlay/preload.js +90 -0
  70. package/src/shared/grid-math.js +82 -0
  71. package/src/shared/inspect-types.js +230 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * UI Automation Configuration
3
+ *
4
+ * Central configuration for the UI automation module.
5
+ * @module ui-automation/config
6
+ */
7
+
8
+ const path = require('path');
9
+ const os = require('os');
10
+ const fs = require('fs');
11
+
12
+ // ============================================================================
13
+ // CONFIGURATION
14
+ // ============================================================================
15
+
16
+ const CONFIG = {
17
+ // Default timeouts (ms)
18
+ DEFAULT_TIMEOUT: 10000,
19
+ ELEMENT_WAIT_INTERVAL: 100,
20
+ CLICK_DELAY: 50,
21
+ FOCUS_DELAY: 100,
22
+
23
+ // PowerShell execution
24
+ PS_MAX_BUFFER: 10 * 1024 * 1024,
25
+
26
+ // Temp directory for scripts
27
+ TEMP_DIR: path.join(os.tmpdir(), 'liku-automation'),
28
+
29
+ // Logging
30
+ DEBUG: process.env.LIKU_DEBUG === 'true',
31
+ };
32
+
33
+ // Ensure temp directory exists
34
+ if (!fs.existsSync(CONFIG.TEMP_DIR)) {
35
+ fs.mkdirSync(CONFIG.TEMP_DIR, { recursive: true });
36
+ }
37
+
38
+ // ============================================================================
39
+ // CONTROL TYPES
40
+ // ============================================================================
41
+
42
+ /**
43
+ * Windows UI Automation control type constants
44
+ */
45
+ const CONTROL_TYPES = {
46
+ BUTTON: 'Button',
47
+ CHECKBOX: 'CheckBox',
48
+ COMBOBOX: 'ComboBox',
49
+ EDIT: 'Edit',
50
+ HYPERLINK: 'Hyperlink',
51
+ IMAGE: 'Image',
52
+ LIST: 'List',
53
+ LISTITEM: 'ListItem',
54
+ MENU: 'Menu',
55
+ MENUITEM: 'MenuItem',
56
+ PANE: 'Pane',
57
+ PROGRESSBAR: 'ProgressBar',
58
+ RADIOBUTTON: 'RadioButton',
59
+ SCROLLBAR: 'ScrollBar',
60
+ SLIDER: 'Slider',
61
+ SPINNER: 'Spinner',
62
+ STATUSBAR: 'StatusBar',
63
+ TAB: 'Tab',
64
+ TABITEM: 'TabItem',
65
+ TEXT: 'Text',
66
+ TOOLBAR: 'Toolbar',
67
+ TOOLTIP: 'ToolTip',
68
+ TREE: 'Tree',
69
+ TREEITEM: 'TreeItem',
70
+ WINDOW: 'Window',
71
+ };
72
+
73
+ module.exports = {
74
+ CONFIG,
75
+ CONTROL_TYPES,
76
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Utility Helpers
3
+ *
4
+ * Common utility functions for UI automation.
5
+ * @module ui-automation/core/helpers
6
+ */
7
+
8
+ const { CONFIG } = require('../config');
9
+
10
+ /**
11
+ * Sleep for specified milliseconds
12
+ * @param {number} ms - Milliseconds to sleep
13
+ * @returns {Promise<void>}
14
+ */
15
+ function sleep(ms) {
16
+ return new Promise(resolve => setTimeout(resolve, ms));
17
+ }
18
+
19
+ /**
20
+ * Log debug messages when DEBUG mode is enabled
21
+ * @param {...any} args - Arguments to log
22
+ */
23
+ function debug(...args) {
24
+ if (CONFIG.DEBUG) {
25
+ console.log('[UI-AUTO DEBUG]', ...args);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Log automation actions
31
+ * @param {...any} args - Arguments to log
32
+ */
33
+ function log(...args) {
34
+ console.log('[UI-AUTO]', ...args);
35
+ }
36
+
37
+ module.exports = {
38
+ sleep,
39
+ debug,
40
+ log,
41
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Core utilities for UI automation
3
+ * @module ui-automation/core
4
+ */
5
+
6
+ const { executePowerShellScript, executePowerShell } = require('./powershell');
7
+ const { sleep, debug, log } = require('./helpers');
8
+
9
+ module.exports = {
10
+ executePowerShellScript,
11
+ executePowerShell,
12
+ sleep,
13
+ debug,
14
+ log,
15
+ };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * PowerShell Execution Layer
3
+ *
4
+ * Provides reliable PowerShell script execution for UI automation.
5
+ * @module ui-automation/core/powershell
6
+ */
7
+
8
+ const { exec } = require('child_process');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { CONFIG } = require('../config');
12
+
13
+ /**
14
+ * Execute a PowerShell script from a temp file
15
+ * More reliable than inline commands for complex scripts
16
+ *
17
+ * @param {string} script - PowerShell script content
18
+ * @param {number} [timeout] - Execution timeout in ms
19
+ * @returns {Promise<{stdout: string, stderr: string, error?: string}>}
20
+ */
21
+ async function executePowerShellScript(script, timeout = CONFIG.DEFAULT_TIMEOUT) {
22
+ const scriptPath = path.join(
23
+ CONFIG.TEMP_DIR,
24
+ `script_${Date.now()}_${Math.random().toString(36).slice(2)}.ps1`
25
+ );
26
+
27
+ try {
28
+ fs.writeFileSync(scriptPath, script, 'utf8');
29
+
30
+ return new Promise((resolve) => {
31
+ exec(
32
+ `powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}"`,
33
+ {
34
+ encoding: 'utf8',
35
+ maxBuffer: CONFIG.PS_MAX_BUFFER,
36
+ timeout: timeout,
37
+ },
38
+ (error, stdout, stderr) => {
39
+ // Clean up temp file
40
+ try { fs.unlinkSync(scriptPath); } catch {}
41
+
42
+ if (error) {
43
+ resolve({ stdout: stdout || '', stderr: stderr || '', error: error.message });
44
+ } else {
45
+ resolve({ stdout: stdout || '', stderr: stderr || '' });
46
+ }
47
+ }
48
+ );
49
+ });
50
+ } catch (err) {
51
+ try { fs.unlinkSync(scriptPath); } catch {}
52
+ return { stdout: '', stderr: '', error: err.message };
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Execute a simple PowerShell command inline
58
+ *
59
+ * @param {string} command - PowerShell command
60
+ * @returns {Promise<string>} Command output
61
+ */
62
+ async function executePowerShell(command) {
63
+ return new Promise((resolve, reject) => {
64
+ const psCommand = command.replace(/"/g, '`"');
65
+
66
+ exec(`powershell -NoProfile -Command "${psCommand}"`, {
67
+ encoding: 'utf8',
68
+ maxBuffer: CONFIG.PS_MAX_BUFFER,
69
+ }, (error, stdout, stderr) => {
70
+ if (error) {
71
+ reject(new Error(stderr || error.message));
72
+ } else {
73
+ resolve(stdout.trim());
74
+ }
75
+ });
76
+ });
77
+ }
78
+
79
+ module.exports = {
80
+ executePowerShellScript,
81
+ executePowerShell,
82
+ };
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Element Discovery
3
+ *
4
+ * Find UI elements using Windows UI Automation.
5
+ * @module ui-automation/elements/finder
6
+ */
7
+
8
+ const { CONFIG } = require('../config');
9
+ const { executePowerShellScript } = require('../core/powershell');
10
+ const { debug, log } = require('../core/helpers');
11
+
12
+ /**
13
+ * @typedef {Object} ElementSearchOptions
14
+ * @property {string} [text] - Text/name to search for (partial match)
15
+ * @property {string} [exactText] - Exact text match
16
+ * @property {string} [automationId] - UI Automation AutomationId
17
+ * @property {string} [className] - Element class name
18
+ * @property {string} [controlType] - Control type (Button, Edit, ComboBox, etc.)
19
+ * @property {Object} [bounds] - Bounding constraints {minX, maxX, minY, maxY}
20
+ * @property {boolean} [isEnabled] - Filter by enabled state
21
+ * @property {string} [windowTitle] - Limit search to specific window
22
+ * @property {number} [index] - Select Nth matching element (0-based)
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} UIElement
27
+ * @property {string} Name - Element accessible name
28
+ * @property {string} ControlType - UI Automation control type
29
+ * @property {string} AutomationId - Unique automation identifier
30
+ * @property {string} ClassName - Win32 class name
31
+ * @property {boolean} IsEnabled - Whether element accepts input
32
+ * @property {Object} Bounds - Bounding rectangle {X, Y, Width, Height, CenterX, CenterY}
33
+ * @property {string[]} Patterns - Supported UI Automation patterns
34
+ */
35
+
36
+ /**
37
+ * Find UI elements matching search criteria
38
+ * Uses Windows UI Automation for semantic element discovery
39
+ *
40
+ * @param {ElementSearchOptions} options - Search criteria
41
+ * @returns {Promise<{success: boolean, elements: UIElement[], count: number, error?: string}>}
42
+ */
43
+ async function findElements(options = {}) {
44
+ const {
45
+ text = '',
46
+ exactText = '',
47
+ automationId = '',
48
+ className = '',
49
+ controlType = '',
50
+ bounds = {},
51
+ isEnabled,
52
+ windowTitle = '',
53
+ index,
54
+ } = options;
55
+
56
+ const searchText = exactText || text;
57
+ const isExactMatch = !!exactText;
58
+
59
+ const psScript = `
60
+ Add-Type -AssemblyName UIAutomationClient
61
+ Add-Type -AssemblyName UIAutomationTypes
62
+
63
+ function Find-UIElements {
64
+ param(
65
+ [string]$SearchText = "",
66
+ [bool]$ExactMatch = $false,
67
+ [string]$AutomationId = "",
68
+ [string]$ClassName = "",
69
+ [string]$ControlType = "",
70
+ [string]$WindowTitle = "",
71
+ [int]$MinX = [int]::MinValue,
72
+ [int]$MaxX = [int]::MaxValue,
73
+ [int]$MinY = [int]::MinValue,
74
+ [int]$MaxY = [int]::MaxValue,
75
+ [bool]$RequireEnabled = $false
76
+ )
77
+
78
+ # Get root element or specific window
79
+ $root = $null
80
+ if ($WindowTitle -ne "") {
81
+ $condition = [System.Windows.Automation.PropertyCondition]::new(
82
+ [System.Windows.Automation.AutomationElement]::NameProperty,
83
+ $WindowTitle,
84
+ [System.Windows.Automation.PropertyConditionFlags]::IgnoreCase
85
+ )
86
+ $windows = [System.Windows.Automation.AutomationElement]::RootElement.FindAll(
87
+ [System.Windows.Automation.TreeScope]::Children,
88
+ $condition
89
+ )
90
+ if ($windows.Count -gt 0) {
91
+ $root = $windows[0]
92
+ }
93
+ }
94
+
95
+ if ($root -eq $null) {
96
+ $root = [System.Windows.Automation.AutomationElement]::RootElement
97
+ }
98
+
99
+ # Always search all elements, filter by ControlType in the loop
100
+ $searchCondition = [System.Windows.Automation.Condition]::TrueCondition
101
+
102
+ $elements = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $searchCondition)
103
+
104
+ $results = @()
105
+ foreach ($el in $elements) {
106
+ try {
107
+ $name = $el.Current.Name
108
+ $ctrlType = $el.Current.ControlType.ProgrammaticName
109
+ $autoId = $el.Current.AutomationId
110
+ $cls = $el.Current.ClassName
111
+ $enabled = $el.Current.IsEnabled
112
+ $rect = $el.Current.BoundingRectangle
113
+
114
+ # Skip invisible elements
115
+ if ($rect.Width -le 0 -or $rect.Height -le 0) { continue }
116
+ if ([double]::IsInfinity($rect.X) -or [double]::IsInfinity($rect.Y)) { continue }
117
+
118
+ # Apply filters
119
+ if ($SearchText -ne "") {
120
+ $textMatch = $false
121
+ if ($ExactMatch) {
122
+ $textMatch = ($name -eq $SearchText)
123
+ } else {
124
+ $textMatch = ($name -like "*$SearchText*")
125
+ }
126
+ if (-not $textMatch) { continue }
127
+ }
128
+
129
+ if ($AutomationId -ne "" -and $autoId -notlike "*$AutomationId*") { continue }
130
+ if ($ClassName -ne "" -and $cls -notlike "*$ClassName*") { continue }
131
+ if ($ControlType -ne "" -and $ctrlType -notlike "*$ControlType*") { continue }
132
+ if ($RequireEnabled -and -not $enabled) { continue }
133
+
134
+ # Bounds filter
135
+ $centerX = [int]($rect.X + $rect.Width / 2)
136
+ $centerY = [int]($rect.Y + $rect.Height / 2)
137
+ if ($centerX -lt $MinX -or $centerX -gt $MaxX) { continue }
138
+ if ($centerY -lt $MinY -or $centerY -gt $MaxY) { continue }
139
+
140
+ # Get supported patterns
141
+ $patterns = @()
142
+ try {
143
+ $supportedPatterns = $el.GetSupportedPatterns()
144
+ foreach ($p in $supportedPatterns) {
145
+ $patterns += $p.ProgrammaticName
146
+ }
147
+ } catch {}
148
+
149
+ $results += @{
150
+ Name = $name
151
+ ControlType = $ctrlType.Replace("ControlType.", "")
152
+ AutomationId = $autoId
153
+ ClassName = $cls
154
+ IsEnabled = $enabled
155
+ Bounds = @{
156
+ X = [int]$rect.X
157
+ Y = [int]$rect.Y
158
+ Width = [int]$rect.Width
159
+ Height = [int]$rect.Height
160
+ CenterX = $centerX
161
+ CenterY = $centerY
162
+ }
163
+ Patterns = $patterns
164
+ }
165
+ } catch {}
166
+ }
167
+
168
+ return $results
169
+ }
170
+
171
+ $results = Find-UIElements \`
172
+ -SearchText "${searchText.replace(/"/g, '`"')}" \`
173
+ -ExactMatch $${isExactMatch} \`
174
+ -AutomationId "${automationId}" \`
175
+ -ClassName "${className}" \`
176
+ -ControlType "${controlType}" \`
177
+ -WindowTitle "${windowTitle.replace(/"/g, '`"')}" \`
178
+ ${bounds.minX !== undefined ? `-MinX ${bounds.minX}` : ''} \`
179
+ ${bounds.maxX !== undefined ? `-MaxX ${bounds.maxX}` : ''} \`
180
+ ${bounds.minY !== undefined ? `-MinY ${bounds.minY}` : ''} \`
181
+ ${bounds.maxY !== undefined ? `-MaxY ${bounds.maxY}` : ''} \`
182
+ -RequireEnabled $${isEnabled === true}
183
+
184
+ $results | ConvertTo-Json -Depth 5 -Compress
185
+ `;
186
+
187
+ const result = await executePowerShellScript(psScript, 30000);
188
+
189
+ debug('PowerShell stdout:', result.stdout?.substring(0, 500));
190
+ debug('PowerShell stderr:', result.stderr);
191
+ debug('PowerShell error:', result.error);
192
+
193
+ if (result.error) {
194
+ return { success: false, elements: [], count: 0, error: result.error };
195
+ }
196
+
197
+ try {
198
+ // Handle empty results
199
+ const output = (result.stdout || '').trim();
200
+ if (!output) {
201
+ return { success: true, elements: [], count: 0, element: null };
202
+ }
203
+
204
+ let rawElements = JSON.parse(output);
205
+ if (!Array.isArray(rawElements)) {
206
+ rawElements = rawElements ? [rawElements] : [];
207
+ }
208
+
209
+ // Normalize element structure to camelCase
210
+ let elements = rawElements.map(e => ({
211
+ name: e.Name,
212
+ controlType: e.ControlType,
213
+ automationId: e.AutomationId,
214
+ className: e.ClassName,
215
+ isEnabled: e.IsEnabled,
216
+ patterns: e.Patterns,
217
+ bounds: e.Bounds ? {
218
+ x: e.Bounds.X,
219
+ y: e.Bounds.Y,
220
+ width: e.Bounds.Width,
221
+ height: e.Bounds.Height,
222
+ centerX: e.Bounds.CenterX,
223
+ centerY: e.Bounds.CenterY,
224
+ } : null,
225
+ // Keep original PascalCase for backward compatibility
226
+ Name: e.Name,
227
+ ControlType: e.ControlType,
228
+ AutomationId: e.AutomationId,
229
+ ClassName: e.ClassName,
230
+ IsEnabled: e.IsEnabled,
231
+ Patterns: e.Patterns,
232
+ Bounds: e.Bounds,
233
+ }));
234
+
235
+ // Apply index filter if specified
236
+ if (typeof index === 'number' && index >= 0 && index < elements.length) {
237
+ elements = [elements[index]];
238
+ }
239
+
240
+ log(`Found ${elements.length} element(s) matching criteria`);
241
+ debug('Search options:', options);
242
+ debug('Results:', elements.map(e => `${e.name} (${e.controlType})`));
243
+
244
+ return {
245
+ success: true,
246
+ elements,
247
+ count: elements.length,
248
+ element: elements[0] || null,
249
+ };
250
+ } catch (e) {
251
+ return { success: false, elements: [], count: 0, error: `Parse error: ${e.message}`, raw: result.stdout };
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Find a single element matching criteria
257
+ * Convenience wrapper around findElements
258
+ *
259
+ * @param {ElementSearchOptions} options - Search criteria
260
+ * @returns {Promise<{success: boolean, element: UIElement|null, error?: string}>}
261
+ */
262
+ async function findElement(options = {}) {
263
+ const result = await findElements({ ...options, index: 0 });
264
+ return {
265
+ success: result.success && result.element !== null,
266
+ element: result.element,
267
+ error: result.element ? undefined : result.error || 'Element not found',
268
+ };
269
+ }
270
+
271
+ module.exports = {
272
+ findElements,
273
+ findElement,
274
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Element Discovery and Wait Utilities
3
+ * @module ui-automation/elements
4
+ */
5
+
6
+ const { findElements, findElement } = require('./finder');
7
+ const { waitForElement, waitForElementGone } = require('./wait');
8
+
9
+ module.exports = {
10
+ findElements,
11
+ findElement,
12
+ waitForElement,
13
+ waitForElementGone,
14
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Element Wait Utilities
3
+ *
4
+ * Wait for elements to appear or disappear.
5
+ * @module ui-automation/elements/wait
6
+ */
7
+
8
+ const { CONFIG } = require('../config');
9
+ const { sleep } = require('../core/helpers');
10
+ const { findElement } = require('./finder');
11
+
12
+ /**
13
+ * Wait for an element to appear
14
+ *
15
+ * @param {Object} options - Search criteria (same as findElement)
16
+ * @param {number} [timeout=10000] - Maximum wait time in ms
17
+ * @returns {Promise<{success: boolean, element: Object|null, elapsed: number, error?: string}>}
18
+ */
19
+ async function waitForElement(options = {}, timeout = CONFIG.DEFAULT_TIMEOUT) {
20
+ const startTime = Date.now();
21
+
22
+ while (Date.now() - startTime < timeout) {
23
+ const result = await findElement(options);
24
+ if (result.success && result.element) {
25
+ return {
26
+ success: true,
27
+ element: result.element,
28
+ elapsed: Date.now() - startTime,
29
+ };
30
+ }
31
+ await sleep(CONFIG.ELEMENT_WAIT_INTERVAL);
32
+ }
33
+
34
+ return {
35
+ success: false,
36
+ element: null,
37
+ elapsed: Date.now() - startTime,
38
+ error: `Element not found within ${timeout}ms`,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Wait for an element to disappear
44
+ *
45
+ * @param {Object} options - Search criteria (same as findElement)
46
+ * @param {number} [timeout=10000] - Maximum wait time in ms
47
+ * @returns {Promise<{success: boolean, elapsed: number}>}
48
+ */
49
+ async function waitForElementGone(options = {}, timeout = CONFIG.DEFAULT_TIMEOUT) {
50
+ const startTime = Date.now();
51
+
52
+ while (Date.now() - startTime < timeout) {
53
+ const result = await findElement(options);
54
+ if (!result.success || !result.element) {
55
+ return { success: true, elapsed: Date.now() - startTime };
56
+ }
57
+ await sleep(CONFIG.ELEMENT_WAIT_INTERVAL);
58
+ }
59
+
60
+ return { success: false, elapsed: Date.now() - startTime };
61
+ }
62
+
63
+ module.exports = {
64
+ waitForElement,
65
+ waitForElementGone,
66
+ };