awb-agent-manager 1.0.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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +246 -0
  3. package/dist/lib/agent-lockfile.d.ts +37 -0
  4. package/dist/lib/agent-lockfile.js +205 -0
  5. package/dist/lib/agent-lockfile.js.map +1 -0
  6. package/dist/lib/agent-manager-commands.d.ts +34 -0
  7. package/dist/lib/agent-manager-commands.js +531 -0
  8. package/dist/lib/agent-manager-commands.js.map +1 -0
  9. package/dist/lib/base-session-manager.d.ts +127 -0
  10. package/dist/lib/base-session-manager.js +667 -0
  11. package/dist/lib/base-session-manager.js.map +1 -0
  12. package/dist/lib/chat-attachment-prep.d.ts +35 -0
  13. package/dist/lib/chat-attachment-prep.js +180 -0
  14. package/dist/lib/chat-attachment-prep.js.map +1 -0
  15. package/dist/lib/chat-session-manager.d.ts +29 -0
  16. package/dist/lib/chat-session-manager.js +400 -0
  17. package/dist/lib/chat-session-manager.js.map +1 -0
  18. package/dist/lib/circuit-breaker.d.ts +34 -0
  19. package/dist/lib/circuit-breaker.js +146 -0
  20. package/dist/lib/circuit-breaker.js.map +1 -0
  21. package/dist/lib/cli-adapters/antigravity.d.ts +15 -0
  22. package/dist/lib/cli-adapters/antigravity.js +114 -0
  23. package/dist/lib/cli-adapters/antigravity.js.map +1 -0
  24. package/dist/lib/cli-adapters/base.d.ts +115 -0
  25. package/dist/lib/cli-adapters/base.js +111 -0
  26. package/dist/lib/cli-adapters/base.js.map +1 -0
  27. package/dist/lib/cli-adapters/claude.d.ts +19 -0
  28. package/dist/lib/cli-adapters/claude.js +233 -0
  29. package/dist/lib/cli-adapters/claude.js.map +1 -0
  30. package/dist/lib/cli-adapters/codex.d.ts +14 -0
  31. package/dist/lib/cli-adapters/codex.js +152 -0
  32. package/dist/lib/cli-adapters/codex.js.map +1 -0
  33. package/dist/lib/cli-adapters/deepseek.d.ts +15 -0
  34. package/dist/lib/cli-adapters/deepseek.js +70 -0
  35. package/dist/lib/cli-adapters/deepseek.js.map +1 -0
  36. package/dist/lib/cli-adapters/index.d.ts +8 -0
  37. package/dist/lib/cli-adapters/index.js +27 -0
  38. package/dist/lib/cli-adapters/index.js.map +1 -0
  39. package/dist/lib/cli-adapters/model-introspect.d.ts +3 -0
  40. package/dist/lib/cli-adapters/model-introspect.js +65 -0
  41. package/dist/lib/cli-adapters/model-introspect.js.map +1 -0
  42. package/dist/lib/cli-error-signatures.d.ts +9 -0
  43. package/dist/lib/cli-error-signatures.js +22 -0
  44. package/dist/lib/cli-error-signatures.js.map +1 -0
  45. package/dist/lib/cli-output-summary.d.ts +2 -0
  46. package/dist/lib/cli-output-summary.js +104 -0
  47. package/dist/lib/cli-output-summary.js.map +1 -0
  48. package/dist/lib/cli-resolver.d.ts +2 -0
  49. package/dist/lib/cli-resolver.js +177 -0
  50. package/dist/lib/cli-resolver.js.map +1 -0
  51. package/dist/lib/config.d.ts +13 -0
  52. package/dist/lib/config.js +124 -0
  53. package/dist/lib/config.js.map +1 -0
  54. package/dist/lib/constants.d.ts +36 -0
  55. package/dist/lib/constants.js +51 -0
  56. package/dist/lib/constants.js.map +1 -0
  57. package/dist/lib/environment-provisioner.d.ts +31 -0
  58. package/dist/lib/environment-provisioner.js +182 -0
  59. package/dist/lib/environment-provisioner.js.map +1 -0
  60. package/dist/lib/error-log-uploader.d.ts +19 -0
  61. package/dist/lib/error-log-uploader.js +138 -0
  62. package/dist/lib/error-log-uploader.js.map +1 -0
  63. package/dist/lib/event-dispatcher.d.ts +149 -0
  64. package/dist/lib/event-dispatcher.js +799 -0
  65. package/dist/lib/event-dispatcher.js.map +1 -0
  66. package/dist/lib/event-log-recorder.d.ts +11 -0
  67. package/dist/lib/event-log-recorder.js +77 -0
  68. package/dist/lib/event-log-recorder.js.map +1 -0
  69. package/dist/lib/event-stream.d.ts +26 -0
  70. package/dist/lib/event-stream.js +177 -0
  71. package/dist/lib/event-stream.js.map +1 -0
  72. package/dist/lib/fs-browser.d.ts +30 -0
  73. package/dist/lib/fs-browser.js +287 -0
  74. package/dist/lib/fs-browser.js.map +1 -0
  75. package/dist/lib/host-mcp/logs.d.ts +19 -0
  76. package/dist/lib/host-mcp/logs.js +102 -0
  77. package/dist/lib/host-mcp/logs.js.map +1 -0
  78. package/dist/lib/host-mcp/platform.d.ts +24 -0
  79. package/dist/lib/host-mcp/platform.js +133 -0
  80. package/dist/lib/host-mcp/platform.js.map +1 -0
  81. package/dist/lib/host-mcp/screens.d.ts +22 -0
  82. package/dist/lib/host-mcp/screens.js +172 -0
  83. package/dist/lib/host-mcp/screens.js.map +1 -0
  84. package/dist/lib/host-mcp/server.d.ts +3 -0
  85. package/dist/lib/host-mcp/server.js +233 -0
  86. package/dist/lib/host-mcp/server.js.map +1 -0
  87. package/dist/lib/host-mcp/tools.d.ts +172 -0
  88. package/dist/lib/host-mcp/tools.js +1241 -0
  89. package/dist/lib/host-mcp/tools.js.map +1 -0
  90. package/dist/lib/instance-heartbeat.d.ts +55 -0
  91. package/dist/lib/instance-heartbeat.js +118 -0
  92. package/dist/lib/instance-heartbeat.js.map +1 -0
  93. package/dist/lib/legacy-import.d.ts +17 -0
  94. package/dist/lib/legacy-import.js +70 -0
  95. package/dist/lib/legacy-import.js.map +1 -0
  96. package/dist/lib/logging.d.ts +3 -0
  97. package/dist/lib/logging.js +65 -0
  98. package/dist/lib/logging.js.map +1 -0
  99. package/dist/lib/managed-agent-context.d.ts +25 -0
  100. package/dist/lib/managed-agent-context.js +33 -0
  101. package/dist/lib/managed-agent-context.js.map +1 -0
  102. package/dist/lib/managed-agent-store.d.ts +34 -0
  103. package/dist/lib/managed-agent-store.js +160 -0
  104. package/dist/lib/managed-agent-store.js.map +1 -0
  105. package/dist/lib/managed-agents.d.ts +24 -0
  106. package/dist/lib/managed-agents.js +87 -0
  107. package/dist/lib/managed-agents.js.map +1 -0
  108. package/dist/lib/mcp-client.d.ts +8 -0
  109. package/dist/lib/mcp-client.js +122 -0
  110. package/dist/lib/mcp-client.js.map +1 -0
  111. package/dist/lib/orphan-cleanup.d.ts +6 -0
  112. package/dist/lib/orphan-cleanup.js +108 -0
  113. package/dist/lib/orphan-cleanup.js.map +1 -0
  114. package/dist/lib/presence-heartbeat.d.ts +8 -0
  115. package/dist/lib/presence-heartbeat.js +61 -0
  116. package/dist/lib/presence-heartbeat.js.map +1 -0
  117. package/dist/lib/prompts.d.ts +29 -0
  118. package/dist/lib/prompts.js +208 -0
  119. package/dist/lib/prompts.js.map +1 -0
  120. package/dist/lib/rest.d.ts +61 -0
  121. package/dist/lib/rest.js +307 -0
  122. package/dist/lib/rest.js.map +1 -0
  123. package/dist/lib/run-provisioner.d.ts +21 -0
  124. package/dist/lib/run-provisioner.js +115 -0
  125. package/dist/lib/run-provisioner.js.map +1 -0
  126. package/dist/lib/self-path.d.ts +7 -0
  127. package/dist/lib/self-path.js +29 -0
  128. package/dist/lib/self-path.js.map +1 -0
  129. package/dist/lib/self-update.d.ts +46 -0
  130. package/dist/lib/self-update.js +482 -0
  131. package/dist/lib/self-update.js.map +1 -0
  132. package/dist/lib/service-install.d.ts +23 -0
  133. package/dist/lib/service-install.js +673 -0
  134. package/dist/lib/service-install.js.map +1 -0
  135. package/dist/lib/setup.d.ts +16 -0
  136. package/dist/lib/setup.js +157 -0
  137. package/dist/lib/setup.js.map +1 -0
  138. package/dist/lib/subagent-manager.d.ts +84 -0
  139. package/dist/lib/subagent-manager.js +793 -0
  140. package/dist/lib/subagent-manager.js.map +1 -0
  141. package/dist/lib/subagent-monitor.d.ts +43 -0
  142. package/dist/lib/subagent-monitor.js +229 -0
  143. package/dist/lib/subagent-monitor.js.map +1 -0
  144. package/dist/lib/ticket-session-manager.d.ts +23 -0
  145. package/dist/lib/ticket-session-manager.js +471 -0
  146. package/dist/lib/ticket-session-manager.js.map +1 -0
  147. package/dist/lib/worktree-manager.d.ts +39 -0
  148. package/dist/lib/worktree-manager.js +226 -0
  149. package/dist/lib/worktree-manager.js.map +1 -0
  150. package/dist/main.d.ts +2 -0
  151. package/dist/main.js +670 -0
  152. package/dist/main.js.map +1 -0
  153. package/dist/package.json +57 -0
  154. package/package.json +57 -0
