codeloop-mcp-server 0.1.48 → 0.1.49

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 (41) hide show
  1. package/dist/auth/critical_floors.d.ts +8 -4
  2. package/dist/auth/critical_floors.d.ts.map +1 -1
  3. package/dist/auth/critical_floors.js +13 -17
  4. package/dist/auth/critical_floors.js.map +1 -1
  5. package/dist/auth/init_hint_cache.d.ts +35 -0
  6. package/dist/auth/init_hint_cache.d.ts.map +1 -0
  7. package/dist/auth/init_hint_cache.js +143 -0
  8. package/dist/auth/init_hint_cache.js.map +1 -0
  9. package/dist/evidence/screenshot_diff.d.ts +23 -0
  10. package/dist/evidence/screenshot_diff.d.ts.map +1 -1
  11. package/dist/evidence/screenshot_diff.js +46 -13
  12. package/dist/evidence/screenshot_diff.js.map +1 -1
  13. package/dist/index.js +168 -11
  14. package/dist/index.js.map +1 -1
  15. package/dist/runners/csproj_output_path.d.ts +22 -0
  16. package/dist/runners/csproj_output_path.d.ts.map +1 -0
  17. package/dist/runners/csproj_output_path.js +108 -0
  18. package/dist/runners/csproj_output_path.js.map +1 -0
  19. package/dist/runners/png_dims.d.ts +20 -0
  20. package/dist/runners/png_dims.d.ts.map +1 -0
  21. package/dist/runners/png_dims.js +58 -0
  22. package/dist/runners/png_dims.js.map +1 -0
  23. package/dist/runners/window_manager.d.ts +17 -4
  24. package/dist/runners/window_manager.d.ts.map +1 -1
  25. package/dist/runners/window_manager.js +135 -22
  26. package/dist/runners/window_manager.js.map +1 -1
  27. package/dist/tools/design_compare.d.ts.map +1 -1
  28. package/dist/tools/design_compare.js +14 -0
  29. package/dist/tools/design_compare.js.map +1 -1
  30. package/dist/tools/desktop_app_mode.d.ts +48 -0
  31. package/dist/tools/desktop_app_mode.d.ts.map +1 -0
  32. package/dist/tools/desktop_app_mode.js +86 -0
  33. package/dist/tools/desktop_app_mode.js.map +1 -0
  34. package/dist/tools/self_test.d.ts +40 -0
  35. package/dist/tools/self_test.d.ts.map +1 -0
  36. package/dist/tools/self_test.js +205 -0
  37. package/dist/tools/self_test.js.map +1 -0
  38. package/dist/tools/verify.d.ts.map +1 -1
  39. package/dist/tools/verify.js +4 -5
  40. package/dist/tools/verify.js.map +1 -1
  41. package/package.json +1 -1
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Scans every `.csproj` / `.vbproj` under `projectAbs` (depth-capped
3
+ * to keep wide repos cheap) and returns the union of relative or
4
+ * absolute output paths declared via `<OutputPath>` and
5
+ * `<BaseOutputPath>`. The returned paths are normalised: backslash
6
+ * separators preserved on Windows-style hits, MSBuild variables like
7
+ * `$(Configuration)` collapsed to `Release` (so the helper never
8
+ * yields an unresolvable path), and trailing slashes stripped.
9
+ *
10
+ * Photometry-DB E2E 7+8: shipped with
11
+ * <BaseOutputPath>artifacts\bin\Release\</BaseOutputPath>
12
+ * which the pre-0.1.49 launchDesktopApp ignored — every
13
+ * codeloop_launch_app call returned "no .exe found" and the agent
14
+ * fell back to capturing the IDE.
15
+ *
16
+ * The helper takes its fs primitives as injected functions so the
17
+ * caller (window_manager) keeps its existing dynamic-import shape and
18
+ * we don't have to import `fs` at the top of the module twice.
19
+ */
20
+ export function collectCsprojOutputPaths(projectAbs, pjoin, existsSync, statSync, readdirSync) {
21
+ const out = new Set();
22
+ const csprojFiles = [];
23
+ const visit = (dir, depth) => {
24
+ if (depth > 4 || csprojFiles.length >= 100)
25
+ return;
26
+ let entries;
27
+ try {
28
+ entries = readdirSync(dir);
29
+ }
30
+ catch {
31
+ return;
32
+ }
33
+ for (const e of entries) {
34
+ if (e === "node_modules" || e === "bin" || e === "obj" || e === ".git" ||
35
+ e === "publish" || e === "dist" || e === ".next" || e === ".cache" ||
36
+ e === "artifacts") {
37
+ // Note: we DO scan artifacts in `bin`/`publish`/`dist` mode
38
+ // through standardSubs, but we don't recurse INTO them
39
+ // looking for more .csproj files.
40
+ continue;
41
+ }
42
+ const full = pjoin(dir, e);
43
+ let st;
44
+ try {
45
+ st = statSync(full);
46
+ }
47
+ catch {
48
+ continue;
49
+ }
50
+ if (st.isDirectory())
51
+ visit(full, depth + 1);
52
+ else if (e.endsWith(".csproj") || e.endsWith(".vbproj")) {
53
+ csprojFiles.push(full);
54
+ }
55
+ }
56
+ };
57
+ if (existsSync(projectAbs))
58
+ visit(projectAbs, 0);
59
+ for (const proj of csprojFiles) {
60
+ let content = "";
61
+ try {
62
+ // We avoid pulling readFileSync from fs — the caller already
63
+ // has it. Use a local require if available (we're in CJS via
64
+ // ts-node / esbuild bundle); otherwise read via dynamic import.
65
+ // Simplest: read via fs.readFileSync indirectly by trying to
66
+ // open the file with statSync first and bailing if huge.
67
+ const st = statSync(proj);
68
+ if (!st.isFile() || st.size > 1024 * 1024)
69
+ continue;
70
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
71
+ content = require("fs").readFileSync(proj, "utf-8");
72
+ }
73
+ catch {
74
+ continue;
75
+ }
76
+ const matches = [
77
+ ...content.matchAll(/<OutputPath[^>]*>([^<]+)<\/OutputPath>/gi),
78
+ ...content.matchAll(/<BaseOutputPath[^>]*>([^<]+)<\/BaseOutputPath>/gi),
79
+ ];
80
+ for (const m of matches) {
81
+ let raw = (m[1] || "").trim();
82
+ if (!raw)
83
+ continue;
84
+ // Resolve common MSBuild variables to safe defaults so the
85
+ // resulting path is at least walkable. The actual output is
86
+ // newest-mtime-wins, so picking Release here is fine.
87
+ raw = raw
88
+ .replace(/\$\(Configuration\)/gi, "Release")
89
+ .replace(/\$\(Platform\)/gi, "AnyCPU")
90
+ .replace(/\$\(TargetFramework\)/gi, "")
91
+ .replace(/\$\(MSBuildProjectName\)/gi, "")
92
+ .replace(/[\\/]+$/, "");
93
+ // If <OutputPath> is relative, it lives next to the .csproj
94
+ // (MSBuild semantics). Resolve relative to the project file's
95
+ // directory so we walk the right place.
96
+ if (!/^[A-Za-z]:[\\/]/.test(raw) && !raw.startsWith("/") && !raw.startsWith("\\")) {
97
+ const projDir = proj.substring(0, Math.max(proj.lastIndexOf("\\"), proj.lastIndexOf("/")));
98
+ const resolved = pjoin(projDir, raw);
99
+ out.add(resolved);
100
+ }
101
+ else {
102
+ out.add(raw);
103
+ }
104
+ }
105
+ }
106
+ return Array.from(out);
107
+ }
108
+ //# sourceMappingURL=csproj_output_path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csproj_output_path.js","sourceRoot":"","sources":["../../src/runners/csproj_output_path.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Reads the IHDR width/height of a PNG file without decoding the
3
+ * pixel stream. Used by codeloop_interact's `coords: "screenshot"`
4
+ * mode to scale agent-supplied coordinates from the dimensions of
5
+ * the captured PNG (which the MCP transport may have downscaled
6
+ * before the agent ever saw it) up to the window's actual pixel
7
+ * dimensions.
8
+ *
9
+ * The PNG layout is fixed: 8-byte magic header + 8-byte IHDR chunk
10
+ * length+type + 4-byte BE width at offset 16 + 4-byte BE height at
11
+ * offset 20.
12
+ *
13
+ * Returns `null` on any failure (missing file, truncated, not a
14
+ * PNG); callers fall back to the un-scaled translation path.
15
+ */
16
+ export declare function readPngDims(filePath: string): {
17
+ width: number;
18
+ height: number;
19
+ } | null;
20
+ //# sourceMappingURL=png_dims.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"png_dims.d.ts","sourceRoot":"","sources":["../../src/runners/png_dims.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAkCtF"}
@@ -0,0 +1,58 @@
1
+ import { existsSync, openSync, readSync, closeSync } from "fs";
2
+ /**
3
+ * Reads the IHDR width/height of a PNG file without decoding the
4
+ * pixel stream. Used by codeloop_interact's `coords: "screenshot"`
5
+ * mode to scale agent-supplied coordinates from the dimensions of
6
+ * the captured PNG (which the MCP transport may have downscaled
7
+ * before the agent ever saw it) up to the window's actual pixel
8
+ * dimensions.
9
+ *
10
+ * The PNG layout is fixed: 8-byte magic header + 8-byte IHDR chunk
11
+ * length+type + 4-byte BE width at offset 16 + 4-byte BE height at
12
+ * offset 20.
13
+ *
14
+ * Returns `null` on any failure (missing file, truncated, not a
15
+ * PNG); callers fall back to the un-scaled translation path.
16
+ */
17
+ export function readPngDims(filePath) {
18
+ if (!existsSync(filePath))
19
+ return null;
20
+ let fd;
21
+ try {
22
+ fd = openSync(filePath, "r");
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ try {
28
+ const buf = Buffer.alloc(24);
29
+ const bytesRead = readSync(fd, buf, 0, 24, 0);
30
+ if (bytesRead < 24)
31
+ return null;
32
+ // PNG magic header
33
+ if (buf[0] !== 0x89 || buf[1] !== 0x50 || buf[2] !== 0x4e || buf[3] !== 0x47 ||
34
+ buf[4] !== 0x0d || buf[5] !== 0x0a || buf[6] !== 0x1a || buf[7] !== 0x0a) {
35
+ return null;
36
+ }
37
+ // IHDR chunk type at bytes 12-15 ("IHDR")
38
+ if (buf[12] !== 0x49 || buf[13] !== 0x48 || buf[14] !== 0x44 || buf[15] !== 0x52) {
39
+ return null;
40
+ }
41
+ const width = buf.readUInt32BE(16);
42
+ const height = buf.readUInt32BE(20);
43
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
44
+ return null;
45
+ }
46
+ return { width, height };
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ finally {
52
+ try {
53
+ closeSync(fd);
54
+ }
55
+ catch { /* ignore */ }
56
+ }
57
+ }
58
+ //# sourceMappingURL=png_dims.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"png_dims.js","sourceRoot":"","sources":["../../src/runners/png_dims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,IAAI,EAAU,CAAC;IACf,IAAI,CAAC;QACH,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC7B,MAAM,SAAS,GAAG,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QAC9C,IAAI,SAAS,GAAG,EAAE;YAAE,OAAO,IAAI,CAAC;QAChC,mBAAmB;QACnB,IACE,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;YACxE,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EACxE,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,0CAA0C;QAC1C,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;YACjF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QACnC,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;YACrF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC"}
@@ -32,15 +32,28 @@ export declare function buildWindowsProcessLookup(appName: string): string;
32
32
  */
33
33
  export declare function bringAppToFront(appName: string): Promise<string>;
34
34
  /**
35
- * Get the window bounds (position + size) for cropping video capture.
36
- * Returns { x, y, width, height } in screen points.
35
+ * Window bounds in screen pixels, plus optional DPI scale factors so
36
+ * codeloop_interact can translate logical-pixel coords from a captured
37
+ * screenshot to physical-pixel coords for the OS click APIs.
38
+ *
39
+ * dpi_x / dpi_y are physical-pixel-per-logical ratios (1.0 = 96 DPI
40
+ * baseline; 2.0 = 200% / Retina). Omitted when we couldn't read the
41
+ * value cleanly — translateXY treats absence as 1.0 and clicks
42
+ * unscaled, which preserves legacy behaviour.
37
43
  */
38
- export declare function getWindowBounds(appName: string): Promise<{
44
+ export type WindowBounds = {
39
45
  x: number;
40
46
  y: number;
41
47
  width: number;
42
48
  height: number;
43
- } | null>;
49
+ dpi_x?: number;
50
+ dpi_y?: number;
51
+ };
52
+ /**
53
+ * Get the window bounds (position + size) for cropping video capture.
54
+ * Returns { x, y, width, height, dpi_x?, dpi_y? } in screen points.
55
+ */
56
+ export declare function getWindowBounds(appName: string): Promise<WindowBounds | null>;
44
57
  /**
45
58
  * Click at screen coordinates using CGEvent (macOS), user32.dll (Windows),
46
59
  * or xdotool (Linux). These are low-level HID events that Flutter and all
@@ -1 +1 @@
1
- {"version":3,"file":"window_manager.d.ts","sourceRoot":"","sources":["../../src/runners/window_manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"window_manager.d.ts","sourceRoot":"","sources":["../../src/runners/window_manager.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,7 @@ import { platform, tmpdir } from "os";
2
2
  import { writeFileSync, unlinkSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { runCommand, checkToolAvailable } from "./base.js";
5
+ import { collectCsprojOutputPaths } from "./csproj_output_path.js";
5
6
  /**
6
7
  * Cross-platform window ID lookup.
7
8
  * macOS: CGWindowListCopyWindowInfo via Swift
@@ -194,7 +195,7 @@ async function bringToFrontLinux(appName) {
194
195
  }
195
196
  /**
196
197
  * Get the window bounds (position + size) for cropping video capture.
197
- * Returns { x, y, width, height } in screen points.
198
+ * Returns { x, y, width, height, dpi_x?, dpi_y? } in screen points.
198
199
  */
199
200
  export async function getWindowBounds(appName) {
200
201
  const os = platform();
@@ -208,11 +209,17 @@ export async function getWindowBounds(appName) {
208
209
  }
209
210
  async function getWindowBoundsMacOS(appName) {
210
211
  const tmpFile = join(tmpdir(), `codeloop_winbounds_${Date.now()}.swift`);
212
+ // We also ask NSScreen for backingScaleFactor so codeloop_interact
213
+ // can scale logical-pixel screenshots back to physical pixels for
214
+ // CGEvent (which expects logical pts on macOS — backingScale 1.0
215
+ // is the right answer for clicking, but we still emit it so the
216
+ // value is observable in window_bounds for diagnostics).
211
217
  const swiftCode = [
212
218
  'import Cocoa',
213
219
  'let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]',
214
220
  'guard let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else { exit(1) }',
215
221
  `let search = "${appName}".lowercased()`,
222
+ 'let scale = NSScreen.main?.backingScaleFactor ?? 1.0',
216
223
  'for window in windowList {',
217
224
  ' let owner = window[kCGWindowOwnerName as String] as? String ?? ""',
218
225
  ' if owner.lowercased().contains(search) {',
@@ -222,7 +229,7 @@ async function getWindowBoundsMacOS(appName) {
222
229
  ' let w = bounds["Width"] as? Int ?? 0',
223
230
  ' let h = bounds["Height"] as? Int ?? 0',
224
231
  ' if w > 50 && h > 50 {',
225
- ' print("\\(x),\\(y),\\(w),\\(h)")',
232
+ ' print("\\(x),\\(y),\\(w),\\(h),\\(scale)")',
226
233
  ' exit(0)',
227
234
  ' }',
228
235
  ' }',
@@ -234,8 +241,23 @@ async function getWindowBoundsMacOS(appName) {
234
241
  const result = await runCommand("swift", [tmpFile], process.cwd(), undefined, undefined, SWIFT_TIMEOUT_MS);
235
242
  if (result.exit_code === 0 && result.stdout.trim()) {
236
243
  const parts = result.stdout.trim().split(",").map(Number);
237
- if (parts.length === 4 && parts.every(n => !isNaN(n))) {
238
- return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
244
+ if (parts.length >= 4 && parts.slice(0, 4).every(n => !isNaN(n))) {
245
+ const out = {
246
+ x: parts[0],
247
+ y: parts[1],
248
+ width: parts[2],
249
+ height: parts[3],
250
+ };
251
+ // CGEvent on macOS expects LOGICAL points, not physical
252
+ // pixels. So we report dpi_x / dpi_y as 1.0 even on
253
+ // Retina displays — translateXY uses 1.0 to preserve the
254
+ // legacy unscaled behaviour, which is correct for macOS.
255
+ // We still emit the backingScaleFactor for diagnostics in
256
+ // case future work (e.g. scaling screenshot dims) needs it.
257
+ if (parts.length >= 5 && !isNaN(parts[4]) && parts[4] > 0) {
258
+ out._backing_scale = parts[4];
259
+ }
260
+ return out;
239
261
  }
240
262
  }
241
263
  }
@@ -248,6 +270,14 @@ async function getWindowBoundsMacOS(appName) {
248
270
  return null;
249
271
  }
250
272
  async function getWindowBoundsWindows(appName) {
273
+ // GetDpiForWindow (Win10 1607+) returns the per-monitor DPI of the
274
+ // window's HWND. Divide by 96 to convert to a physical-per-logical
275
+ // ratio (1.0 = 100%, 1.5 = 150% scaling, 2.0 = 200%).
276
+ //
277
+ // user32.dll's mouse_event / SendInput consumes PHYSICAL pixels
278
+ // when the process is per-monitor DPI-aware (which is the .NET 4.6+
279
+ // default), so a screenshot taken at logical pixels needs to be
280
+ // multiplied by this factor before being passed to clickAtPosition.
251
281
  const script = `
252
282
  Add-Type @"
253
283
  using System;
@@ -256,20 +286,43 @@ public class Win32Bounds {
256
286
  [StructLayout(LayoutKind.Sequential)]
257
287
  public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
258
288
  [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
289
+ [DllImport("user32.dll")] public static extern uint GetDpiForWindow(IntPtr hWnd);
259
290
  }
260
291
  "@
261
292
  ${buildWindowsProcessLookup(appName)}
262
293
  if ($proc) {
263
294
  $rect = New-Object Win32Bounds+RECT
264
295
  [Win32Bounds]::GetWindowRect($proc.MainWindowHandle, [ref]$rect) | Out-Null
265
- Write-Output "$($rect.Left),$($rect.Top),$($rect.Right - $rect.Left),$($rect.Bottom - $rect.Top)"
296
+ $dpi = 96
297
+ try {
298
+ $dpi = [Win32Bounds]::GetDpiForWindow($proc.MainWindowHandle)
299
+ if (-not $dpi -or $dpi -le 0) { $dpi = 96 }
300
+ } catch { $dpi = 96 }
301
+ Write-Output "$($rect.Left),$($rect.Top),$($rect.Right - $rect.Left),$($rect.Bottom - $rect.Top),$dpi"
266
302
  }
267
303
  `;
268
304
  const result = await runCommand("powershell", ["-NoProfile", "-Command", script], process.cwd());
269
305
  if (result.exit_code === 0 && result.stdout.trim()) {
270
306
  const parts = result.stdout.trim().split(",").map(Number);
271
- if (parts.length === 4 && parts.every(n => !isNaN(n))) {
272
- return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
307
+ if (parts.length >= 4 && parts.slice(0, 4).every(n => !isNaN(n))) {
308
+ const out = {
309
+ x: parts[0],
310
+ y: parts[1],
311
+ width: parts[2],
312
+ height: parts[3],
313
+ };
314
+ if (parts.length >= 5 && !isNaN(parts[4]) && parts[4] > 0) {
315
+ const ratio = parts[4] / 96;
316
+ // Only surface the DPI when the user is at non-100%
317
+ // scaling. translateXY treats absence as 1.0; we keep the
318
+ // payload tight by not emitting a redundant 1.0 on every
319
+ // call.
320
+ if (Math.abs(ratio - 1) > 0.01) {
321
+ out.dpi_x = ratio;
322
+ out.dpi_y = ratio;
323
+ }
324
+ }
325
+ return out;
273
326
  }
274
327
  }
275
328
  return null;
@@ -297,8 +350,19 @@ async function getWindowBoundsLinux(appName) {
297
350
  else if (key === "HEIGHT")
298
351
  h = parseInt(val, 10);
299
352
  }
300
- if (w > 0 && h > 0)
301
- return { x, y, width: w, height: h };
353
+ if (w > 0 && h > 0) {
354
+ const out = { x, y, width: w, height: h };
355
+ // X11 GDK_SCALE / GDK_DPI_SCALE drives the per-display scaling on
356
+ // most desktop distros. xdotool reports DEVICE pixels, so we only
357
+ // surface a non-1.0 dpi when the env explicitly sets it (which
358
+ // matches what xdotool's click target expects too).
359
+ const gdkScale = parseFloat(process.env.GDK_SCALE || "");
360
+ if (Number.isFinite(gdkScale) && gdkScale > 0 && Math.abs(gdkScale - 1) > 0.01) {
361
+ out.dpi_x = gdkScale;
362
+ out.dpi_y = gdkScale;
363
+ }
364
+ return out;
365
+ }
302
366
  return null;
303
367
  }
304
368
  /**
@@ -541,8 +605,27 @@ export async function launchDesktopApp(appName, projectDir) {
541
605
  ? appName.toLowerCase()
542
606
  : appName.toLowerCase() + ".exe";
543
607
  const candidates = [];
544
- for (const sub of ["publish", "bin", "build", "dist", "out"]) {
545
- const d = pjoin(projectAbs, sub);
608
+ // Standard convention paths.
609
+ const standardSubs = ["publish", "bin", "build", "dist", "out"];
610
+ // Custom <OutputPath> / <BaseOutputPath> from any .csproj in the
611
+ // project. Photometry-DB ships with
612
+ // <BaseOutputPath>artifacts\bin\Release\…</BaseOutputPath>
613
+ // and pre-0.1.49 launchDesktopApp would not find that path
614
+ // because it only walked the convention subs above. Extracting
615
+ // the path here also covers projects with
616
+ // <OutputPath>D:\builds\$(Configuration)\</OutputPath>
617
+ // common on enterprise build configs.
618
+ const csprojOutputPaths = collectCsprojOutputPaths(projectAbs, pjoin, existsSync, statSync, readdirSync);
619
+ const allSubs = Array.from(new Set([
620
+ ...standardSubs,
621
+ ...csprojOutputPaths,
622
+ ]));
623
+ for (const sub of allSubs) {
624
+ // sub may be relative (resolve against projectAbs) or
625
+ // absolute (absolute custom OutputPath).
626
+ const d = sub.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(sub) || sub.startsWith("/")
627
+ ? sub
628
+ : pjoin(projectAbs, sub);
546
629
  if (existsSync(d)) {
547
630
  candidates.push(...findRecursive(d, (f) => f.toLowerCase() === want).filter((p) => isExecCandidate(p, ".exe")));
548
631
  }
@@ -557,20 +640,50 @@ export async function launchDesktopApp(appName, projectDir) {
557
640
  }
558
641
  });
559
642
  const exe = candidates[0];
560
- if (!exe) {
561
- return {
562
- launched: false,
563
- reason: `No .exe found under ${projectAbs}\\{publish|bin|build|dist|out} matching '${want}'. Build the project first (e.g. \`dotnet publish -c Release\`) or set evidence.target_app in .codeloop/config.json to a file that exists.`,
564
- };
643
+ if (exe) {
644
+ try {
645
+ const child = spawn(exe, [], { detached: true, stdio: "ignore", cwd: pjoin(exe, "..") });
646
+ child.unref();
647
+ return { launched: true, command: exe, pid: child.pid };
648
+ }
649
+ catch (e) {
650
+ return { launched: false, command: exe, reason: e.message };
651
+ }
565
652
  }
653
+ // Last-resort fallback for MSIX / Microsoft Store / Start-menu
654
+ // installed apps (e.g. WinUI 3 packaged apps and any .NET MAUI
655
+ // packaged release). `Get-StartApps` returns the AppUserModelID
656
+ // we can launch via `explorer.exe shell:AppsFolder\<AUMID>`.
566
657
  try {
567
- const child = spawn(exe, [], { detached: true, stdio: "ignore", cwd: pjoin(exe, "..") });
568
- child.unref();
569
- return { launched: true, command: exe, pid: child.pid };
570
- }
571
- catch (e) {
572
- return { launched: false, command: exe, reason: e.message };
658
+ const psLiteral = appName.replace(/'/g, "''");
659
+ const regexLiteral = appName
660
+ .replace(/'/g, "''")
661
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
662
+ const lookupScript = `
663
+ $apps = Get-StartApps | Where-Object { $_.Name -eq '${psLiteral}' -or $_.Name -match '${regexLiteral}' }
664
+ $first = $apps | Select-Object -First 1
665
+ if ($first) { Write-Output $first.AppID }
666
+ `;
667
+ const lookup = await runCommand("powershell", ["-NoProfile", "-Command", lookupScript], projectAbs);
668
+ const aumid = lookup.exit_code === 0 ? lookup.stdout.trim() : "";
669
+ if (aumid) {
670
+ const launchCmd = `explorer.exe shell:AppsFolder\\${aumid}`;
671
+ const launch = await runCommand("powershell", ["-NoProfile", "-Command", `Start-Process -FilePath 'explorer.exe' -ArgumentList 'shell:AppsFolder\\${aumid}'`], projectAbs);
672
+ if (launch.exit_code === 0) {
673
+ return { launched: true, command: launchCmd };
674
+ }
675
+ return {
676
+ launched: false,
677
+ command: launchCmd,
678
+ reason: launch.stderr || "Get-StartApps lookup found AUMID but Start-Process failed",
679
+ };
680
+ }
573
681
  }
682
+ catch { /* fall through to error message below */ }
683
+ return {
684
+ launched: false,
685
+ reason: `No .exe found under ${projectAbs}\\{publish|bin|build|dist|out} (plus any <OutputPath> from .csproj) matching '${want}', and \`Get-StartApps\` did not match an MSIX / Store-installed app named '${appName}'. Build the project first (e.g. \`dotnet publish -c Release\`) or set evidence.target_app in .codeloop/config.json to a file that exists.`,
686
+ };
574
687
  }
575
688
  if (os === "darwin") {
576
689
  const wantApp = appName.toLowerCase().endsWith(".app")