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.
- package/ARCHITECTURE.md +411 -0
- package/CONFIGURATION.md +302 -0
- package/CONTRIBUTING.md +225 -0
- package/ELECTRON_README.md +121 -0
- package/INSTALLATION.md +350 -0
- package/LICENSE.md +1 -0
- package/PROJECT_STATUS.md +229 -0
- package/QUICKSTART.md +255 -0
- package/README.md +167 -0
- package/TESTING.md +274 -0
- package/package.json +61 -0
- package/scripts/start.js +30 -0
- package/src/assets/tray-icon.png +0 -0
- package/src/cli/commands/agent.js +327 -0
- package/src/cli/commands/click.js +108 -0
- package/src/cli/commands/drag.js +85 -0
- package/src/cli/commands/find.js +109 -0
- package/src/cli/commands/keys.js +132 -0
- package/src/cli/commands/mouse.js +79 -0
- package/src/cli/commands/repl.js +290 -0
- package/src/cli/commands/screenshot.js +72 -0
- package/src/cli/commands/scroll.js +74 -0
- package/src/cli/commands/start.js +67 -0
- package/src/cli/commands/type.js +57 -0
- package/src/cli/commands/wait.js +84 -0
- package/src/cli/commands/window.js +104 -0
- package/src/cli/liku.js +249 -0
- package/src/cli/util/output.js +174 -0
- package/src/main/agents/base-agent.js +410 -0
- package/src/main/agents/builder.js +484 -0
- package/src/main/agents/index.js +62 -0
- package/src/main/agents/orchestrator.js +362 -0
- package/src/main/agents/researcher.js +511 -0
- package/src/main/agents/state-manager.js +344 -0
- package/src/main/agents/supervisor.js +365 -0
- package/src/main/agents/verifier.js +452 -0
- package/src/main/ai-service.js +1633 -0
- package/src/main/index.js +2208 -0
- package/src/main/inspect-service.js +467 -0
- package/src/main/system-automation.js +1186 -0
- package/src/main/ui-automation/config.js +76 -0
- package/src/main/ui-automation/core/helpers.js +41 -0
- package/src/main/ui-automation/core/index.js +15 -0
- package/src/main/ui-automation/core/powershell.js +82 -0
- package/src/main/ui-automation/elements/finder.js +274 -0
- package/src/main/ui-automation/elements/index.js +14 -0
- package/src/main/ui-automation/elements/wait.js +66 -0
- package/src/main/ui-automation/index.js +164 -0
- package/src/main/ui-automation/interactions/element-click.js +211 -0
- package/src/main/ui-automation/interactions/high-level.js +230 -0
- package/src/main/ui-automation/interactions/index.js +47 -0
- package/src/main/ui-automation/keyboard/index.js +15 -0
- package/src/main/ui-automation/keyboard/input.js +179 -0
- package/src/main/ui-automation/mouse/click.js +186 -0
- package/src/main/ui-automation/mouse/drag.js +88 -0
- package/src/main/ui-automation/mouse/index.js +30 -0
- package/src/main/ui-automation/mouse/movement.js +51 -0
- package/src/main/ui-automation/mouse/scroll.js +116 -0
- package/src/main/ui-automation/screenshot.js +183 -0
- package/src/main/ui-automation/window/index.js +23 -0
- package/src/main/ui-automation/window/manager.js +305 -0
- package/src/main/utils/time.js +62 -0
- package/src/main/visual-awareness.js +597 -0
- package/src/renderer/chat/chat.js +671 -0
- package/src/renderer/chat/index.html +725 -0
- package/src/renderer/chat/preload.js +112 -0
- package/src/renderer/overlay/index.html +648 -0
- package/src/renderer/overlay/overlay.js +782 -0
- package/src/renderer/overlay/preload.js +90 -0
- package/src/shared/grid-math.js +82 -0
- package/src/shared/inspect-types.js +230 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual Awareness Module
|
|
3
|
+
* Advanced screen analysis, OCR, element detection, and active window tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { exec } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
// ===== STATE =====
|
|
12
|
+
let previousScreenshot = null;
|
|
13
|
+
let screenDiffHistory = [];
|
|
14
|
+
let activeWindowInfo = null;
|
|
15
|
+
let ocrCache = new Map();
|
|
16
|
+
let elementCache = new Map();
|
|
17
|
+
|
|
18
|
+
const MAX_DIFF_HISTORY = 10;
|
|
19
|
+
const DIFF_THRESHOLD = 0.05; // 5% change threshold
|
|
20
|
+
|
|
21
|
+
// ===== POWERSHELL HELPER =====
|
|
22
|
+
// BLOCKER-2 FIX: Write scripts to temp files instead of inline commands
|
|
23
|
+
// This preserves Here-String syntax which requires newlines
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execute a PowerShell script by writing to a temp file
|
|
27
|
+
* This fixes the Here-String (@" ... "@) syntax issue
|
|
28
|
+
* @param {string} script - PowerShell script content
|
|
29
|
+
* @param {number} timeout - Execution timeout in ms
|
|
30
|
+
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
31
|
+
*/
|
|
32
|
+
function executePowerShellScript(script, timeout = 10000) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const tempDir = path.join(os.tmpdir(), 'liku-ps');
|
|
35
|
+
if (!fs.existsSync(tempDir)) {
|
|
36
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const scriptPath = path.join(tempDir, `script-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.ps1`);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
fs.writeFileSync(scriptPath, script, 'utf8');
|
|
43
|
+
} catch (err) {
|
|
44
|
+
resolve({ error: `Failed to write temp script: ${err.message}` });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Execute with -File to avoid quote escaping issues
|
|
49
|
+
exec(`powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}"`,
|
|
50
|
+
{ timeout },
|
|
51
|
+
(error, stdout, stderr) => {
|
|
52
|
+
// Clean up temp file
|
|
53
|
+
try { fs.unlinkSync(scriptPath); } catch (e) {}
|
|
54
|
+
|
|
55
|
+
if (error) {
|
|
56
|
+
resolve({ error: error.message, stderr });
|
|
57
|
+
} else {
|
|
58
|
+
resolve({ stdout: stdout.trim(), stderr });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ===== SCREEN DIFFING =====
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compare two screenshots and detect changes
|
|
69
|
+
* Returns regions that have changed significantly
|
|
70
|
+
*/
|
|
71
|
+
function compareScreenshots(current, previous) {
|
|
72
|
+
if (!previous || !current) return null;
|
|
73
|
+
|
|
74
|
+
// Both should be base64 data URLs
|
|
75
|
+
// For actual pixel comparison, we'd use a canvas-based approach
|
|
76
|
+
// Here we provide a simplified version that can be enhanced
|
|
77
|
+
|
|
78
|
+
const currentData = current.dataURL;
|
|
79
|
+
const previousData = previous.dataURL;
|
|
80
|
+
|
|
81
|
+
// Simple comparison: if the base64 differs significantly
|
|
82
|
+
if (currentData === previousData) {
|
|
83
|
+
return { changed: false, changePercent: 0, regions: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Calculate approximate change based on string difference
|
|
87
|
+
// This is a heuristic; real implementation would use pixel comparison
|
|
88
|
+
const lenDiff = Math.abs(currentData.length - previousData.length);
|
|
89
|
+
const avgLen = (currentData.length + previousData.length) / 2;
|
|
90
|
+
const changePercent = lenDiff / avgLen;
|
|
91
|
+
|
|
92
|
+
const changed = changePercent > DIFF_THRESHOLD;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
changed,
|
|
96
|
+
changePercent: Math.min(changePercent * 100, 100),
|
|
97
|
+
timestamp: Date.now(),
|
|
98
|
+
regions: changed ? detectChangedRegions(current, previous) : []
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Detect which regions of the screen changed
|
|
104
|
+
* This is a simplified version - real implementation would use image processing
|
|
105
|
+
*/
|
|
106
|
+
function detectChangedRegions(current, previous) {
|
|
107
|
+
// Placeholder for region detection
|
|
108
|
+
// In a real implementation, this would:
|
|
109
|
+
// 1. Divide screen into grid
|
|
110
|
+
// 2. Compare each cell
|
|
111
|
+
// 3. Return list of changed regions with coordinates
|
|
112
|
+
|
|
113
|
+
return [{
|
|
114
|
+
x: 0, y: 0,
|
|
115
|
+
width: current.width,
|
|
116
|
+
height: current.height,
|
|
117
|
+
type: 'full-screen-change'
|
|
118
|
+
}];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Store current screenshot and return diff from previous
|
|
123
|
+
*/
|
|
124
|
+
function trackScreenChange(screenshot) {
|
|
125
|
+
const diff = compareScreenshots(screenshot, previousScreenshot);
|
|
126
|
+
|
|
127
|
+
if (diff && diff.changed) {
|
|
128
|
+
screenDiffHistory.push({
|
|
129
|
+
...diff,
|
|
130
|
+
from: previousScreenshot?.timestamp,
|
|
131
|
+
to: screenshot.timestamp
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Trim history
|
|
135
|
+
while (screenDiffHistory.length > MAX_DIFF_HISTORY) {
|
|
136
|
+
screenDiffHistory.shift();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
previousScreenshot = screenshot;
|
|
141
|
+
return diff;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get recent screen changes
|
|
146
|
+
*/
|
|
147
|
+
function getScreenDiffHistory() {
|
|
148
|
+
return screenDiffHistory;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ===== ACTIVE WINDOW TRACKING (Windows) =====
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get information about the currently active window
|
|
155
|
+
* Uses PowerShell on Windows
|
|
156
|
+
*/
|
|
157
|
+
async function getActiveWindow() {
|
|
158
|
+
if (process.platform !== 'win32') {
|
|
159
|
+
return { error: 'Active window tracking only supported on Windows currently' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const psScript = `
|
|
163
|
+
Add-Type @"
|
|
164
|
+
using System;
|
|
165
|
+
using System.Runtime.InteropServices;
|
|
166
|
+
using System.Text;
|
|
167
|
+
public class Win32 {
|
|
168
|
+
[DllImport("user32.dll")]
|
|
169
|
+
public static extern IntPtr GetForegroundWindow();
|
|
170
|
+
[DllImport("user32.dll")]
|
|
171
|
+
public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
|
|
172
|
+
[DllImport("user32.dll")]
|
|
173
|
+
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
|
174
|
+
[DllImport("user32.dll")]
|
|
175
|
+
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
|
176
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
177
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
178
|
+
}
|
|
179
|
+
"@
|
|
180
|
+
$hwnd = [Win32]::GetForegroundWindow()
|
|
181
|
+
$title = New-Object System.Text.StringBuilder 256
|
|
182
|
+
[Win32]::GetWindowText($hwnd, $title, 256) | Out-Null
|
|
183
|
+
$processId = 0
|
|
184
|
+
[Win32]::GetWindowThreadProcessId($hwnd, [ref]$processId) | Out-Null
|
|
185
|
+
$process = Get-Process -Id $processId -ErrorAction SilentlyContinue
|
|
186
|
+
$rect = New-Object Win32+RECT
|
|
187
|
+
[Win32]::GetWindowRect($hwnd, [ref]$rect) | Out-Null
|
|
188
|
+
@{
|
|
189
|
+
Title = $title.ToString()
|
|
190
|
+
ProcessName = $process.ProcessName
|
|
191
|
+
ProcessId = $processId
|
|
192
|
+
Bounds = @{
|
|
193
|
+
X = $rect.Left
|
|
194
|
+
Y = $rect.Top
|
|
195
|
+
Width = $rect.Right - $rect.Left
|
|
196
|
+
Height = $rect.Bottom - $rect.Top
|
|
197
|
+
}
|
|
198
|
+
} | ConvertTo-Json
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
const result = await executePowerShellScript(psScript, 5000);
|
|
202
|
+
|
|
203
|
+
if (result.error) {
|
|
204
|
+
return { error: result.error };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const info = JSON.parse(result.stdout);
|
|
209
|
+
activeWindowInfo = {
|
|
210
|
+
...info,
|
|
211
|
+
timestamp: Date.now()
|
|
212
|
+
};
|
|
213
|
+
return activeWindowInfo;
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return { error: 'Failed to parse window info', raw: result.stdout };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get cached active window info
|
|
221
|
+
*/
|
|
222
|
+
function getCachedActiveWindow() {
|
|
223
|
+
return activeWindowInfo;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ===== OCR INTEGRATION =====
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Extract text from an image using OCR
|
|
230
|
+
* Supports Tesseract (local) or cloud OCR services
|
|
231
|
+
*/
|
|
232
|
+
async function extractTextFromImage(imageData, options = {}) {
|
|
233
|
+
const { provider = 'tesseract', language = 'eng' } = options;
|
|
234
|
+
|
|
235
|
+
// Check cache
|
|
236
|
+
const cacheKey = `${imageData.timestamp}-${provider}`;
|
|
237
|
+
if (ocrCache.has(cacheKey)) {
|
|
238
|
+
return ocrCache.get(cacheKey);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
let result;
|
|
243
|
+
|
|
244
|
+
switch (provider) {
|
|
245
|
+
case 'tesseract':
|
|
246
|
+
result = await extractWithTesseract(imageData, language);
|
|
247
|
+
break;
|
|
248
|
+
case 'windows-ocr':
|
|
249
|
+
result = await extractWithWindowsOCR(imageData);
|
|
250
|
+
break;
|
|
251
|
+
default:
|
|
252
|
+
result = { error: `Unknown OCR provider: ${provider}` };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Cache result
|
|
256
|
+
ocrCache.set(cacheKey, result);
|
|
257
|
+
|
|
258
|
+
// Limit cache size
|
|
259
|
+
if (ocrCache.size > 50) {
|
|
260
|
+
const firstKey = ocrCache.keys().next().value;
|
|
261
|
+
ocrCache.delete(firstKey);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return result;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return { error: error.message };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Extract text using Tesseract OCR
|
|
272
|
+
*/
|
|
273
|
+
function extractWithTesseract(imageData, language) {
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
// Save image to temp file
|
|
276
|
+
const tempDir = path.join(os.tmpdir(), 'liku-ocr');
|
|
277
|
+
if (!fs.existsSync(tempDir)) {
|
|
278
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const tempImagePath = path.join(tempDir, `ocr-${Date.now()}.png`);
|
|
282
|
+
const base64Data = imageData.dataURL.replace(/^data:image\/\w+;base64,/, '');
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
fs.writeFileSync(tempImagePath, base64Data, 'base64');
|
|
286
|
+
} catch (err) {
|
|
287
|
+
resolve({ error: 'Failed to write temp image: ' + err.message });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Call Tesseract
|
|
292
|
+
exec(`tesseract "${tempImagePath}" stdout -l ${language}`,
|
|
293
|
+
{ timeout: 30000 },
|
|
294
|
+
(error, stdout, stderr) => {
|
|
295
|
+
// Clean up temp file
|
|
296
|
+
try { fs.unlinkSync(tempImagePath); } catch (e) {}
|
|
297
|
+
|
|
298
|
+
if (error) {
|
|
299
|
+
if (error.message.includes('not recognized') || error.message.includes('not found')) {
|
|
300
|
+
resolve({
|
|
301
|
+
error: 'Tesseract not installed. Install from: https://github.com/UB-Mannheim/tesseract/wiki',
|
|
302
|
+
installHint: true
|
|
303
|
+
});
|
|
304
|
+
} else {
|
|
305
|
+
resolve({ error: error.message });
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
resolve({
|
|
311
|
+
text: stdout.trim(),
|
|
312
|
+
language,
|
|
313
|
+
timestamp: Date.now()
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Extract text using Windows built-in OCR
|
|
322
|
+
*/
|
|
323
|
+
async function extractWithWindowsOCR(imageData) {
|
|
324
|
+
if (process.platform !== 'win32') {
|
|
325
|
+
return { error: 'Windows OCR only available on Windows' };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Save image to temp file
|
|
329
|
+
const tempDir = path.join(os.tmpdir(), 'liku-ocr');
|
|
330
|
+
if (!fs.existsSync(tempDir)) {
|
|
331
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const tempImagePath = path.join(tempDir, `ocr-${Date.now()}.png`);
|
|
335
|
+
const base64Data = imageData.dataURL.replace(/^data:image\/\w+;base64,/, '');
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
fs.writeFileSync(tempImagePath, base64Data, 'base64');
|
|
339
|
+
} catch (err) {
|
|
340
|
+
return { error: 'Failed to write temp image: ' + err.message };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Use Windows OCR via PowerShell
|
|
344
|
+
const psScript = `
|
|
345
|
+
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
|
346
|
+
$null = [Windows.Media.Ocr.OcrEngine,Windows.Foundation,ContentType=WindowsRuntime]
|
|
347
|
+
$null = [Windows.Graphics.Imaging.BitmapDecoder,Windows.Foundation,ContentType=WindowsRuntime]
|
|
348
|
+
$null = [Windows.Storage.StorageFile,Windows.Foundation,ContentType=WindowsRuntime]
|
|
349
|
+
|
|
350
|
+
$file = [Windows.Storage.StorageFile]::GetFileFromPathAsync("${tempImagePath.replace(/\\/g, '\\\\')}").GetAwaiter().GetResult()
|
|
351
|
+
$stream = $file.OpenReadAsync().GetAwaiter().GetResult()
|
|
352
|
+
$decoder = [Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream).GetAwaiter().GetResult()
|
|
353
|
+
$bitmap = $decoder.GetSoftwareBitmapAsync().GetAwaiter().GetResult()
|
|
354
|
+
|
|
355
|
+
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
|
|
356
|
+
$result = $engine.RecognizeAsync($bitmap).GetAwaiter().GetResult()
|
|
357
|
+
$result.Text
|
|
358
|
+
`;
|
|
359
|
+
|
|
360
|
+
const result = await executePowerShellScript(psScript, 30000);
|
|
361
|
+
|
|
362
|
+
// Clean up temp file
|
|
363
|
+
try { fs.unlinkSync(tempImagePath); } catch (e) {}
|
|
364
|
+
|
|
365
|
+
if (result.error) {
|
|
366
|
+
return { error: 'Windows OCR failed: ' + result.error };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
text: result.stdout,
|
|
371
|
+
provider: 'windows-ocr',
|
|
372
|
+
timestamp: Date.now()
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ===== UI ELEMENT DETECTION =====
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Detect UI elements from accessibility tree (Windows UI Automation)
|
|
380
|
+
*/
|
|
381
|
+
async function detectUIElements(options = {}) {
|
|
382
|
+
if (process.platform !== 'win32') {
|
|
383
|
+
return { error: 'UI Automation only available on Windows' };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const { depth = 3 } = options;
|
|
387
|
+
|
|
388
|
+
const psScript = `
|
|
389
|
+
Add-Type -AssemblyName UIAutomationClient
|
|
390
|
+
Add-Type -AssemblyName UIAutomationTypes
|
|
391
|
+
|
|
392
|
+
function Get-UIElements {
|
|
393
|
+
param($element, $depth, $currentDepth = 0)
|
|
394
|
+
|
|
395
|
+
if ($currentDepth -ge $depth) { return @() }
|
|
396
|
+
|
|
397
|
+
$results = @()
|
|
398
|
+
$condition = [System.Windows.Automation.Condition]::TrueCondition
|
|
399
|
+
$children = $element.FindAll([System.Windows.Automation.TreeScope]::Children, $condition)
|
|
400
|
+
|
|
401
|
+
foreach ($child in $children) {
|
|
402
|
+
try {
|
|
403
|
+
$rect = $child.Current.BoundingRectangle
|
|
404
|
+
if ($rect.Width -gt 0 -and $rect.Height -gt 0) {
|
|
405
|
+
$results += @{
|
|
406
|
+
Name = $child.Current.Name
|
|
407
|
+
ControlType = $child.Current.ControlType.ProgrammaticName
|
|
408
|
+
AutomationId = $child.Current.AutomationId
|
|
409
|
+
ClassName = $child.Current.ClassName
|
|
410
|
+
Bounds = @{
|
|
411
|
+
X = [int]$rect.X
|
|
412
|
+
Y = [int]$rect.Y
|
|
413
|
+
Width = [int]$rect.Width
|
|
414
|
+
Height = [int]$rect.Height
|
|
415
|
+
}
|
|
416
|
+
IsEnabled = $child.Current.IsEnabled
|
|
417
|
+
}
|
|
418
|
+
$results += Get-UIElements -element $child -depth $depth -currentDepth ($currentDepth + 1)
|
|
419
|
+
}
|
|
420
|
+
} catch {}
|
|
421
|
+
}
|
|
422
|
+
return $results
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
$root = [System.Windows.Automation.AutomationElement]::FocusedElement
|
|
426
|
+
if ($null -eq $root) {
|
|
427
|
+
$root = [System.Windows.Automation.AutomationElement]::RootElement
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
$elements = Get-UIElements -element $root -depth ${depth}
|
|
431
|
+
$elements | ConvertTo-Json -Depth 10
|
|
432
|
+
`;
|
|
433
|
+
|
|
434
|
+
const result = await executePowerShellScript(psScript, 10000);
|
|
435
|
+
|
|
436
|
+
if (result.error) {
|
|
437
|
+
return { error: 'UI Automation failed: ' + result.error };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
let elements = JSON.parse(result.stdout || '[]');
|
|
442
|
+
if (!Array.isArray(elements)) {
|
|
443
|
+
elements = [elements];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Cache results
|
|
447
|
+
elementCache.set(Date.now(), elements);
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
elements,
|
|
451
|
+
count: elements.length,
|
|
452
|
+
timestamp: Date.now()
|
|
453
|
+
};
|
|
454
|
+
} catch (e) {
|
|
455
|
+
return { elements: [], count: 0, error: 'Parse error' };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Find UI element at specific coordinates
|
|
461
|
+
*/
|
|
462
|
+
async function findElementAtPoint(x, y) {
|
|
463
|
+
if (process.platform !== 'win32') {
|
|
464
|
+
return { error: 'UI Automation only available on Windows' };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const psScript = `
|
|
468
|
+
Add-Type -AssemblyName UIAutomationClient
|
|
469
|
+
Add-Type -AssemblyName UIAutomationTypes
|
|
470
|
+
|
|
471
|
+
$point = New-Object System.Windows.Point(${x}, ${y})
|
|
472
|
+
$element = [System.Windows.Automation.AutomationElement]::FromPoint($point)
|
|
473
|
+
|
|
474
|
+
if ($null -ne $element) {
|
|
475
|
+
$rect = $element.Current.BoundingRectangle
|
|
476
|
+
@{
|
|
477
|
+
Name = $element.Current.Name
|
|
478
|
+
ControlType = $element.Current.ControlType.ProgrammaticName
|
|
479
|
+
AutomationId = $element.Current.AutomationId
|
|
480
|
+
ClassName = $element.Current.ClassName
|
|
481
|
+
Value = try { $element.GetCurrentPropertyValue([System.Windows.Automation.AutomationElement]::ValueProperty) } catch { $null }
|
|
482
|
+
Bounds = @{
|
|
483
|
+
X = [int]$rect.X
|
|
484
|
+
Y = [int]$rect.Y
|
|
485
|
+
Width = [int]$rect.Width
|
|
486
|
+
Height = [int]$rect.Height
|
|
487
|
+
}
|
|
488
|
+
IsEnabled = $element.Current.IsEnabled
|
|
489
|
+
HasKeyboardFocus = $element.Current.HasKeyboardFocus
|
|
490
|
+
} | ConvertTo-Json
|
|
491
|
+
} else {
|
|
492
|
+
@{ error = "No element found at point" } | ConvertTo-Json
|
|
493
|
+
}
|
|
494
|
+
`;
|
|
495
|
+
|
|
496
|
+
const result = await executePowerShellScript(psScript, 5000);
|
|
497
|
+
|
|
498
|
+
if (result.error) {
|
|
499
|
+
return { error: 'Element lookup failed: ' + result.error };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const element = JSON.parse(result.stdout);
|
|
504
|
+
return {
|
|
505
|
+
...element,
|
|
506
|
+
queryPoint: { x, y },
|
|
507
|
+
timestamp: Date.now()
|
|
508
|
+
};
|
|
509
|
+
} catch (e) {
|
|
510
|
+
return { error: 'Parse error' };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ===== COMPREHENSIVE SCREEN ANALYSIS =====
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Perform full screen analysis including:
|
|
518
|
+
* - Active window detection
|
|
519
|
+
* - Screen diff from previous
|
|
520
|
+
* - OCR text extraction
|
|
521
|
+
* - UI element detection
|
|
522
|
+
*/
|
|
523
|
+
async function analyzeScreen(screenshot, options = {}) {
|
|
524
|
+
const {
|
|
525
|
+
includeOCR = true,
|
|
526
|
+
includeElements = true,
|
|
527
|
+
ocrProvider = 'tesseract'
|
|
528
|
+
} = options;
|
|
529
|
+
|
|
530
|
+
const results = {
|
|
531
|
+
timestamp: Date.now(),
|
|
532
|
+
screenshot: {
|
|
533
|
+
width: screenshot.width,
|
|
534
|
+
height: screenshot.height,
|
|
535
|
+
timestamp: screenshot.timestamp
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// Parallel execution of analysis tasks
|
|
540
|
+
const tasks = [];
|
|
541
|
+
|
|
542
|
+
// Active window
|
|
543
|
+
tasks.push(
|
|
544
|
+
getActiveWindow().then(info => {
|
|
545
|
+
results.activeWindow = info;
|
|
546
|
+
})
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// Screen diff
|
|
550
|
+
const diff = trackScreenChange(screenshot);
|
|
551
|
+
results.screenDiff = diff;
|
|
552
|
+
|
|
553
|
+
// OCR (optional, can be slow)
|
|
554
|
+
if (includeOCR) {
|
|
555
|
+
tasks.push(
|
|
556
|
+
extractTextFromImage(screenshot, { provider: ocrProvider }).then(ocr => {
|
|
557
|
+
results.ocr = ocr;
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// UI Elements (optional)
|
|
563
|
+
if (includeElements) {
|
|
564
|
+
tasks.push(
|
|
565
|
+
detectUIElements({ depth: 2 }).then(elements => {
|
|
566
|
+
results.uiElements = elements;
|
|
567
|
+
})
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Wait for all tasks
|
|
572
|
+
await Promise.allSettled(tasks);
|
|
573
|
+
|
|
574
|
+
return results;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ===== EXPORTS =====
|
|
578
|
+
module.exports = {
|
|
579
|
+
// Screen diffing
|
|
580
|
+
trackScreenChange,
|
|
581
|
+
getScreenDiffHistory,
|
|
582
|
+
compareScreenshots,
|
|
583
|
+
|
|
584
|
+
// Active window
|
|
585
|
+
getActiveWindow,
|
|
586
|
+
getCachedActiveWindow,
|
|
587
|
+
|
|
588
|
+
// OCR
|
|
589
|
+
extractTextFromImage,
|
|
590
|
+
|
|
591
|
+
// UI Elements
|
|
592
|
+
detectUIElements,
|
|
593
|
+
findElementAtPoint,
|
|
594
|
+
|
|
595
|
+
// Comprehensive analysis
|
|
596
|
+
analyzeScreen
|
|
597
|
+
};
|