@@ -0,0 +1,1241 @@
1
+ import { promises as fsp } from 'node:fs';
2
+ import { existsSync, statSync } from 'node:fs';
3
+ import { hostname, userInfo, release, totalmem, freemem, cpus, arch } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { findBinary, hostPlatform, isWayland, makeTempDir, runCommand, runOsascript, runPowerShell, } from './platform.js';
6
+ const WIN32_TYPE = String.raw `
7
+ Add-Type -AssemblyName System.Windows.Forms
8
+ Add-Type -AssemblyName System.Drawing
9
+ Add-Type -ReferencedAssemblies System.Drawing.Common @"
10
+ using System;
11
+ using System.Runtime.InteropServices;
12
+ using System.Text;
13
+ public static class AwbHostWin32 {
14
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
15
+ [StructLayout(LayoutKind.Sequential)] public struct RECT {
16
+ public int Left; public int Top; public int Right; public int Bottom;
17
+ }
18
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
19
+ [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
20
+ public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
21
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
22
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
23
+ [DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
24
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
25
+ [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd);
26
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
27
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
28
+ [DllImport("user32.dll")] public static extern void SetCursorPos(int x, int y);
29
+ [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, int dwExtraInfo);
30
+ public const uint MOUSE_LEFTDOWN = 0x0002;
31
+ public const uint MOUSE_LEFTUP = 0x0004;
32
+ public const uint MOUSE_RIGHTDOWN= 0x0008;
33
+ public const uint MOUSE_RIGHTUP = 0x0010;
34
+ public const uint MOUSE_MIDDOWN = 0x0020;
35
+ public const uint MOUSE_MIDUP = 0x0040;
36
+ }
37
+ "@
38
+ `;
39
+ export async function takeScreenshot(args = {}) {
40
+ const plat = hostPlatform();
41
+ try {
42
+ if (plat === 'win32')
43
+ return await screenshotWin(args);
44
+ if (plat === 'darwin')
45
+ return await screenshotMac(args);
46
+ return await screenshotLinux(args);
47
+ }
48
+ catch (err) {
49
+ return { ok: false, error: `screenshot failed: ${err?.message || err}` };
50
+ }
51
+ }
52
+ async function screenshotWin(args) {
53
+ const tmpDir = makeTempDir('shot');
54
+ const outPath = args.save_path || join(tmpDir, 'screen.png');
55
+ const displayIdx = Math.max(0, args.display ?? 0);
56
+ const script = `${WIN32_TYPE}
57
+ $ErrorActionPreference = "Stop"
58
+ $screens = [System.Windows.Forms.Screen]::AllScreens
59
+ $idx = ${displayIdx}
60
+ if ($idx -ge $screens.Length) { $idx = 0 }
61
+ $bounds = $screens[$idx].Bounds
62
+ $bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
63
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
64
+ $g.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bmp.Size)
65
+ $g.Dispose()
66
+ $bmp.Save('${outPath.replace(/'/g, "''").replace(/\\/g, '\\\\')}', [System.Drawing.Imaging.ImageFormat]::Png)
67
+ $bmp.Dispose()
68
+ Write-Output ("{0}x{1}" -f $bounds.Width, $bounds.Height)
69
+ `;
70
+ const result = await runPowerShell(script, { timeoutMs: 20_000 });
71
+ if (result.spawnFailed) {
72
+ return { ok: false, error: 'PowerShell not available on PATH', hint: 'Install PowerShell or pwsh' };
73
+ }
74
+ if (result.code !== 0) {
75
+ return { ok: false, error: `screenshot powershell exited ${result.code}`, details: { stderr: result.stderr.slice(0, 500) } };
76
+ }
77
+ const m = /(\d+)x(\d+)/.exec(result.stdout);
78
+ const width = m ? parseInt(m[1], 10) : 0;
79
+ const height = m ? parseInt(m[2], 10) : 0;
80
+ const bytes = await fsp.readFile(outPath);
81
+ return {
82
+ ok: true,
83
+ base64_png: bytes.toString('base64'),
84
+ width, height,
85
+ saved_path: outPath,
86
+ platform: 'win32',
87
+ backend: 'powershell+System.Drawing',
88
+ };
89
+ }
90
+ async function screenshotMac(args) {
91
+ const tmpDir = makeTempDir('shot');
92
+ const outPath = args.save_path || join(tmpDir, 'screen.png');
93
+ const dispArg = typeof args.display === 'number' && args.display > 0
94
+ ? ['-D', String(args.display + 1)]
95
+ : [];
96
+ const result = await runCommand('screencapture', ['-x', '-t', 'png', ...dispArg, outPath], { timeoutMs: 15_000 });
97
+ if (result.spawnFailed) {
98
+ return { ok: false, error: 'screencapture not available', hint: 'macOS built-in tool; verify /usr/sbin is on PATH' };
99
+ }
100
+ if (result.code !== 0) {
101
+ return { ok: false, error: `screencapture exited ${result.code}`, details: { stderr: result.stderr.slice(0, 500) } };
102
+ }
103
+ const bytes = await fsp.readFile(outPath);
104
+ const { width, height } = await pngDimensions(bytes);
105
+ return {
106
+ ok: true,
107
+ base64_png: bytes.toString('base64'),
108
+ width, height,
109
+ saved_path: outPath,
110
+ platform: 'darwin',
111
+ backend: 'screencapture',
112
+ };
113
+ }
114
+ async function screenshotLinux(args) {
115
+ const tmpDir = makeTempDir('shot');
116
+ const outPath = args.save_path || join(tmpDir, 'screen.png');
117
+ const backendCandidates = isWayland()
118
+ ? ['grim', 'gnome-screenshot', 'spectacle', 'scrot', 'maim']
119
+ : ['scrot', 'maim', 'gnome-screenshot', 'import', 'grim'];
120
+ const backend = await findBinary(backendCandidates);
121
+ if (!backend) {
122
+ return {
123
+ ok: false,
124
+ error: 'no screenshot backend found',
125
+ hint: 'Install one of: grim (Wayland), scrot, maim, gnome-screenshot, or ImageMagick (provides `import`)',
126
+ details: { tried: backendCandidates, wayland: isWayland() },
127
+ };
128
+ }
129
+ let argv;
130
+ switch (backend) {
131
+ case 'grim':
132
+ argv = [outPath];
133
+ break;
134
+ case 'scrot':
135
+ argv = ['-o', outPath];
136
+ break;
137
+ case 'maim':
138
+ argv = ['-q', outPath];
139
+ break;
140
+ case 'gnome-screenshot':
141
+ argv = ['-f', outPath];
142
+ break;
143
+ case 'spectacle':
144
+ argv = ['-b', '-n', '-o', outPath];
145
+ break;
146
+ case 'import':
147
+ argv = ['-window', 'root', outPath];
148
+ break;
149
+ default:
150
+ argv = [outPath];
151
+ }
152
+ const result = await runCommand(backend, argv, { timeoutMs: 15_000 });
153
+ if (result.spawnFailed) {
154
+ return { ok: false, error: `${backend} not available`, hint: 'Install one of: grim / scrot / maim / gnome-screenshot / imagemagick' };
155
+ }
156
+ if (result.code !== 0) {
157
+ return { ok: false, error: `${backend} exited ${result.code}`, details: { stderr: result.stderr.slice(0, 500) } };
158
+ }
159
+ if (!existsSync(outPath)) {
160
+ return { ok: false, error: `${backend} ran but produced no output at ${outPath}` };
161
+ }
162
+ const bytes = await fsp.readFile(outPath);
163
+ const { width, height } = await pngDimensions(bytes);
164
+ return {
165
+ ok: true,
166
+ base64_png: bytes.toString('base64'),
167
+ width, height,
168
+ saved_path: outPath,
169
+ platform: 'linux',
170
+ backend,
171
+ };
172
+ }
173
+ async function pngDimensions(bytes) {
174
+ if (bytes.length < 24)
175
+ return { width: 0, height: 0 };
176
+ if (bytes.toString('ascii', 12, 16) !== 'IHDR')
177
+ return { width: 0, height: 0 };
178
+ return {
179
+ width: bytes.readUInt32BE(16),
180
+ height: bytes.readUInt32BE(20),
181
+ };
182
+ }
183
+ export async function screenshotWindow(args) {
184
+ const plat = hostPlatform();
185
+ if (!args.title_pattern) {
186
+ return { ok: false, error: 'title_pattern is required' };
187
+ }
188
+ try {
189
+ if (plat === 'win32')
190
+ return await screenshotWindowWin(args);
191
+ if (plat === 'darwin')
192
+ return await screenshotWindowMac(args);
193
+ return await screenshotWindowLinux(args);
194
+ }
195
+ catch (err) {
196
+ return { ok: false, error: `window screenshot failed: ${err?.message || err}` };
197
+ }
198
+ }
199
+ async function screenshotWindowWin(args) {
200
+ const tmpDir = makeTempDir('winshot');
201
+ const outPath = args.save_path || join(tmpDir, 'window.png');
202
+ const focus = args.focus_first !== false;
203
+ const script = `${WIN32_TYPE}
204
+ $ErrorActionPreference = "Stop"
205
+ $pattern = '${args.title_pattern.replace(/'/g, "''")}'
206
+ $rx = [System.Text.RegularExpressions.Regex]::new($pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
207
+ $matches = New-Object System.Collections.ArrayList
208
+ $cb = [AwbHostWin32+EnumWindowsProc]{
209
+ param($h,$l)
210
+ if (-not [AwbHostWin32]::IsWindowVisible($h)) { return $true }
211
+ $sb = New-Object System.Text.StringBuilder 1024
212
+ [void][AwbHostWin32]::GetWindowText($h, $sb, $sb.Capacity)
213
+ $title = $sb.ToString().Trim()
214
+ if ([string]::IsNullOrWhiteSpace($title)) { return $true }
215
+ if ($rx.IsMatch($title)) { [void]$matches.Add([pscustomobject]@{ H=$h; T=$title }) }
216
+ return $true
217
+ }
218
+ [void][AwbHostWin32]::EnumWindows($cb, [IntPtr]::Zero)
219
+ if ($matches.Count -eq 0) { Write-Error "no window matched"; exit 2 }
220
+ $target = $matches[0]
221
+ if (${focus ? '$true' : '$false'}) {
222
+ [void][AwbHostWin32]::ShowWindowAsync($target.H, 9)
223
+ Start-Sleep -Milliseconds 200
224
+ [void][AwbHostWin32]::BringWindowToTop($target.H)
225
+ [void][AwbHostWin32]::SetForegroundWindow($target.H)
226
+ Start-Sleep -Milliseconds 250
227
+ }
228
+ $rect = New-Object AwbHostWin32+RECT
229
+ if (-not [AwbHostWin32]::GetWindowRect($target.H, [ref]$rect)) { Write-Error "GetWindowRect failed"; exit 3 }
230
+ $w = $rect.Right - $rect.Left
231
+ $h = $rect.Bottom - $rect.Top
232
+ if ($w -le 0 -or $h -le 0) { Write-Error "window has invalid bounds"; exit 4 }
233
+ $bmp = New-Object System.Drawing.Bitmap $w, $h
234
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
235
+ $g.CopyFromScreen($rect.Left, $rect.Top, 0, 0, $bmp.Size)
236
+ $bmp.Save('${outPath.replace(/'/g, "''").replace(/\\/g, '\\\\')}', [System.Drawing.Imaging.ImageFormat]::Png)
237
+ $bmp.Dispose(); $g.Dispose()
238
+ Write-Output ("{0}|{1}|{2}|{3}|{4}" -f $target.T, $rect.Left, $rect.Top, $w, $h)
239
+ `;
240
+ const result = await runPowerShell(script, { timeoutMs: 20_000 });
241
+ if (result.spawnFailed) {
242
+ return { ok: false, error: 'PowerShell not available on PATH' };
243
+ }
244
+ if (result.code !== 0) {
245
+ return { ok: false, error: `window screenshot powershell exited ${result.code}`, details: { stderr: result.stderr.slice(0, 500) } };
246
+ }
247
+ const parts = result.stdout.trim().split('|');
248
+ const width = parseInt(parts[3] || '0', 10);
249
+ const height = parseInt(parts[4] || '0', 10);
250
+ const bytes = await fsp.readFile(outPath);
251
+ return {
252
+ ok: true,
253
+ base64_png: bytes.toString('base64'),
254
+ width, height,
255
+ saved_path: outPath,
256
+ platform: 'win32',
257
+ backend: 'powershell+System.Drawing',
258
+ };
259
+ }
260
+ async function screenshotWindowMac(args) {
261
+ const tmpDir = makeTempDir('winshot');
262
+ const outPath = args.save_path || join(tmpDir, 'window.png');
263
+ const focusScript = `
264
+ on findAndFocus(pat)
265
+ tell application "System Events"
266
+ set procs to (every process whose visible is true)
267
+ repeat with p in procs
268
+ set ws to (every window of p)
269
+ repeat with w in ws
270
+ try
271
+ set t to name of w
272
+ on error
273
+ set t to ""
274
+ end try
275
+ if t is not "" and (do shell script "echo " & quoted form of t & " | /usr/bin/grep -i " & quoted form of pat & " > /dev/null 2>&1; echo $?") is "0" then
276
+ set frontmost of p to true
277
+ delay 0.2
278
+ set posn to position of w
279
+ set sz to size of w
280
+ return (name of p) & "|" & t & "|" & (item 1 of posn) & "|" & (item 2 of posn) & "|" & (item 1 of sz) & "|" & (item 2 of sz)
281
+ end if
282
+ end repeat
283
+ end repeat
284
+ end tell
285
+ return ""
286
+ end findAndFocus
287
+ findAndFocus("${args.title_pattern.replace(/"/g, '\\"')}")
288
+ `;
289
+ const focusResult = await runOsascript(focusScript, { timeoutMs: 10_000 });
290
+ if (focusResult.spawnFailed) {
291
+ return { ok: false, error: 'osascript not available' };
292
+ }
293
+ const line = focusResult.stdout.trim();
294
+ if (!line) {
295
+ return { ok: false, error: `no window matched /${args.title_pattern}/i` };
296
+ }
297
+ const [, , xs, ys, ws, hs] = line.split('|');
298
+ const x = parseInt(xs, 10), y = parseInt(ys, 10), w = parseInt(ws, 10), h = parseInt(hs, 10);
299
+ if (!Number.isFinite(x) || !Number.isFinite(y) || w <= 0 || h <= 0) {
300
+ return { ok: false, error: `osascript returned malformed window geometry: ${line}` };
301
+ }
302
+ const capResult = await runCommand('screencapture', ['-x', '-t', 'png', '-R', `${x},${y},${w},${h}`, outPath], { timeoutMs: 10_000 });
303
+ if (capResult.code !== 0) {
304
+ return { ok: false, error: `screencapture region exited ${capResult.code}`, details: { stderr: capResult.stderr.slice(0, 500) } };
305
+ }
306
+ const bytes = await fsp.readFile(outPath);
307
+ const dims = await pngDimensions(bytes);
308
+ return {
309
+ ok: true,
310
+ base64_png: bytes.toString('base64'),
311
+ width: dims.width || w,
312
+ height: dims.height || h,
313
+ saved_path: outPath,
314
+ platform: 'darwin',
315
+ backend: 'osascript+screencapture',
316
+ };
317
+ }
318
+ async function screenshotWindowLinux(args) {
319
+ if (isWayland()) {
320
+ const full = await screenshotLinux({ save_path: args.save_path });
321
+ if (full.ok) {
322
+ return { ...full, backend: full.backend + '+wayland-fallback-fullscreen' };
323
+ }
324
+ return full;
325
+ }
326
+ const tmpDir = makeTempDir('winshot');
327
+ const outPath = args.save_path || join(tmpDir, 'window.png');
328
+ const xdotool = await findBinary(['xdotool']);
329
+ let windowId = null;
330
+ if (xdotool) {
331
+ const search = await runCommand('xdotool', ['search', '--name', args.title_pattern], { timeoutMs: 5000 });
332
+ if (!search.spawnFailed && search.code === 0) {
333
+ const lines = search.stdout.split('\n').filter(Boolean);
334
+ if (lines.length > 0) {
335
+ windowId = lines[0].trim();
336
+ if (args.focus_first !== false) {
337
+ await runCommand('xdotool', ['windowactivate', '--sync', windowId], { timeoutMs: 5000 });
338
+ }
339
+ }
340
+ }
341
+ }
342
+ if (!windowId) {
343
+ const wmctrl = await findBinary(['wmctrl']);
344
+ if (wmctrl) {
345
+ const list = await runCommand('wmctrl', ['-l'], { timeoutMs: 5000 });
346
+ if (list.code === 0) {
347
+ const rx = new RegExp(args.title_pattern, 'i');
348
+ for (const line of list.stdout.split('\n')) {
349
+ const cols = line.split(/\s+/);
350
+ const wid = cols[0];
351
+ const title = cols.slice(3).join(' ');
352
+ if (wid && rx.test(title)) {
353
+ windowId = wid;
354
+ if (args.focus_first !== false) {
355
+ await runCommand('wmctrl', ['-i', '-a', windowId], { timeoutMs: 5000 });
356
+ }
357
+ break;
358
+ }
359
+ }
360
+ }
361
+ }
362
+ }
363
+ if (!windowId) {
364
+ return { ok: false, error: `no X11 window matched /${args.title_pattern}/i`, hint: 'Install xdotool or wmctrl' };
365
+ }
366
+ const imBin = await findBinary(['import']);
367
+ if (!imBin) {
368
+ return { ok: false, error: 'imagemagick `import` not available for per-window capture', hint: 'Install imagemagick' };
369
+ }
370
+ const cap = await runCommand('import', ['-window', windowId, outPath], { timeoutMs: 10_000 });
371
+ if (cap.code !== 0) {
372
+ return { ok: false, error: `import exited ${cap.code}`, details: { stderr: cap.stderr.slice(0, 500) } };
373
+ }
374
+ const bytes = await fsp.readFile(outPath);
375
+ const dims = await pngDimensions(bytes);
376
+ return {
377
+ ok: true,
378
+ base64_png: bytes.toString('base64'),
379
+ width: dims.width, height: dims.height,
380
+ saved_path: outPath,
381
+ platform: 'linux',
382
+ backend: `${xdotool ? 'xdotool' : 'wmctrl'}+import`,
383
+ };
384
+ }
385
+ export async function listWindows(args = {}) {
386
+ const plat = hostPlatform();
387
+ try {
388
+ let windows;
389
+ if (plat === 'win32')
390
+ windows = await listWindowsWin(args);
391
+ else if (plat === 'darwin')
392
+ windows = await listWindowsMac(args);
393
+ else
394
+ windows = await listWindowsLinux(args);
395
+ if (args.title_pattern) {
396
+ const rx = new RegExp(args.title_pattern, 'i');
397
+ windows = windows.filter((w) => rx.test(w.title || ''));
398
+ }
399
+ return { ok: true, windows, platform: plat };
400
+ }
401
+ catch (err) {
402
+ return { ok: false, error: `list_windows failed: ${err?.message || err}` };
403
+ }
404
+ }
405
+ async function listWindowsWin(args) {
406
+ const includeHidden = !!args.include_hidden;
407
+ const script = `${WIN32_TYPE}
408
+ $results = New-Object System.Collections.ArrayList
409
+ $cb = [AwbHostWin32+EnumWindowsProc]{
410
+ param($h,$l)
411
+ $vis = [AwbHostWin32]::IsWindowVisible($h)
412
+ if ((-not $vis) -and (-not ${includeHidden ? '$true' : '$false'})) { return $true }
413
+ $sb = New-Object System.Text.StringBuilder 2048
414
+ [void][AwbHostWin32]::GetWindowText($h, $sb, $sb.Capacity)
415
+ $title = $sb.ToString().Trim()
416
+ if ([string]::IsNullOrWhiteSpace($title)) { return $true }
417
+ $rect = New-Object AwbHostWin32+RECT
418
+ [void][AwbHostWin32]::GetWindowRect($h, [ref]$rect)
419
+ $pid = 0
420
+ [void][AwbHostWin32]::GetWindowThreadProcessId($h, [ref]$pid)
421
+ $fg = [AwbHostWin32]::GetForegroundWindow()
422
+ $obj = [pscustomobject]@{
423
+ id = $h.ToInt64()
424
+ title = $title
425
+ pid = [int]$pid
426
+ x = $rect.Left
427
+ y = $rect.Top
428
+ width = ($rect.Right - $rect.Left)
429
+ height = ($rect.Bottom - $rect.Top)
430
+ focused = ($fg -eq $h)
431
+ }
432
+ [void]$results.Add($obj)
433
+ return $true
434
+ }
435
+ [void][AwbHostWin32]::EnumWindows($cb, [IntPtr]::Zero)
436
+ $results | ConvertTo-Json -Depth 5 -Compress
437
+ `;
438
+ const result = await runPowerShell(script, { timeoutMs: 10_000 });
439
+ if (result.spawnFailed || result.code !== 0)
440
+ return [];
441
+ const raw = result.stdout.trim();
442
+ if (!raw)
443
+ return [];
444
+ let parsed;
445
+ try {
446
+ parsed = JSON.parse(raw);
447
+ }
448
+ catch {
449
+ return [];
450
+ }
451
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
452
+ return arr.map((w) => ({
453
+ id: String(w.id),
454
+ title: String(w.title),
455
+ pid: Number(w.pid),
456
+ x: Number(w.x), y: Number(w.y),
457
+ width: Number(w.width), height: Number(w.height),
458
+ focused: !!w.focused,
459
+ }));
460
+ }
461
+ async function listWindowsMac(args) {
462
+ void args;
463
+ const script = `
464
+ set out to ""
465
+ tell application "System Events"
466
+ set procs to (every process whose visible is true)
467
+ repeat with p in procs
468
+ set pn to name of p
469
+ set ppid to unix id of p
470
+ try
471
+ set ws to every window of p
472
+ on error
473
+ set ws to {}
474
+ end try
475
+ repeat with w in ws
476
+ try
477
+ set t to name of w
478
+ on error
479
+ set t to ""
480
+ end try
481
+ try
482
+ set ps to position of w
483
+ set sz to size of w
484
+ set out to out & pn & "\\t" & ppid & "\\t" & t & "\\t" & (item 1 of ps) & "\\t" & (item 2 of ps) & "\\t" & (item 1 of sz) & "\\t" & (item 2 of sz) & "\\n"
485
+ on error
486
+ set out to out & pn & "\\t" & ppid & "\\t" & t & "\\t\\t\\t\\t\\n"
487
+ end try
488
+ end repeat
489
+ end repeat
490
+ end tell
491
+ return out
492
+ `;
493
+ const result = await runOsascript(script, { timeoutMs: 15_000 });
494
+ if (result.spawnFailed || result.code !== 0)
495
+ return [];
496
+ const out = [];
497
+ for (const line of result.stdout.split('\n')) {
498
+ const cols = line.split('\t');
499
+ if (cols.length < 7)
500
+ continue;
501
+ const [app, pidStr, title, xs, ys, ws, hs] = cols;
502
+ if (!app || !title)
503
+ continue;
504
+ out.push({
505
+ app,
506
+ title,
507
+ pid: parseInt(pidStr, 10) || undefined,
508
+ x: parseInt(xs, 10) || 0,
509
+ y: parseInt(ys, 10) || 0,
510
+ width: parseInt(ws, 10) || 0,
511
+ height: parseInt(hs, 10) || 0,
512
+ });
513
+ }
514
+ return out;
515
+ }
516
+ async function listWindowsLinux(args) {
517
+ void args;
518
+ if (isWayland()) {
519
+ return [];
520
+ }
521
+ const wmctrl = await findBinary(['wmctrl']);
522
+ if (!wmctrl)
523
+ return [];
524
+ const result = await runCommand('wmctrl', ['-l', '-G', '-p'], { timeoutMs: 5000 });
525
+ if (result.code !== 0)
526
+ return [];
527
+ const out = [];
528
+ for (const line of result.stdout.split('\n')) {
529
+ const m = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line);
530
+ if (!m)
531
+ continue;
532
+ const [, id, , pid, x, y, w, h, host, ...titleParts] = m;
533
+ out.push({
534
+ id,
535
+ title: titleParts.join(' ').trim() || m[9],
536
+ pid: parseInt(pid, 10) || undefined,
537
+ x: parseInt(x, 10),
538
+ y: parseInt(y, 10),
539
+ width: parseInt(w, 10),
540
+ height: parseInt(h, 10),
541
+ app: host,
542
+ });
543
+ }
544
+ return out;
545
+ }
546
+ export async function focusWindow(args) {
547
+ if (!args.title_pattern)
548
+ return { ok: false, error: 'title_pattern is required' };
549
+ const plat = hostPlatform();
550
+ try {
551
+ if (plat === 'win32')
552
+ return await focusWindowWin(args);
553
+ if (plat === 'darwin')
554
+ return await focusWindowMac(args);
555
+ return await focusWindowLinux(args);
556
+ }
557
+ catch (err) {
558
+ return { ok: false, error: `focus_window failed: ${err?.message || err}` };
559
+ }
560
+ }
561
+ async function focusWindowWin(args) {
562
+ const script = `${WIN32_TYPE}
563
+ $ErrorActionPreference = "Stop"
564
+ $pattern = '${args.title_pattern.replace(/'/g, "''")}'
565
+ $rx = [System.Text.RegularExpressions.Regex]::new($pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
566
+ $found = $null
567
+ $cb = [AwbHostWin32+EnumWindowsProc]{
568
+ param($h,$l)
569
+ if (-not [AwbHostWin32]::IsWindowVisible($h)) { return $true }
570
+ $sb = New-Object System.Text.StringBuilder 1024
571
+ [void][AwbHostWin32]::GetWindowText($h, $sb, $sb.Capacity)
572
+ $title = $sb.ToString().Trim()
573
+ if ([string]::IsNullOrWhiteSpace($title)) { return $true }
574
+ if ($rx.IsMatch($title) -and ($null -eq $script:found)) {
575
+ $script:found = [pscustomobject]@{ H = $h; T = $title }
576
+ }
577
+ return $true
578
+ }
579
+ [void][AwbHostWin32]::EnumWindows($cb, [IntPtr]::Zero)
580
+ if ($null -eq $script:found) { Write-Error "no match"; exit 2 }
581
+ [void][AwbHostWin32]::ShowWindowAsync($script:found.H, 9)
582
+ Start-Sleep -Milliseconds 200
583
+ [void][AwbHostWin32]::BringWindowToTop($script:found.H)
584
+ [void][AwbHostWin32]::SetForegroundWindow($script:found.H)
585
+ Write-Output $script:found.T
586
+ `;
587
+ const result = await runPowerShell(script, { timeoutMs: 8000 });
588
+ if (result.spawnFailed)
589
+ return { ok: false, error: 'PowerShell not available' };
590
+ if (result.code !== 0)
591
+ return { ok: false, error: `no window matched /${args.title_pattern}/i` };
592
+ return { ok: true, title: result.stdout.trim(), platform: 'win32' };
593
+ }
594
+ async function focusWindowMac(args) {
595
+ const script = `
596
+ on doFocus(pat)
597
+ tell application "System Events"
598
+ repeat with p in (every process whose visible is true)
599
+ try
600
+ set ws to (every window of p)
601
+ on error
602
+ set ws to {}
603
+ end try
604
+ repeat with w in ws
605
+ try
606
+ set t to name of w
607
+ on error
608
+ set t to ""
609
+ end try
610
+ if t is not "" and (do shell script "echo " & quoted form of t & " | /usr/bin/grep -i " & quoted form of pat & " > /dev/null 2>&1; echo $?") is "0" then
611
+ set frontmost of p to true
612
+ return t
613
+ end if
614
+ end repeat
615
+ end repeat
616
+ end tell
617
+ return ""
618
+ end doFocus
619
+ doFocus("${args.title_pattern.replace(/"/g, '\\"')}")
620
+ `;
621
+ const result = await runOsascript(script, { timeoutMs: 10_000 });
622
+ if (result.spawnFailed)
623
+ return { ok: false, error: 'osascript not available' };
624
+ const title = result.stdout.trim();
625
+ if (!title)
626
+ return { ok: false, error: `no window matched /${args.title_pattern}/i` };
627
+ return { ok: true, title, platform: 'darwin' };
628
+ }
629
+ async function focusWindowLinux(args) {
630
+ if (isWayland()) {
631
+ return { ok: false, error: 'window focus is not supported on Wayland (no portable API)', hint: 'Run under X11 for window focus tools' };
632
+ }
633
+ const xdotool = await findBinary(['xdotool']);
634
+ if (xdotool) {
635
+ const search = await runCommand('xdotool', ['search', '--name', args.title_pattern], { timeoutMs: 5000 });
636
+ if (!search.spawnFailed && search.code === 0) {
637
+ const lines = search.stdout.split('\n').filter(Boolean);
638
+ if (lines.length > 0) {
639
+ const id = lines[0].trim();
640
+ const act = await runCommand('xdotool', ['windowactivate', '--sync', id], { timeoutMs: 5000 });
641
+ if (act.code === 0) {
642
+ const nameRes = await runCommand('xdotool', ['getwindowname', id], { timeoutMs: 5000 });
643
+ return { ok: true, title: nameRes.stdout.trim() || `xid:${id}`, platform: 'linux' };
644
+ }
645
+ }
646
+ }
647
+ }
648
+ const wmctrl = await findBinary(['wmctrl']);
649
+ if (wmctrl) {
650
+ const res = await runCommand('wmctrl', ['-a', args.title_pattern], { timeoutMs: 5000 });
651
+ if (res.code === 0)
652
+ return { ok: true, title: args.title_pattern, platform: 'linux' };
653
+ }
654
+ return { ok: false, error: 'no window focus backend (need xdotool or wmctrl)' };
655
+ }
656
+ export async function sendKeys(args) {
657
+ if (!args.keys)
658
+ return { ok: false, error: 'keys is required' };
659
+ const plat = hostPlatform();
660
+ try {
661
+ if (args.focus_window) {
662
+ const focused = await focusWindow({ title_pattern: args.focus_window });
663
+ if (!focused.ok)
664
+ return focused;
665
+ }
666
+ if (plat === 'win32')
667
+ return await sendKeysWin(args);
668
+ if (plat === 'darwin')
669
+ return await sendKeysMac(args);
670
+ return await sendKeysLinux(args);
671
+ }
672
+ catch (err) {
673
+ return { ok: false, error: `send_keys failed: ${err?.message || err}` };
674
+ }
675
+ }
676
+ async function sendKeysWin(args) {
677
+ let payload;
678
+ if (args.chord) {
679
+ payload = chordToSendKeys(args.keys);
680
+ }
681
+ else {
682
+ payload = escapeSendKeysText(args.keys);
683
+ }
684
+ const script = `
685
+ Add-Type -AssemblyName System.Windows.Forms
686
+ [System.Windows.Forms.SendKeys]::SendWait('${payload.replace(/'/g, "''")}')
687
+ Write-Output "ok"
688
+ `;
689
+ const result = await runPowerShell(script, { timeoutMs: 5000 });
690
+ if (result.spawnFailed)
691
+ return { ok: false, error: 'PowerShell not available' };
692
+ if (result.code !== 0)
693
+ return { ok: false, error: `send_keys powershell exited ${result.code}`, details: { stderr: result.stderr.slice(0, 500) } };
694
+ return { ok: true, platform: 'win32' };
695
+ }
696
+ function chordToSendKeys(chord) {
697
+ const parts = chord.toLowerCase().split('+').map((s) => s.trim()).filter(Boolean);
698
+ const keys = [];
699
+ let modifiers = '';
700
+ for (const p of parts) {
701
+ if (p === 'ctrl' || p === 'control')
702
+ modifiers += '^';
703
+ else if (p === 'shift')
704
+ modifiers += '+';
705
+ else if (p === 'alt' || p === 'option')
706
+ modifiers += '%';
707
+ else if (p === 'cmd' || p === 'meta' || p === 'win')
708
+ modifiers += '^';
709
+ else
710
+ keys.push(p);
711
+ }
712
+ const named = {
713
+ enter: '{ENTER}', return: '{ENTER}',
714
+ escape: '{ESC}', esc: '{ESC}',
715
+ tab: '{TAB}', space: ' ',
716
+ backspace: '{BS}', delete: '{DEL}', del: '{DEL}',
717
+ up: '{UP}', down: '{DOWN}', left: '{LEFT}', right: '{RIGHT}',
718
+ home: '{HOME}', end: '{END}',
719
+ pageup: '{PGUP}', pagedown: '{PGDN}',
720
+ f1: '{F1}', f2: '{F2}', f3: '{F3}', f4: '{F4}', f5: '{F5}',
721
+ f6: '{F6}', f7: '{F7}', f8: '{F8}', f9: '{F9}', f10: '{F10}',
722
+ f11: '{F11}', f12: '{F12}',
723
+ };
724
+ const last = keys[keys.length - 1] || '';
725
+ const keyOut = named[last] ?? (last.length === 1 ? last : `{${last.toUpperCase()}}`);
726
+ return modifiers + keyOut;
727
+ }
728
+ function escapeSendKeysText(text) {
729
+ return text.replace(/[+^%~(){}\[\]]/g, (c) => `{${c}}`);
730
+ }
731
+ async function sendKeysMac(args) {
732
+ let script;
733
+ if (args.chord) {
734
+ const { key, modifiers } = parseChordForApple(args.keys);
735
+ const modStr = modifiers.length ? ` using {${modifiers.join(', ')}}` : '';
736
+ script = `tell application "System Events" to keystroke "${key.replace(/"/g, '\\"')}"${modStr}`;
737
+ }
738
+ else {
739
+ script = `tell application "System Events" to keystroke "${args.keys.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
740
+ }
741
+ const result = await runOsascript(script, { timeoutMs: 8000 });
742
+ if (result.spawnFailed)
743
+ return { ok: false, error: 'osascript not available' };
744
+ if (result.code !== 0)
745
+ return { ok: false, error: `osascript exited ${result.code}`, details: { stderr: result.stderr.slice(0, 500) } };
746
+ return { ok: true, platform: 'darwin' };
747
+ }
748
+ function parseChordForApple(chord) {
749
+ const parts = chord.toLowerCase().split('+').map((s) => s.trim()).filter(Boolean);
750
+ const mods = [];
751
+ let key = '';
752
+ for (const p of parts) {
753
+ if (p === 'ctrl' || p === 'control')
754
+ mods.push('control down');
755
+ else if (p === 'shift')
756
+ mods.push('shift down');
757
+ else if (p === 'alt' || p === 'option')
758
+ mods.push('option down');
759
+ else if (p === 'cmd' || p === 'meta' || p === 'win')
760
+ mods.push('command down');
761
+ else
762
+ key = p;
763
+ }
764
+ return { key, modifiers: mods };
765
+ }
766
+ async function sendKeysLinux(args) {
767
+ if (isWayland()) {
768
+ const wtype = await findBinary(['wtype']);
769
+ if (wtype) {
770
+ if (args.chord) {
771
+ const { key, modifiers } = parseChordForWtype(args.keys);
772
+ const argv = [];
773
+ for (const m of modifiers)
774
+ argv.push('-M', m);
775
+ argv.push('-k', key);
776
+ const res = await runCommand('wtype', argv, { timeoutMs: 5000 });
777
+ if (res.code !== 0)
778
+ return { ok: false, error: `wtype exited ${res.code}` };
779
+ return { ok: true, platform: 'linux' };
780
+ }
781
+ const res = await runCommand('wtype', ['--', args.keys], { timeoutMs: 5000 });
782
+ if (res.code !== 0)
783
+ return { ok: false, error: `wtype exited ${res.code}` };
784
+ return { ok: true, platform: 'linux' };
785
+ }
786
+ return { ok: false, error: 'wtype not installed (Wayland input requires wtype or ydotool)' };
787
+ }
788
+ const xdotool = await findBinary(['xdotool']);
789
+ if (!xdotool)
790
+ return { ok: false, error: 'xdotool not installed' };
791
+ if (args.chord) {
792
+ const chord = args.keys.replace(/cmd|meta|win/gi, 'super').replace(/option/gi, 'alt');
793
+ const res = await runCommand('xdotool', ['key', chord], { timeoutMs: 5000 });
794
+ if (res.code !== 0)
795
+ return { ok: false, error: `xdotool key exited ${res.code}` };
796
+ return { ok: true, platform: 'linux' };
797
+ }
798
+ const res = await runCommand('xdotool', ['type', '--delay', '20', '--', args.keys], { timeoutMs: 15_000 });
799
+ if (res.code !== 0)
800
+ return { ok: false, error: `xdotool type exited ${res.code}` };
801
+ return { ok: true, platform: 'linux' };
802
+ }
803
+ function parseChordForWtype(chord) {
804
+ const parts = chord.toLowerCase().split('+').map((s) => s.trim()).filter(Boolean);
805
+ const mods = [];
806
+ let key = '';
807
+ for (const p of parts) {
808
+ if (p === 'ctrl' || p === 'control')
809
+ mods.push('ctrl');
810
+ else if (p === 'shift')
811
+ mods.push('shift');
812
+ else if (p === 'alt' || p === 'option')
813
+ mods.push('alt');
814
+ else if (p === 'cmd' || p === 'meta' || p === 'win')
815
+ mods.push('logo');
816
+ else
817
+ key = p;
818
+ }
819
+ return { key, modifiers: mods };
820
+ }
821
+ export async function mouseClick(args) {
822
+ if (typeof args.x !== 'number' || typeof args.y !== 'number') {
823
+ return { ok: false, error: 'x and y are required numbers' };
824
+ }
825
+ const plat = hostPlatform();
826
+ const button = args.button ?? 'left';
827
+ const count = Math.max(1, args.count ?? 1);
828
+ try {
829
+ if (plat === 'win32')
830
+ return await mouseClickWin(args.x, args.y, button, count);
831
+ if (plat === 'darwin')
832
+ return await mouseClickMac(args.x, args.y, button, count);
833
+ return await mouseClickLinux(args.x, args.y, button, count);
834
+ }
835
+ catch (err) {
836
+ return { ok: false, error: `mouse_click failed: ${err?.message || err}` };
837
+ }
838
+ }
839
+ async function mouseClickWin(x, y, button, count) {
840
+ const downConst = button === 'right' ? 'MOUSE_RIGHTDOWN' : button === 'middle' ? 'MOUSE_MIDDOWN' : 'MOUSE_LEFTDOWN';
841
+ const upConst = button === 'right' ? 'MOUSE_RIGHTUP' : button === 'middle' ? 'MOUSE_MIDUP' : 'MOUSE_LEFTUP';
842
+ const script = `${WIN32_TYPE}
843
+ [AwbHostWin32]::SetCursorPos(${x}, ${y})
844
+ Start-Sleep -Milliseconds 50
845
+ for ($i = 0; $i -lt ${count}; $i++) {
846
+ [AwbHostWin32]::mouse_event([AwbHostWin32]::${downConst}, 0, 0, 0, 0)
847
+ [AwbHostWin32]::mouse_event([AwbHostWin32]::${upConst}, 0, 0, 0, 0)
848
+ if ($i -lt (${count} - 1)) { Start-Sleep -Milliseconds 60 }
849
+ }
850
+ Write-Output "ok"
851
+ `;
852
+ const result = await runPowerShell(script, { timeoutMs: 5000 });
853
+ if (result.spawnFailed)
854
+ return { ok: false, error: 'PowerShell not available' };
855
+ if (result.code !== 0)
856
+ return { ok: false, error: `mouse_click powershell exited ${result.code}` };
857
+ return { ok: true, platform: 'win32' };
858
+ }
859
+ async function mouseClickMac(x, y, button, count) {
860
+ const cliclick = await findBinary(['cliclick']);
861
+ if (cliclick) {
862
+ const cmd = button === 'right' ? 'rc' : button === 'middle' ? 'mc' : count >= 2 ? 'dc' : 'c';
863
+ const res = await runCommand('cliclick', [`${cmd}:${x},${y}`], { timeoutMs: 5000 });
864
+ if (res.code !== 0)
865
+ return { ok: false, error: `cliclick exited ${res.code}` };
866
+ return { ok: true, platform: 'darwin' };
867
+ }
868
+ if (button !== 'left')
869
+ return { ok: false, error: `${button} click on macOS requires cliclick`, hint: 'brew install cliclick' };
870
+ const script = `do shell script "/usr/bin/python3 -c 'import Quartz, sys; x=int(sys.argv[1]); y=int(sys.argv[2]); n=int(sys.argv[3]);
871
+ import Quartz.CoreGraphics as CG
872
+ for i in range(n):
873
+ e1 = CG.CGEventCreateMouseEvent(None, CG.kCGEventLeftMouseDown, (x,y), CG.kCGMouseButtonLeft);
874
+ e2 = CG.CGEventCreateMouseEvent(None, CG.kCGEventLeftMouseUp, (x,y), CG.kCGMouseButtonLeft);
875
+ CG.CGEventPost(CG.kCGHIDEventTap, e1); CG.CGEventPost(CG.kCGHIDEventTap, e2);
876
+ ' ${x} ${y} ${count}"`;
877
+ const result = await runOsascript(script, { timeoutMs: 8000 });
878
+ if (result.code !== 0)
879
+ return { ok: false, error: 'mouse_click failed; install cliclick or expose Quartz via pyobjc' };
880
+ return { ok: true, platform: 'darwin' };
881
+ }
882
+ async function mouseClickLinux(x, y, button, count) {
883
+ if (isWayland()) {
884
+ const ydotool = await findBinary(['ydotool']);
885
+ if (!ydotool)
886
+ return { ok: false, error: 'ydotool not installed (Wayland mouse needs ydotool)' };
887
+ const btn = button === 'right' ? '0xC1' : button === 'middle' ? '0xC2' : '0xC0';
888
+ const moveRes = await runCommand('ydotool', ['mousemove', '--absolute', '--', String(x), String(y)], { timeoutMs: 5000 });
889
+ if (moveRes.code !== 0)
890
+ return { ok: false, error: `ydotool mousemove exited ${moveRes.code}` };
891
+ for (let i = 0; i < count; i++) {
892
+ const clickRes = await runCommand('ydotool', ['click', btn], { timeoutMs: 3000 });
893
+ if (clickRes.code !== 0)
894
+ return { ok: false, error: `ydotool click exited ${clickRes.code}` };
895
+ }
896
+ return { ok: true, platform: 'linux' };
897
+ }
898
+ const xdotool = await findBinary(['xdotool']);
899
+ if (!xdotool)
900
+ return { ok: false, error: 'xdotool not installed' };
901
+ const btn = button === 'right' ? '3' : button === 'middle' ? '2' : '1';
902
+ const move = await runCommand('xdotool', ['mousemove', String(x), String(y)], { timeoutMs: 5000 });
903
+ if (move.code !== 0)
904
+ return { ok: false, error: `xdotool mousemove exited ${move.code}` };
905
+ for (let i = 0; i < count; i++) {
906
+ const click = await runCommand('xdotool', ['click', btn], { timeoutMs: 3000 });
907
+ if (click.code !== 0)
908
+ return { ok: false, error: `xdotool click exited ${click.code}` };
909
+ }
910
+ return { ok: true, platform: 'linux' };
911
+ }
912
+ export async function mouseMove(args) {
913
+ const plat = hostPlatform();
914
+ if (typeof args.x !== 'number' || typeof args.y !== 'number') {
915
+ return { ok: false, error: 'x and y are required numbers' };
916
+ }
917
+ try {
918
+ if (plat === 'win32') {
919
+ const script = `${WIN32_TYPE}\n[AwbHostWin32]::SetCursorPos(${args.x}, ${args.y})\nWrite-Output "ok"`;
920
+ const res = await runPowerShell(script, { timeoutMs: 5000 });
921
+ if (res.code !== 0)
922
+ return { ok: false, error: 'SetCursorPos failed' };
923
+ return { ok: true, platform: 'win32' };
924
+ }
925
+ if (plat === 'darwin') {
926
+ const cliclick = await findBinary(['cliclick']);
927
+ if (cliclick) {
928
+ const r = await runCommand('cliclick', [`m:${args.x},${args.y}`], { timeoutMs: 5000 });
929
+ return r.code === 0 ? { ok: true, platform: 'darwin' } : { ok: false, error: `cliclick exited ${r.code}` };
930
+ }
931
+ return { ok: false, error: 'cliclick not installed; brew install cliclick' };
932
+ }
933
+ if (isWayland()) {
934
+ const r = await runCommand('ydotool', ['mousemove', '--absolute', '--', String(args.x), String(args.y)], { timeoutMs: 5000 });
935
+ return r.code === 0 ? { ok: true, platform: 'linux' } : { ok: false, error: 'ydotool mousemove failed' };
936
+ }
937
+ const r = await runCommand('xdotool', ['mousemove', String(args.x), String(args.y)], { timeoutMs: 5000 });
938
+ return r.code === 0 ? { ok: true, platform: 'linux' } : { ok: false, error: 'xdotool mousemove failed' };
939
+ }
940
+ catch (err) {
941
+ return { ok: false, error: `mouse_move failed: ${err?.message || err}` };
942
+ }
943
+ }
944
+ export async function listProcesses(args = {}) {
945
+ const plat = hostPlatform();
946
+ try {
947
+ let procs;
948
+ if (plat === 'win32')
949
+ procs = await listProcessesWin();
950
+ else
951
+ procs = await listProcessesUnix();
952
+ if (args.name_pattern) {
953
+ const rx = new RegExp(args.name_pattern, 'i');
954
+ procs = procs.filter((p) => rx.test(p.name) || (p.cmdline && rx.test(p.cmdline)));
955
+ }
956
+ return { ok: true, processes: procs, platform: plat };
957
+ }
958
+ catch (err) {
959
+ return { ok: false, error: `list_processes failed: ${err?.message || err}` };
960
+ }
961
+ }
962
+ async function listProcessesWin() {
963
+ const script = `
964
+ Get-Process | ForEach-Object {
965
+ [pscustomobject]@{
966
+ pid = $_.Id
967
+ name = $_.ProcessName
968
+ mem_kb = [int]($_.WorkingSet64 / 1024)
969
+ cpu = $_.CPU
970
+ }
971
+ } | ConvertTo-Json -Depth 3 -Compress
972
+ `;
973
+ const res = await runPowerShell(script, { timeoutMs: 10_000 });
974
+ if (res.code !== 0)
975
+ return [];
976
+ try {
977
+ const parsed = JSON.parse(res.stdout);
978
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
979
+ return arr.map((p) => ({
980
+ pid: Number(p.pid),
981
+ name: String(p.name),
982
+ mem_kb: typeof p.mem_kb === 'number' ? p.mem_kb : undefined,
983
+ cpu: typeof p.cpu === 'number' ? p.cpu : undefined,
984
+ }));
985
+ }
986
+ catch {
987
+ return [];
988
+ }
989
+ }
990
+ async function listProcessesUnix() {
991
+ const res = await runCommand('ps', ['-eo', 'pid,ppid,user,pcpu,rss,comm'], { timeoutMs: 8000 });
992
+ if (res.code !== 0)
993
+ return [];
994
+ const out = [];
995
+ const lines = res.stdout.split('\n');
996
+ for (const line of lines.slice(1)) {
997
+ const cols = line.trim().split(/\s+/);
998
+ if (cols.length < 6)
999
+ continue;
1000
+ const [pidStr, ppidStr, user, cpuStr, rssStr, ...nameParts] = cols;
1001
+ out.push({
1002
+ pid: parseInt(pidStr, 10),
1003
+ ppid: parseInt(ppidStr, 10) || undefined,
1004
+ user,
1005
+ cpu: parseFloat(cpuStr),
1006
+ mem_kb: parseInt(rssStr, 10),
1007
+ name: nameParts.join(' '),
1008
+ });
1009
+ }
1010
+ return out;
1011
+ }
1012
+ export async function killProcess(args) {
1013
+ if (!args.pid && !args.name_pattern) {
1014
+ return { ok: false, error: 'either pid or name_pattern is required' };
1015
+ }
1016
+ const plat = hostPlatform();
1017
+ try {
1018
+ let targets = [];
1019
+ if (args.pid)
1020
+ targets.push(args.pid);
1021
+ if (args.name_pattern) {
1022
+ const proc = await listProcesses({ name_pattern: args.name_pattern });
1023
+ if (!proc.ok)
1024
+ return proc;
1025
+ for (const p of proc.processes)
1026
+ targets.push(p.pid);
1027
+ }
1028
+ targets = Array.from(new Set(targets));
1029
+ if (targets.length === 0) {
1030
+ return { ok: false, error: 'no matching processes' };
1031
+ }
1032
+ const killed = [];
1033
+ if (plat === 'win32') {
1034
+ for (const pid of targets) {
1035
+ const argv = args.force ? ['/PID', String(pid), '/F', '/T'] : ['/PID', String(pid), '/T'];
1036
+ const r = await runCommand('taskkill', argv, { timeoutMs: 5000 });
1037
+ if (r.code === 0)
1038
+ killed.push(pid);
1039
+ }
1040
+ }
1041
+ else {
1042
+ const sig = args.force ? 'SIGKILL' : 'SIGTERM';
1043
+ for (const pid of targets) {
1044
+ try {
1045
+ process.kill(pid, sig);
1046
+ killed.push(pid);
1047
+ }
1048
+ catch {
1049
+ }
1050
+ }
1051
+ }
1052
+ return { ok: true, killed, platform: plat };
1053
+ }
1054
+ catch (err) {
1055
+ return { ok: false, error: `kill_process failed: ${err?.message || err}` };
1056
+ }
1057
+ }
1058
+ export async function launchProcess(args) {
1059
+ if (!args.command)
1060
+ return { ok: false, error: 'command is required' };
1061
+ try {
1062
+ const { spawn } = await import('node:child_process');
1063
+ const child = spawn(args.command, args.args || [], {
1064
+ cwd: args.cwd,
1065
+ env: { ...process.env, ...(args.env || {}) },
1066
+ detached: args.detach !== false,
1067
+ stdio: 'ignore',
1068
+ });
1069
+ if (args.detach !== false) {
1070
+ child.unref();
1071
+ }
1072
+ return { ok: true, pid: child.pid ?? null, platform: hostPlatform() };
1073
+ }
1074
+ catch (err) {
1075
+ return { ok: false, error: `launch_process failed: ${err?.message || err}` };
1076
+ }
1077
+ }
1078
+ export async function waitForFile(args) {
1079
+ if (!args.path)
1080
+ return { ok: false, error: 'path is required' };
1081
+ const deadline = Date.now() + Math.max(1, args.timeout_seconds ?? 30) * 1000;
1082
+ const interval = Math.max(50, args.poll_ms ?? 500);
1083
+ const rx = args.content_pattern ? new RegExp(args.content_pattern) : null;
1084
+ while (Date.now() < deadline) {
1085
+ try {
1086
+ const st = statSync(args.path);
1087
+ const mtimeOk = !args.newer_than_ms || st.mtimeMs >= args.newer_than_ms;
1088
+ if (mtimeOk) {
1089
+ if (!rx) {
1090
+ return { ok: true, appeared_at: new Date(st.mtimeMs).toISOString(), size: st.size };
1091
+ }
1092
+ const content = (await fsp.readFile(args.path, 'utf8')).slice(0, 64 * 1024);
1093
+ if (rx.test(content)) {
1094
+ return { ok: true, appeared_at: new Date(st.mtimeMs).toISOString(), size: st.size, content };
1095
+ }
1096
+ }
1097
+ }
1098
+ catch {
1099
+ }
1100
+ await new Promise((res) => setTimeout(res, interval));
1101
+ }
1102
+ return { ok: false, error: `timed out after ${args.timeout_seconds ?? 30}s waiting for ${args.path}` };
1103
+ }
1104
+ export async function readFileTail(args) {
1105
+ if (!args.path)
1106
+ return { ok: false, error: 'path is required' };
1107
+ if (!existsSync(args.path))
1108
+ return { ok: false, error: `file not found: ${args.path}` };
1109
+ try {
1110
+ const st = statSync(args.path);
1111
+ const want = Math.min(1024 * 1024, Math.max(1, args.bytes ?? 8192));
1112
+ const start = Math.max(0, st.size - want);
1113
+ const fh = await fsp.open(args.path, 'r');
1114
+ try {
1115
+ const buf = Buffer.alloc(st.size - start);
1116
+ await fh.read(buf, 0, buf.length, start);
1117
+ return {
1118
+ ok: true,
1119
+ content: buf.toString('utf8'),
1120
+ size: st.size,
1121
+ mtime: new Date(st.mtimeMs).toISOString(),
1122
+ };
1123
+ }
1124
+ finally {
1125
+ await fh.close();
1126
+ }
1127
+ }
1128
+ catch (err) {
1129
+ return { ok: false, error: `read_file_tail failed: ${err?.message || err}` };
1130
+ }
1131
+ }
1132
+ export async function clipboardRead() {
1133
+ const plat = hostPlatform();
1134
+ try {
1135
+ if (plat === 'win32') {
1136
+ const res = await runPowerShell('Get-Clipboard -Raw', { timeoutMs: 5000 });
1137
+ if (res.code !== 0)
1138
+ return { ok: false, error: 'Get-Clipboard failed' };
1139
+ return { ok: true, text: res.stdout, platform: 'win32' };
1140
+ }
1141
+ if (plat === 'darwin') {
1142
+ const res = await runCommand('pbpaste', [], { timeoutMs: 5000 });
1143
+ if (res.spawnFailed)
1144
+ return { ok: false, error: 'pbpaste not available' };
1145
+ return { ok: true, text: res.stdout, platform: 'darwin' };
1146
+ }
1147
+ if (isWayland()) {
1148
+ const res = await runCommand('wl-paste', [], { timeoutMs: 5000 });
1149
+ if (res.spawnFailed)
1150
+ return { ok: false, error: 'wl-paste not installed' };
1151
+ return { ok: true, text: res.stdout, platform: 'linux' };
1152
+ }
1153
+ const xclip = await findBinary(['xclip', 'xsel']);
1154
+ if (!xclip)
1155
+ return { ok: false, error: 'xclip / xsel not installed' };
1156
+ const argv = xclip === 'xclip' ? ['-selection', 'clipboard', '-o'] : ['--clipboard', '--output'];
1157
+ const res = await runCommand(xclip, argv, { timeoutMs: 5000 });
1158
+ if (res.code !== 0)
1159
+ return { ok: false, error: `${xclip} exited ${res.code}` };
1160
+ return { ok: true, text: res.stdout, platform: 'linux' };
1161
+ }
1162
+ catch (err) {
1163
+ return { ok: false, error: `clipboard_read failed: ${err?.message || err}` };
1164
+ }
1165
+ }
1166
+ export async function clipboardWrite(args) {
1167
+ if (typeof args.text !== 'string')
1168
+ return { ok: false, error: 'text is required' };
1169
+ const plat = hostPlatform();
1170
+ try {
1171
+ if (plat === 'win32') {
1172
+ const script = `$input | Set-Clipboard`;
1173
+ const res = await runPowerShell(script, { timeoutMs: 5000, stdin: args.text });
1174
+ if (res.code !== 0)
1175
+ return { ok: false, error: 'Set-Clipboard failed' };
1176
+ return { ok: true, platform: 'win32' };
1177
+ }
1178
+ if (plat === 'darwin') {
1179
+ const res = await runCommand('pbcopy', [], { timeoutMs: 5000, stdin: args.text });
1180
+ if (res.spawnFailed)
1181
+ return { ok: false, error: 'pbcopy not available' };
1182
+ return { ok: true, platform: 'darwin' };
1183
+ }
1184
+ if (isWayland()) {
1185
+ const res = await runCommand('wl-copy', [], { timeoutMs: 5000, stdin: args.text });
1186
+ if (res.spawnFailed)
1187
+ return { ok: false, error: 'wl-copy not installed' };
1188
+ return { ok: true, platform: 'linux' };
1189
+ }
1190
+ const xclip = await findBinary(['xclip', 'xsel']);
1191
+ if (!xclip)
1192
+ return { ok: false, error: 'xclip / xsel not installed' };
1193
+ const argv = xclip === 'xclip' ? ['-selection', 'clipboard', '-i'] : ['--clipboard', '--input'];
1194
+ const res = await runCommand(xclip, argv, { timeoutMs: 5000, stdin: args.text });
1195
+ if (res.code !== 0)
1196
+ return { ok: false, error: `${xclip} exited ${res.code}` };
1197
+ return { ok: true, platform: 'linux' };
1198
+ }
1199
+ catch (err) {
1200
+ return { ok: false, error: `clipboard_write failed: ${err?.message || err}` };
1201
+ }
1202
+ }
1203
+ export async function osInfo() {
1204
+ return {
1205
+ ok: true,
1206
+ info: {
1207
+ platform: hostPlatform(),
1208
+ wayland: isWayland(),
1209
+ hostname: hostname(),
1210
+ user: userInfo().username,
1211
+ os_release: release(),
1212
+ arch: arch(),
1213
+ cpu_count: cpus().length,
1214
+ memory_total_mb: Math.round(totalmem() / (1024 * 1024)),
1215
+ memory_free_mb: Math.round(freemem() / (1024 * 1024)),
1216
+ env_display: process.env.DISPLAY || null,
1217
+ env_wayland_display: process.env.WAYLAND_DISPLAY || null,
1218
+ node_version: process.version,
1219
+ pid: process.pid,
1220
+ },
1221
+ };
1222
+ }
1223
+ export async function shellExec(args) {
1224
+ if (!args.command)
1225
+ return { ok: false, error: 'command is required' };
1226
+ const timeoutMs = Math.max(1, args.timeout_seconds ?? 30) * 1000;
1227
+ try {
1228
+ const res = await runCommand(args.command, args.args || [], {
1229
+ cwd: args.cwd,
1230
+ stdin: args.stdin,
1231
+ timeoutMs,
1232
+ });
1233
+ if (res.spawnFailed)
1234
+ return { ok: false, error: res.spawnError };
1235
+ return { ok: true, code: res.code, stdout: res.stdout, stderr: res.stderr };
1236
+ }
1237
+ catch (err) {
1238
+ return { ok: false, error: `shell_exec failed: ${err?.message || err}` };
1239
+ }
1240
+ }
1241
+ //# sourceMappingURL=tools.js.map