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,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot Module
|
|
3
|
+
*
|
|
4
|
+
* Capture screenshots of screen, windows, or regions.
|
|
5
|
+
* @module ui-automation/screenshot
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { executePowerShellScript } = require('./core/powershell');
|
|
9
|
+
const { log } = require('./core/helpers');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Take a screenshot
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} [options] - Screenshot options
|
|
17
|
+
* @param {string} [options.path] - Save path (auto-generated if omitted)
|
|
18
|
+
* @param {Object} [options.region] - Region to capture {x, y, width, height}
|
|
19
|
+
* @param {number} [options.windowHwnd] - Capture specific window by handle
|
|
20
|
+
* @param {string} [options.format='png'] - Image format (png, jpg, bmp)
|
|
21
|
+
* @returns {Promise<{success: boolean, path: string|null, base64: string|null}>}
|
|
22
|
+
*/
|
|
23
|
+
async function screenshot(options = {}) {
|
|
24
|
+
const {
|
|
25
|
+
path: savePath,
|
|
26
|
+
region,
|
|
27
|
+
windowHwnd,
|
|
28
|
+
format = 'png',
|
|
29
|
+
} = options;
|
|
30
|
+
|
|
31
|
+
// Generate path if not provided
|
|
32
|
+
const outputPath = savePath || path.join(
|
|
33
|
+
os.tmpdir(),
|
|
34
|
+
`screenshot_${Date.now()}.${format}`
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Build PowerShell script based on capture type
|
|
38
|
+
let captureScript;
|
|
39
|
+
|
|
40
|
+
if (windowHwnd) {
|
|
41
|
+
// Capture specific window
|
|
42
|
+
captureScript = `
|
|
43
|
+
Add-Type @'
|
|
44
|
+
using System;
|
|
45
|
+
using System.Drawing;
|
|
46
|
+
using System.Runtime.InteropServices;
|
|
47
|
+
|
|
48
|
+
public class WindowCapture {
|
|
49
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
|
50
|
+
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr hWnd, IntPtr hDC, int flags);
|
|
51
|
+
|
|
52
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
53
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
54
|
+
|
|
55
|
+
public static Bitmap Capture(IntPtr hwnd) {
|
|
56
|
+
RECT rect;
|
|
57
|
+
GetWindowRect(hwnd, out rect);
|
|
58
|
+
int w = rect.Right - rect.Left;
|
|
59
|
+
int h = rect.Bottom - rect.Top;
|
|
60
|
+
if (w <= 0 || h <= 0) return null;
|
|
61
|
+
|
|
62
|
+
var bmp = new Bitmap(w, h);
|
|
63
|
+
using (var g = Graphics.FromImage(bmp)) {
|
|
64
|
+
IntPtr hdc = g.GetHdc();
|
|
65
|
+
PrintWindow(hwnd, hdc, 2);
|
|
66
|
+
g.ReleaseHdc(hdc);
|
|
67
|
+
}
|
|
68
|
+
return bmp;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
'@
|
|
72
|
+
|
|
73
|
+
Add-Type -AssemblyName System.Drawing
|
|
74
|
+
$bmp = [WindowCapture]::Capture([IntPtr]::new(${windowHwnd}))
|
|
75
|
+
`;
|
|
76
|
+
} else if (region) {
|
|
77
|
+
// Capture region
|
|
78
|
+
captureScript = `
|
|
79
|
+
Add-Type -AssemblyName System.Drawing
|
|
80
|
+
$bmp = New-Object System.Drawing.Bitmap(${region.width}, ${region.height})
|
|
81
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
82
|
+
$g.CopyFromScreen(${region.x}, ${region.y}, 0, 0, $bmp.Size)
|
|
83
|
+
$g.Dispose()
|
|
84
|
+
`;
|
|
85
|
+
} else {
|
|
86
|
+
// Capture full screen
|
|
87
|
+
captureScript = `
|
|
88
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
89
|
+
Add-Type -AssemblyName System.Drawing
|
|
90
|
+
|
|
91
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
|
92
|
+
$bmp = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height)
|
|
93
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
94
|
+
$g.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size)
|
|
95
|
+
$g.Dispose()
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Add save and output
|
|
100
|
+
const formatMap = { png: 'Png', jpg: 'Jpeg', bmp: 'Bmp' };
|
|
101
|
+
const imageFormat = formatMap[format.toLowerCase()] || 'Png';
|
|
102
|
+
|
|
103
|
+
const psScript = `
|
|
104
|
+
${captureScript}
|
|
105
|
+
if ($bmp -eq $null) {
|
|
106
|
+
Write-Output "capture_failed"
|
|
107
|
+
exit
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
$path = '${outputPath.replace(/\\/g, '\\\\').replace(/'/g, "''")}'
|
|
111
|
+
$bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::${imageFormat})
|
|
112
|
+
$bmp.Dispose()
|
|
113
|
+
|
|
114
|
+
# Output base64 for convenience
|
|
115
|
+
$bytes = [System.IO.File]::ReadAllBytes($path)
|
|
116
|
+
$base64 = [System.Convert]::ToBase64String($bytes)
|
|
117
|
+
Write-Output "SCREENSHOT_PATH:$path"
|
|
118
|
+
Write-Output "SCREENSHOT_BASE64:$base64"
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const result = await executePowerShellScript(psScript);
|
|
123
|
+
|
|
124
|
+
if (result.stdout.includes('capture_failed')) {
|
|
125
|
+
log('Screenshot capture failed', 'error');
|
|
126
|
+
return { success: false, path: null, base64: null };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const pathMatch = result.stdout.match(/SCREENSHOT_PATH:(.+)/);
|
|
130
|
+
const base64Match = result.stdout.match(/SCREENSHOT_BASE64:(.+)/);
|
|
131
|
+
|
|
132
|
+
const screenshotPath = pathMatch ? pathMatch[1].trim() : outputPath;
|
|
133
|
+
const base64 = base64Match ? base64Match[1].trim() : null;
|
|
134
|
+
|
|
135
|
+
log(`Screenshot saved to: ${screenshotPath}`);
|
|
136
|
+
|
|
137
|
+
return { success: true, path: screenshotPath, base64 };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
log(`Screenshot error: ${err.message}`, 'error');
|
|
140
|
+
return { success: false, path: null, base64: null };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Take screenshot of active window
|
|
146
|
+
*
|
|
147
|
+
* @param {Object} [options] - Screenshot options
|
|
148
|
+
* @returns {Promise<{success: boolean, path: string|null}>}
|
|
149
|
+
*/
|
|
150
|
+
async function screenshotActiveWindow(options = {}) {
|
|
151
|
+
const { getActiveWindow } = require('./window');
|
|
152
|
+
const activeWindow = await getActiveWindow();
|
|
153
|
+
|
|
154
|
+
if (!activeWindow) {
|
|
155
|
+
return { success: false, path: null, base64: null };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return screenshot({ ...options, windowHwnd: activeWindow.hwnd });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Take screenshot of element
|
|
163
|
+
*
|
|
164
|
+
* @param {Object} criteria - Element search criteria
|
|
165
|
+
* @param {Object} [options] - Screenshot options
|
|
166
|
+
* @returns {Promise<{success: boolean, path: string|null}>}
|
|
167
|
+
*/
|
|
168
|
+
async function screenshotElement(criteria, options = {}) {
|
|
169
|
+
const { findElement } = require('./elements');
|
|
170
|
+
const element = await findElement(criteria);
|
|
171
|
+
|
|
172
|
+
if (!element || !element.bounds) {
|
|
173
|
+
return { success: false, path: null, base64: null };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return screenshot({ ...options, region: element.bounds });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
screenshot,
|
|
181
|
+
screenshotActiveWindow,
|
|
182
|
+
screenshotElement,
|
|
183
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Window Management Module
|
|
3
|
+
*
|
|
4
|
+
* @module ui-automation/window
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
getActiveWindow,
|
|
9
|
+
findWindows,
|
|
10
|
+
focusWindow,
|
|
11
|
+
minimizeWindow,
|
|
12
|
+
maximizeWindow,
|
|
13
|
+
restoreWindow,
|
|
14
|
+
} = require('./manager');
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
getActiveWindow,
|
|
18
|
+
findWindows,
|
|
19
|
+
focusWindow,
|
|
20
|
+
minimizeWindow,
|
|
21
|
+
maximizeWindow,
|
|
22
|
+
restoreWindow,
|
|
23
|
+
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Window Management Module
|
|
3
|
+
*
|
|
4
|
+
* Find, focus, and interact with windows.
|
|
5
|
+
* @module ui-automation/window
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { executePowerShellScript } = require('../core/powershell');
|
|
9
|
+
const { log, sleep } = require('../core/helpers');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get the active (foreground) window info
|
|
13
|
+
*
|
|
14
|
+
* @returns {Promise<{hwnd: number, title: string, processName: string, className: string, bounds: Object} | null>}
|
|
15
|
+
*/
|
|
16
|
+
async function getActiveWindow() {
|
|
17
|
+
const psScript = `
|
|
18
|
+
Add-Type @'
|
|
19
|
+
using System;
|
|
20
|
+
using System.Runtime.InteropServices;
|
|
21
|
+
using System.Text;
|
|
22
|
+
|
|
23
|
+
public class WinAPI {
|
|
24
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
25
|
+
[DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
|
|
26
|
+
[DllImport("user32.dll")] public static extern int GetClassName(IntPtr hWnd, StringBuilder name, int count);
|
|
27
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
|
28
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
|
29
|
+
|
|
30
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
31
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
32
|
+
}
|
|
33
|
+
'@
|
|
34
|
+
|
|
35
|
+
$hwnd = [WinAPI]::GetForegroundWindow()
|
|
36
|
+
if ($hwnd -eq [IntPtr]::Zero) { Write-Output "null"; exit }
|
|
37
|
+
|
|
38
|
+
$titleSB = New-Object System.Text.StringBuilder 256
|
|
39
|
+
$classSB = New-Object System.Text.StringBuilder 256
|
|
40
|
+
[void][WinAPI]::GetWindowText($hwnd, $titleSB, 256)
|
|
41
|
+
[void][WinAPI]::GetClassName($hwnd, $classSB, 256)
|
|
42
|
+
|
|
43
|
+
$procId = 0
|
|
44
|
+
[void][WinAPI]::GetWindowThreadProcessId($hwnd, [ref]$procId)
|
|
45
|
+
$proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
|
|
46
|
+
|
|
47
|
+
$rect = New-Object WinAPI+RECT
|
|
48
|
+
[void][WinAPI]::GetWindowRect($hwnd, [ref]$rect)
|
|
49
|
+
|
|
50
|
+
@{
|
|
51
|
+
hwnd = $hwnd.ToInt64()
|
|
52
|
+
title = $titleSB.ToString()
|
|
53
|
+
className = $classSB.ToString()
|
|
54
|
+
processName = if ($proc) { $proc.ProcessName } else { "" }
|
|
55
|
+
bounds = @{ x = $rect.Left; y = $rect.Top; width = $rect.Right - $rect.Left; height = $rect.Bottom - $rect.Top }
|
|
56
|
+
} | ConvertTo-Json -Compress
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const result = await executePowerShellScript(psScript);
|
|
61
|
+
if (result.stdout.trim() === 'null') return null;
|
|
62
|
+
const data = JSON.parse(result.stdout.trim());
|
|
63
|
+
log(`Active window: "${data.title}" (${data.processName})`);
|
|
64
|
+
return data;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
log(`getActiveWindow error: ${err.message}`, 'error');
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Find windows matching criteria
|
|
73
|
+
*
|
|
74
|
+
* @param {Object} [criteria] - Search criteria
|
|
75
|
+
* @param {string} [criteria.title] - Window title contains
|
|
76
|
+
* @param {string} [criteria.processName] - Process name equals
|
|
77
|
+
* @param {string} [criteria.className] - Window class contains
|
|
78
|
+
* @returns {Promise<Array<{hwnd: number, title: string, processName: string, className: string, bounds: Object}>>}
|
|
79
|
+
*/
|
|
80
|
+
async function findWindows(criteria = {}) {
|
|
81
|
+
const { title, processName, className } = criteria;
|
|
82
|
+
|
|
83
|
+
const psScript = `
|
|
84
|
+
Add-Type @'
|
|
85
|
+
using System;
|
|
86
|
+
using System.Collections.Generic;
|
|
87
|
+
using System.Runtime.InteropServices;
|
|
88
|
+
using System.Text;
|
|
89
|
+
|
|
90
|
+
public class WindowFinder {
|
|
91
|
+
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam);
|
|
92
|
+
[DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
|
|
93
|
+
[DllImport("user32.dll")] public static extern int GetClassName(IntPtr hWnd, StringBuilder name, int count);
|
|
94
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
|
95
|
+
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
96
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
|
97
|
+
|
|
98
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
99
|
+
public struct RECT { public int Left, Top, Right, Bottom; }
|
|
100
|
+
|
|
101
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
102
|
+
|
|
103
|
+
public static List<IntPtr> windows = new List<IntPtr>();
|
|
104
|
+
|
|
105
|
+
public static void Find() {
|
|
106
|
+
windows.Clear();
|
|
107
|
+
EnumWindows((h, l) => { if (IsWindowVisible(h)) windows.Add(h); return true; }, IntPtr.Zero);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
'@
|
|
111
|
+
|
|
112
|
+
[WindowFinder]::Find()
|
|
113
|
+
$results = @()
|
|
114
|
+
|
|
115
|
+
foreach ($hwnd in [WindowFinder]::windows) {
|
|
116
|
+
$titleSB = New-Object System.Text.StringBuilder 256
|
|
117
|
+
$classSB = New-Object System.Text.StringBuilder 256
|
|
118
|
+
[void][WindowFinder]::GetWindowText($hwnd, $titleSB, 256)
|
|
119
|
+
[void][WindowFinder]::GetClassName($hwnd, $classSB, 256)
|
|
120
|
+
|
|
121
|
+
$t = $titleSB.ToString()
|
|
122
|
+
$c = $classSB.ToString()
|
|
123
|
+
if ([string]::IsNullOrEmpty($t)) { continue }
|
|
124
|
+
|
|
125
|
+
${title ? `if (-not $t.ToLower().Contains('${title.toLowerCase().replace(/'/g, "''")}')) { continue }` : ''}
|
|
126
|
+
${className ? `if (-not $c.ToLower().Contains('${className.toLowerCase().replace(/'/g, "''")}')) { continue }` : ''}
|
|
127
|
+
|
|
128
|
+
$procId = 0
|
|
129
|
+
[void][WindowFinder]::GetWindowThreadProcessId($hwnd, [ref]$procId)
|
|
130
|
+
$proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
|
|
131
|
+
$pn = if ($proc) { $proc.ProcessName } else { "" }
|
|
132
|
+
|
|
133
|
+
${processName ? `if ($pn -ne '${processName.replace(/'/g, "''")}') { continue }` : ''}
|
|
134
|
+
|
|
135
|
+
$rect = New-Object WindowFinder+RECT
|
|
136
|
+
[void][WindowFinder]::GetWindowRect($hwnd, [ref]$rect)
|
|
137
|
+
|
|
138
|
+
$results += @{
|
|
139
|
+
hwnd = $hwnd.ToInt64()
|
|
140
|
+
title = $t
|
|
141
|
+
className = $c
|
|
142
|
+
processName = $pn
|
|
143
|
+
bounds = @{ x = $rect.Left; y = $rect.Top; width = $rect.Right - $rect.Left; height = $rect.Bottom - $rect.Top }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
$results | ConvertTo-Json -Compress
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const result = await executePowerShellScript(psScript);
|
|
152
|
+
const output = result.stdout.trim();
|
|
153
|
+
if (!output || output === 'null') return [];
|
|
154
|
+
const data = JSON.parse(output);
|
|
155
|
+
const windows = Array.isArray(data) ? data : [data];
|
|
156
|
+
log(`Found ${windows.length} windows matching criteria`);
|
|
157
|
+
return windows;
|
|
158
|
+
} catch (err) {
|
|
159
|
+
log(`findWindows error: ${err.message}`, 'error');
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Focus a window (bring to foreground)
|
|
166
|
+
*
|
|
167
|
+
* @param {number|string|Object} target - Window handle, title substring, or criteria object
|
|
168
|
+
* @returns {Promise<{success: boolean, window: Object|null}>}
|
|
169
|
+
*/
|
|
170
|
+
async function focusWindow(target) {
|
|
171
|
+
let hwnd = null;
|
|
172
|
+
let windowInfo = null;
|
|
173
|
+
|
|
174
|
+
if (typeof target === 'number') {
|
|
175
|
+
hwnd = target;
|
|
176
|
+
} else if (typeof target === 'string') {
|
|
177
|
+
const windows = await findWindows({ title: target });
|
|
178
|
+
if (windows.length > 0) {
|
|
179
|
+
hwnd = windows[0].hwnd;
|
|
180
|
+
windowInfo = windows[0];
|
|
181
|
+
}
|
|
182
|
+
} else if (typeof target === 'object' && target.hwnd) {
|
|
183
|
+
hwnd = target.hwnd;
|
|
184
|
+
windowInfo = target;
|
|
185
|
+
} else if (typeof target === 'object') {
|
|
186
|
+
const windows = await findWindows(target);
|
|
187
|
+
if (windows.length > 0) {
|
|
188
|
+
hwnd = windows[0].hwnd;
|
|
189
|
+
windowInfo = windows[0];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!hwnd) {
|
|
194
|
+
log(`focusWindow: No window found for target`, 'warn');
|
|
195
|
+
return { success: false, window: null };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const psScript = `
|
|
199
|
+
Add-Type @'
|
|
200
|
+
using System;
|
|
201
|
+
using System.Runtime.InteropServices;
|
|
202
|
+
|
|
203
|
+
public class FocusHelper {
|
|
204
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
205
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int cmd);
|
|
206
|
+
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd);
|
|
207
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
208
|
+
}
|
|
209
|
+
'@
|
|
210
|
+
|
|
211
|
+
$hwnd = [IntPtr]::new(${hwnd})
|
|
212
|
+
[FocusHelper]::ShowWindow($hwnd, 9) # SW_RESTORE
|
|
213
|
+
Start-Sleep -Milliseconds 50
|
|
214
|
+
[FocusHelper]::BringWindowToTop($hwnd)
|
|
215
|
+
[FocusHelper]::SetForegroundWindow($hwnd)
|
|
216
|
+
Start-Sleep -Milliseconds 100
|
|
217
|
+
|
|
218
|
+
$fg = [FocusHelper]::GetForegroundWindow()
|
|
219
|
+
if ($fg -eq $hwnd) { "focused" } else { "failed" }
|
|
220
|
+
`;
|
|
221
|
+
|
|
222
|
+
const result = await executePowerShellScript(psScript);
|
|
223
|
+
const success = result.stdout.includes('focused');
|
|
224
|
+
log(`focusWindow hwnd=${hwnd} - ${success ? 'success' : 'failed'}`);
|
|
225
|
+
|
|
226
|
+
return { success, window: windowInfo };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Minimize a window
|
|
231
|
+
*
|
|
232
|
+
* @param {number} hwnd - Window handle
|
|
233
|
+
* @returns {Promise<{success: boolean}>}
|
|
234
|
+
*/
|
|
235
|
+
async function minimizeWindow(hwnd) {
|
|
236
|
+
const psScript = `
|
|
237
|
+
Add-Type @'
|
|
238
|
+
using System;
|
|
239
|
+
using System.Runtime.InteropServices;
|
|
240
|
+
public class MinHelper {
|
|
241
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int cmd);
|
|
242
|
+
}
|
|
243
|
+
'@
|
|
244
|
+
[MinHelper]::ShowWindow([IntPtr]::new(${hwnd}), 6) # SW_MINIMIZE
|
|
245
|
+
'minimized'
|
|
246
|
+
`;
|
|
247
|
+
|
|
248
|
+
const result = await executePowerShellScript(psScript);
|
|
249
|
+
return { success: result.stdout.includes('minimized') };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Maximize a window
|
|
254
|
+
*
|
|
255
|
+
* @param {number} hwnd - Window handle
|
|
256
|
+
* @returns {Promise<{success: boolean}>}
|
|
257
|
+
*/
|
|
258
|
+
async function maximizeWindow(hwnd) {
|
|
259
|
+
const psScript = `
|
|
260
|
+
Add-Type @'
|
|
261
|
+
using System;
|
|
262
|
+
using System.Runtime.InteropServices;
|
|
263
|
+
public class MaxHelper {
|
|
264
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int cmd);
|
|
265
|
+
}
|
|
266
|
+
'@
|
|
267
|
+
[MaxHelper]::ShowWindow([IntPtr]::new(${hwnd}), 3) # SW_MAXIMIZE
|
|
268
|
+
'maximized'
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
const result = await executePowerShellScript(psScript);
|
|
272
|
+
return { success: result.stdout.includes('maximized') };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Restore a window to normal state
|
|
277
|
+
*
|
|
278
|
+
* @param {number} hwnd - Window handle
|
|
279
|
+
* @returns {Promise<{success: boolean}>}
|
|
280
|
+
*/
|
|
281
|
+
async function restoreWindow(hwnd) {
|
|
282
|
+
const psScript = `
|
|
283
|
+
Add-Type @'
|
|
284
|
+
using System;
|
|
285
|
+
using System.Runtime.InteropServices;
|
|
286
|
+
public class RestoreHelper {
|
|
287
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int cmd);
|
|
288
|
+
}
|
|
289
|
+
'@
|
|
290
|
+
[RestoreHelper]::ShowWindow([IntPtr]::new(${hwnd}), 9) # SW_RESTORE
|
|
291
|
+
'restored'
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
const result = await executePowerShellScript(psScript);
|
|
295
|
+
return { success: result.stdout.includes('restored') };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = {
|
|
299
|
+
getActiveWindow,
|
|
300
|
+
findWindows,
|
|
301
|
+
focusWindow,
|
|
302
|
+
minimizeWindow,
|
|
303
|
+
maximizeWindow,
|
|
304
|
+
restoreWindow,
|
|
305
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized time utilities for consistent timestamp handling
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const TIME_FORMAT = {
|
|
6
|
+
ISO: 'iso',
|
|
7
|
+
FILENAME_SAFE: 'filename',
|
|
8
|
+
DISPLAY: 'display'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function nowIso() {
|
|
12
|
+
return new Date().toISOString();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function nowFilenameSafe() {
|
|
16
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function nowDisplay() {
|
|
20
|
+
return new Date().toLocaleString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatTimestamp(date, format = TIME_FORMAT.ISO) {
|
|
24
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
25
|
+
|
|
26
|
+
switch (format) {
|
|
27
|
+
case TIME_FORMAT.FILENAME_SAFE:
|
|
28
|
+
return d.toISOString().replace(/[:.]/g, '-');
|
|
29
|
+
case TIME_FORMAT.DISPLAY:
|
|
30
|
+
return d.toLocaleString();
|
|
31
|
+
case TIME_FORMAT.ISO:
|
|
32
|
+
default:
|
|
33
|
+
return d.toISOString();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseTimestamp(timestamp) {
|
|
38
|
+
return new Date(timestamp);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function timeSince(timestamp) {
|
|
42
|
+
const ms = Date.now() - new Date(timestamp).getTime();
|
|
43
|
+
const seconds = Math.floor(ms / 1000);
|
|
44
|
+
const minutes = Math.floor(seconds / 60);
|
|
45
|
+
const hours = Math.floor(minutes / 60);
|
|
46
|
+
const days = Math.floor(hours / 24);
|
|
47
|
+
|
|
48
|
+
if (days > 0) return `${days}d ago`;
|
|
49
|
+
if (hours > 0) return `${hours}h ago`;
|
|
50
|
+
if (minutes > 0) return `${minutes}m ago`;
|
|
51
|
+
return `${seconds}s ago`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
TIME_FORMAT,
|
|
56
|
+
nowIso,
|
|
57
|
+
nowFilenameSafe,
|
|
58
|
+
nowDisplay,
|
|
59
|
+
formatTimestamp,
|
|
60
|
+
parseTimestamp,
|
|
61
|
+
timeSince
|
|
62
|
+
};
|