devlens-mcp 0.3.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 (175) hide show
  1. package/.claude/settings.json +12 -0
  2. package/.claude/settings.local.json +17 -0
  3. package/INSTALLATION_GUIDE.md +354 -0
  4. package/QUICK_START.md +153 -0
  5. package/README.md +354 -0
  6. package/bin/cli.ts +22 -0
  7. package/bin/register.ts +96 -0
  8. package/dist/bin/cli.d.ts +3 -0
  9. package/dist/bin/cli.d.ts.map +1 -0
  10. package/dist/bin/cli.js +20 -0
  11. package/dist/bin/cli.js.map +1 -0
  12. package/dist/bin/register.d.ts +10 -0
  13. package/dist/bin/register.d.ts.map +1 -0
  14. package/dist/bin/register.js +92 -0
  15. package/dist/bin/register.js.map +1 -0
  16. package/dist/src/config/devlens-config.d.ts +92 -0
  17. package/dist/src/config/devlens-config.d.ts.map +1 -0
  18. package/dist/src/config/devlens-config.js +70 -0
  19. package/dist/src/config/devlens-config.js.map +1 -0
  20. package/dist/src/index.d.ts +35 -0
  21. package/dist/src/index.d.ts.map +1 -0
  22. package/dist/src/index.js +8 -0
  23. package/dist/src/index.js.map +1 -0
  24. package/dist/src/metro/cdp-client.d.ts +48 -0
  25. package/dist/src/metro/cdp-client.d.ts.map +1 -0
  26. package/dist/src/metro/cdp-client.js +127 -0
  27. package/dist/src/metro/cdp-client.js.map +1 -0
  28. package/dist/src/metro/log-collector.d.ts +30 -0
  29. package/dist/src/metro/log-collector.d.ts.map +1 -0
  30. package/dist/src/metro/log-collector.js +114 -0
  31. package/dist/src/metro/log-collector.js.map +1 -0
  32. package/dist/src/metro/metro-bridge.d.ts +56 -0
  33. package/dist/src/metro/metro-bridge.d.ts.map +1 -0
  34. package/dist/src/metro/metro-bridge.js +255 -0
  35. package/dist/src/metro/metro-bridge.js.map +1 -0
  36. package/dist/src/metro/network-inspector.d.ts +34 -0
  37. package/dist/src/metro/network-inspector.d.ts.map +1 -0
  38. package/dist/src/metro/network-inspector.js +100 -0
  39. package/dist/src/metro/network-inspector.js.map +1 -0
  40. package/dist/src/platform/android/adb.d.ts +50 -0
  41. package/dist/src/platform/android/adb.d.ts.map +1 -0
  42. package/dist/src/platform/android/adb.js +137 -0
  43. package/dist/src/platform/android/adb.js.map +1 -0
  44. package/dist/src/platform/android/android-device.d.ts +21 -0
  45. package/dist/src/platform/android/android-device.d.ts.map +1 -0
  46. package/dist/src/platform/android/android-device.js +94 -0
  47. package/dist/src/platform/android/android-device.js.map +1 -0
  48. package/dist/src/platform/android/ui-automator.d.ts +17 -0
  49. package/dist/src/platform/android/ui-automator.d.ts.map +1 -0
  50. package/dist/src/platform/android/ui-automator.js +126 -0
  51. package/dist/src/platform/android/ui-automator.js.map +1 -0
  52. package/dist/src/platform/device-manager.d.ts +28 -0
  53. package/dist/src/platform/device-manager.d.ts.map +1 -0
  54. package/dist/src/platform/device-manager.js +185 -0
  55. package/dist/src/platform/device-manager.js.map +1 -0
  56. package/dist/src/platform/device.d.ts +86 -0
  57. package/dist/src/platform/device.d.ts.map +1 -0
  58. package/dist/src/platform/device.js +7 -0
  59. package/dist/src/platform/device.js.map +1 -0
  60. package/dist/src/platform/ios/accessibility.d.ts +17 -0
  61. package/dist/src/platform/ios/accessibility.d.ts.map +1 -0
  62. package/dist/src/platform/ios/accessibility.js +159 -0
  63. package/dist/src/platform/ios/accessibility.js.map +1 -0
  64. package/dist/src/platform/ios/ios-device.d.ts +22 -0
  65. package/dist/src/platform/ios/ios-device.d.ts.map +1 -0
  66. package/dist/src/platform/ios/ios-device.js +97 -0
  67. package/dist/src/platform/ios/ios-device.js.map +1 -0
  68. package/dist/src/platform/ios/simctl.d.ts +54 -0
  69. package/dist/src/platform/ios/simctl.d.ts.map +1 -0
  70. package/dist/src/platform/ios/simctl.js +192 -0
  71. package/dist/src/platform/ios/simctl.js.map +1 -0
  72. package/dist/src/server.d.ts +3 -0
  73. package/dist/src/server.d.ts.map +1 -0
  74. package/dist/src/server.js +176 -0
  75. package/dist/src/server.js.map +1 -0
  76. package/dist/src/snapshot/formatter.d.ts +18 -0
  77. package/dist/src/snapshot/formatter.d.ts.map +1 -0
  78. package/dist/src/snapshot/formatter.js +86 -0
  79. package/dist/src/snapshot/formatter.js.map +1 -0
  80. package/dist/src/snapshot/ref-registry.d.ts +67 -0
  81. package/dist/src/snapshot/ref-registry.d.ts.map +1 -0
  82. package/dist/src/snapshot/ref-registry.js +169 -0
  83. package/dist/src/snapshot/ref-registry.js.map +1 -0
  84. package/dist/src/snapshot/snapshot-differ.d.ts +57 -0
  85. package/dist/src/snapshot/snapshot-differ.d.ts.map +1 -0
  86. package/dist/src/snapshot/snapshot-differ.js +153 -0
  87. package/dist/src/snapshot/snapshot-differ.js.map +1 -0
  88. package/dist/src/tools/app-tools.d.ts +71 -0
  89. package/dist/src/tools/app-tools.d.ts.map +1 -0
  90. package/dist/src/tools/app-tools.js +97 -0
  91. package/dist/src/tools/app-tools.js.map +1 -0
  92. package/dist/src/tools/device-tools.d.ts +53 -0
  93. package/dist/src/tools/device-tools.d.ts.map +1 -0
  94. package/dist/src/tools/device-tools.js +86 -0
  95. package/dist/src/tools/device-tools.js.map +1 -0
  96. package/dist/src/tools/ds-tools.d.ts +65 -0
  97. package/dist/src/tools/ds-tools.d.ts.map +1 -0
  98. package/dist/src/tools/ds-tools.js +314 -0
  99. package/dist/src/tools/ds-tools.js.map +1 -0
  100. package/dist/src/tools/interaction-tools.d.ts +248 -0
  101. package/dist/src/tools/interaction-tools.d.ts.map +1 -0
  102. package/dist/src/tools/interaction-tools.js +391 -0
  103. package/dist/src/tools/interaction-tools.js.map +1 -0
  104. package/dist/src/tools/metro-tools.d.ts +115 -0
  105. package/dist/src/tools/metro-tools.d.ts.map +1 -0
  106. package/dist/src/tools/metro-tools.js +270 -0
  107. package/dist/src/tools/metro-tools.js.map +1 -0
  108. package/dist/src/tools/navigation-tools.d.ts +36 -0
  109. package/dist/src/tools/navigation-tools.d.ts.map +1 -0
  110. package/dist/src/tools/navigation-tools.js +60 -0
  111. package/dist/src/tools/navigation-tools.js.map +1 -0
  112. package/dist/src/tools/screenshot-tools.d.ts +298 -0
  113. package/dist/src/tools/screenshot-tools.d.ts.map +1 -0
  114. package/dist/src/tools/screenshot-tools.js +565 -0
  115. package/dist/src/tools/screenshot-tools.js.map +1 -0
  116. package/dist/src/tools/snapshot-tools.d.ts +161 -0
  117. package/dist/src/tools/snapshot-tools.d.ts.map +1 -0
  118. package/dist/src/tools/snapshot-tools.js +479 -0
  119. package/dist/src/tools/snapshot-tools.js.map +1 -0
  120. package/dist/src/utils/image-preprocess.d.ts +49 -0
  121. package/dist/src/utils/image-preprocess.d.ts.map +1 -0
  122. package/dist/src/utils/image-preprocess.js +322 -0
  123. package/dist/src/utils/image-preprocess.js.map +1 -0
  124. package/dist/src/utils/retry.d.ts +21 -0
  125. package/dist/src/utils/retry.d.ts.map +1 -0
  126. package/dist/src/utils/retry.js +33 -0
  127. package/dist/src/utils/retry.js.map +1 -0
  128. package/dist/src/visual/comparator.d.ts +51 -0
  129. package/dist/src/visual/comparator.d.ts.map +1 -0
  130. package/dist/src/visual/comparator.js +119 -0
  131. package/dist/src/visual/comparator.js.map +1 -0
  132. package/dist/src/visual/layout-analyzer.d.ts +64 -0
  133. package/dist/src/visual/layout-analyzer.d.ts.map +1 -0
  134. package/dist/src/visual/layout-analyzer.js +198 -0
  135. package/dist/src/visual/layout-analyzer.js.map +1 -0
  136. package/dist/src/visual/screenshot.d.ts +17 -0
  137. package/dist/src/visual/screenshot.d.ts.map +1 -0
  138. package/dist/src/visual/screenshot.js +39 -0
  139. package/dist/src/visual/screenshot.js.map +1 -0
  140. package/docs/figma-workflow.md +289 -0
  141. package/docs/setup-guide.md +360 -0
  142. package/docs/tool-reference.md +622 -0
  143. package/package.json +57 -0
  144. package/src/config/devlens-config.ts +76 -0
  145. package/src/index.ts +5 -0
  146. package/src/metro/cdp-client.ts +160 -0
  147. package/src/metro/log-collector.ts +137 -0
  148. package/src/metro/metro-bridge.ts +307 -0
  149. package/src/metro/network-inspector.ts +134 -0
  150. package/src/platform/android/adb.ts +200 -0
  151. package/src/platform/android/android-device.ts +116 -0
  152. package/src/platform/android/ui-automator.ts +141 -0
  153. package/src/platform/device-manager.ts +229 -0
  154. package/src/platform/device.ts +110 -0
  155. package/src/platform/ios/accessibility.ts +189 -0
  156. package/src/platform/ios/ios-device.ts +116 -0
  157. package/src/platform/ios/simctl.ts +244 -0
  158. package/src/server.ts +228 -0
  159. package/src/snapshot/formatter.ts +102 -0
  160. package/src/snapshot/ref-registry.ts +230 -0
  161. package/src/snapshot/snapshot-differ.ts +220 -0
  162. package/src/tools/app-tools.ts +111 -0
  163. package/src/tools/device-tools.ts +96 -0
  164. package/src/tools/ds-tools.ts +395 -0
  165. package/src/tools/interaction-tools.ts +467 -0
  166. package/src/tools/metro-tools.ts +320 -0
  167. package/src/tools/navigation-tools.ts +71 -0
  168. package/src/tools/screenshot-tools.ts +698 -0
  169. package/src/tools/snapshot-tools.ts +585 -0
  170. package/src/utils/image-preprocess.ts +430 -0
  171. package/src/utils/retry.ts +51 -0
  172. package/src/visual/comparator.ts +191 -0
  173. package/src/visual/layout-analyzer.ts +283 -0
  174. package/src/visual/screenshot.ts +49 -0
  175. package/tsconfig.json +20 -0
@@ -0,0 +1,283 @@
1
+ import pixelmatch from "pixelmatch";
2
+ import type { RefEntry } from "../snapshot/ref-registry.js";
3
+
4
+ /**
5
+ * Layout-aware comparison engine.
6
+ *
7
+ * Uses the accessibility tree (via RefRegistry) to identify semantic UI regions,
8
+ * scales those regions from device coordinate space into Figma image space,
9
+ * and compares each region independently via pixelmatch.
10
+ *
11
+ * This gives per-element similarity scores ("Header: 92%, ChatInput: 44%")
12
+ * instead of a single misleading global pixel-diff percentage.
13
+ */
14
+
15
+ export interface RegionComparisonResult {
16
+ ref: string;
17
+ elementType: string;
18
+ text?: string;
19
+ label?: string;
20
+ /** Element bounds in device pixel space */
21
+ deviceBounds: { left: number; top: number; width: number; height: number };
22
+ /** Corresponding bounds in Figma image space (after scaling) */
23
+ figmaBounds: { left: number; top: number; width: number; height: number };
24
+ similarity: number;
25
+ diffPixels: number;
26
+ totalPixels: number;
27
+ verdict: "excellent" | "good" | "partial" | "poor";
28
+ }
29
+
30
+ export interface LayoutReport {
31
+ regions: RegionComparisonResult[];
32
+ /** Weighted average similarity across all analyzed regions */
33
+ overallSimilarity: number;
34
+ /** Number of refs skipped (too small, out of bounds, crop failed) */
35
+ skippedRefs: number;
36
+ deviceDimensions: { width: number; height: number };
37
+ figmaDimensions: { width: number; height: number };
38
+ }
39
+
40
+ /** Minimum element area (px²) for inclusion in layout analysis */
41
+ const MIN_REGION_AREA = 3000;
42
+
43
+ /** Color difference threshold for pixelmatch (matches global comparator default) */
44
+ const REGION_THRESHOLD = 0.1;
45
+
46
+ function verdictFromSimilarity(s: number): RegionComparisonResult["verdict"] {
47
+ if (s >= 0.92) return "excellent";
48
+ if (s >= 0.80) return "good";
49
+ if (s >= 0.60) return "partial";
50
+ return "poor";
51
+ }
52
+
53
+ /** Scale device-space element bounds into Figma image pixel space */
54
+ function scaleToFigmaSpace(
55
+ bounds: { x: number; y: number; width: number; height: number },
56
+ deviceW: number,
57
+ deviceH: number,
58
+ figmaW: number,
59
+ figmaH: number
60
+ ): { left: number; top: number; width: number; height: number } {
61
+ const scaleX = figmaW / deviceW;
62
+ const scaleY = figmaH / deviceH;
63
+ return {
64
+ left: Math.round(bounds.x * scaleX),
65
+ top: Math.round(bounds.y * scaleY),
66
+ width: Math.max(1, Math.round(bounds.width * scaleX)),
67
+ height: Math.max(1, Math.round(bounds.height * scaleY)),
68
+ };
69
+ }
70
+
71
+ /** Clamp bounds to image dimensions. Returns null if there's no overlap. */
72
+ function clampBounds(
73
+ b: { left: number; top: number; width: number; height: number },
74
+ maxW: number,
75
+ maxH: number
76
+ ): { left: number; top: number; width: number; height: number } | null {
77
+ const left = Math.max(0, b.left);
78
+ const top = Math.max(0, b.top);
79
+ const right = Math.min(maxW, b.left + b.width);
80
+ const bottom = Math.min(maxH, b.top + b.height);
81
+ if (right <= left || bottom <= top) return null;
82
+ return { left, top, width: right - left, height: bottom - top };
83
+ }
84
+
85
+ /**
86
+ * Crop a PNG buffer to a region and return raw RGBA bytes + dimensions.
87
+ * Uses sharp's raw() output so pixelmatch can consume it directly
88
+ * without a PNG encode/decode round-trip.
89
+ */
90
+ async function cropToRaw(
91
+ imageBuffer: Buffer,
92
+ bounds: { left: number; top: number; width: number; height: number }
93
+ ): Promise<{ data: Buffer; width: number; height: number } | null> {
94
+ try {
95
+ const sharp = (await import("sharp")).default;
96
+ const result = await sharp(imageBuffer)
97
+ .extract({ left: bounds.left, top: bounds.top, width: bounds.width, height: bounds.height })
98
+ .ensureAlpha()
99
+ .raw()
100
+ .toBuffer({ resolveWithObject: true });
101
+ return { data: result.data, width: result.info.width, height: result.info.height };
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Resize raw RGBA bytes to target dimensions.
109
+ * Used to match device crop and Figma crop pixel dimensions before pixelmatch.
110
+ */
111
+ async function resizeRaw(
112
+ raw: { data: Buffer; width: number; height: number },
113
+ targetW: number,
114
+ targetH: number
115
+ ): Promise<{ data: Buffer; width: number; height: number } | null> {
116
+ if (raw.width === targetW && raw.height === targetH) return raw;
117
+ try {
118
+ const sharp = (await import("sharp")).default;
119
+ const result = await sharp(raw.data, {
120
+ raw: { width: raw.width, height: raw.height, channels: 4 },
121
+ })
122
+ .resize(targetW, targetH, { fit: "fill" })
123
+ .raw()
124
+ .toBuffer({ resolveWithObject: true });
125
+ return { data: result.data, width: result.info.width, height: result.info.height };
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Core function: for each ref with sufficient area, scale its device bounds
133
+ * into Figma image space, crop both images, and compare independently.
134
+ *
135
+ * @param deviceBuffer Full-screen device screenshot PNG
136
+ * @param figmaBuffer Preprocessed Figma image PNG (artifacts already removed)
137
+ * @param refs All RefEntry objects from RefRegistry.getAllRefs()
138
+ * @param deviceW Device screen width in pixels
139
+ * @param deviceH Device screen height in pixels
140
+ */
141
+ export async function buildLayoutReport(
142
+ deviceBuffer: Buffer,
143
+ figmaBuffer: Buffer,
144
+ refs: RefEntry[],
145
+ deviceW: number,
146
+ deviceH: number
147
+ ): Promise<LayoutReport> {
148
+ const sharp = (await import("sharp")).default;
149
+
150
+ const figmaMeta = await sharp(figmaBuffer).metadata();
151
+ const figmaW = figmaMeta.width!;
152
+ const figmaH = figmaMeta.height!;
153
+
154
+ const regions: RegionComparisonResult[] = [];
155
+ let skippedRefs = 0;
156
+
157
+ for (const entry of refs) {
158
+ const { bounds } = entry;
159
+ const area = bounds.width * bounds.height;
160
+
161
+ if (area < MIN_REGION_AREA) {
162
+ skippedRefs++;
163
+ continue;
164
+ }
165
+
166
+ const deviceBounds = { left: bounds.x, top: bounds.y, width: bounds.width, height: bounds.height };
167
+ const figmaBounds = scaleToFigmaSpace(bounds, deviceW, deviceH, figmaW, figmaH);
168
+
169
+ const clampedDevice = clampBounds(deviceBounds, deviceW, deviceH);
170
+ const clampedFigma = clampBounds(figmaBounds, figmaW, figmaH);
171
+
172
+ if (!clampedDevice || !clampedFigma) {
173
+ skippedRefs++;
174
+ continue;
175
+ }
176
+
177
+ // Crop both in parallel
178
+ const [deviceCrop, figmaCrop] = await Promise.all([
179
+ cropToRaw(deviceBuffer, clampedDevice),
180
+ cropToRaw(figmaBuffer, clampedFigma),
181
+ ]);
182
+
183
+ if (!deviceCrop || !figmaCrop) {
184
+ skippedRefs++;
185
+ continue;
186
+ }
187
+
188
+ // Resize Figma crop to exactly match device crop dimensions
189
+ const figmaResized = await resizeRaw(figmaCrop, deviceCrop.width, deviceCrop.height);
190
+ if (!figmaResized) {
191
+ skippedRefs++;
192
+ continue;
193
+ }
194
+
195
+ const totalPixels = deviceCrop.width * deviceCrop.height;
196
+ const diff = Buffer.alloc(totalPixels * 4);
197
+
198
+ const diffPixels = pixelmatch(
199
+ deviceCrop.data,
200
+ figmaResized.data,
201
+ diff,
202
+ deviceCrop.width,
203
+ deviceCrop.height,
204
+ { threshold: REGION_THRESHOLD, includeAA: false }
205
+ );
206
+
207
+ const similarity = 1 - diffPixels / totalPixels;
208
+
209
+ regions.push({
210
+ ref: entry.ref,
211
+ elementType: entry.node.type,
212
+ text: entry.node.text,
213
+ label: entry.node.label,
214
+ deviceBounds,
215
+ figmaBounds,
216
+ similarity,
217
+ diffPixels,
218
+ totalPixels,
219
+ verdict: verdictFromSimilarity(similarity),
220
+ });
221
+ }
222
+
223
+ // Weighted average by region area
224
+ const totalWeight = regions.reduce((s, r) => s + r.totalPixels, 0);
225
+ const overallSimilarity = totalWeight > 0
226
+ ? regions.reduce((s, r) => s + r.similarity * r.totalPixels, 0) / totalWeight
227
+ : 0;
228
+
229
+ return {
230
+ regions,
231
+ overallSimilarity,
232
+ skippedRefs,
233
+ deviceDimensions: { width: deviceW, height: deviceH },
234
+ figmaDimensions: { width: figmaW, height: figmaH },
235
+ };
236
+ }
237
+
238
+ /** Format layout report as human-readable text for MCP tool output */
239
+ export function formatLayoutReport(report: LayoutReport): string {
240
+ const scaleX = (report.deviceDimensions.width / report.figmaDimensions.width).toFixed(2);
241
+ const scaleY = (report.deviceDimensions.height / report.figmaDimensions.height).toFixed(2);
242
+
243
+ const lines: string[] = [
244
+ "--- Layout Region Analysis ---",
245
+ `Device: ${report.deviceDimensions.width}x${report.deviceDimensions.height}px`,
246
+ `Figma: ${report.figmaDimensions.width}x${report.figmaDimensions.height}px`,
247
+ `Scale: ${scaleX}x (horizontal) ${scaleY}x (vertical)`,
248
+ `Regions: ${report.regions.length} analyzed, ${report.skippedRefs} too small to compare`,
249
+ "",
250
+ ];
251
+
252
+ if (report.regions.length === 0) {
253
+ lines.push("No regions to analyze. Run devlens_snapshot first to populate element refs.");
254
+ return lines.join("\n");
255
+ }
256
+
257
+ // Sort ascending by similarity — problem areas at the bottom
258
+ const sorted = [...report.regions].sort((a, b) => a.similarity - b.similarity);
259
+
260
+ for (const r of sorted) {
261
+ const icon = r.verdict === "excellent" ? "✓"
262
+ : r.verdict === "good" ? "~"
263
+ : r.verdict === "partial" ? "!"
264
+ : "X";
265
+ const pct = (r.similarity * 100).toFixed(1);
266
+ const label = r.text
267
+ ? `"${r.text.slice(0, 35)}"`
268
+ : r.label
269
+ ? `label="${r.label.slice(0, 35)}"`
270
+ : r.elementType;
271
+
272
+ const suffix = (r.verdict === "poor" || r.verdict === "partial")
273
+ ? ` <- investigate (${r.diffPixels.toLocaleString()} diff pixels)`
274
+ : "";
275
+
276
+ lines.push(` [${icon}] [${r.ref}] ${label}: ${pct}%${suffix}`);
277
+ }
278
+
279
+ lines.push("");
280
+ lines.push(`Overall (area-weighted): ${(report.overallSimilarity * 100).toFixed(1)}%`);
281
+
282
+ return lines.join("\n");
283
+ }
@@ -0,0 +1,49 @@
1
+ import { writeFile } from "fs/promises";
2
+ import type { Device } from "../platform/device.js";
3
+ import type { Bounds } from "../platform/device.js";
4
+
5
+ /**
6
+ * Screenshot utilities — capture, encode, crop, and save screenshots.
7
+ */
8
+
9
+ /** Take a screenshot and return it as base64-encoded PNG */
10
+ export async function captureScreenshot(device: Device): Promise<{
11
+ buffer: Buffer;
12
+ base64: string;
13
+ }> {
14
+ const buffer = await device.takeScreenshot();
15
+ const base64 = buffer.toString("base64");
16
+ return { buffer, base64 };
17
+ }
18
+
19
+ /** Save a screenshot buffer to disk */
20
+ export async function saveScreenshot(
21
+ buffer: Buffer,
22
+ filePath: string
23
+ ): Promise<void> {
24
+ await writeFile(filePath, buffer);
25
+ }
26
+
27
+ /** Crop a screenshot to a specific element's bounds */
28
+ export async function cropToElement(
29
+ screenshotBuffer: Buffer,
30
+ bounds: Bounds
31
+ ): Promise<Buffer> {
32
+ const sharp = (await import("sharp")).default;
33
+
34
+ return sharp(screenshotBuffer)
35
+ .extract({
36
+ left: Math.max(0, Math.round(bounds.x)),
37
+ top: Math.max(0, Math.round(bounds.y)),
38
+ width: Math.max(1, Math.round(bounds.width)),
39
+ height: Math.max(1, Math.round(bounds.height)),
40
+ })
41
+ .png()
42
+ .toBuffer();
43
+ }
44
+
45
+ /** Generate a default screenshot filename with timestamp */
46
+ export function defaultScreenshotPath(): string {
47
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
48
+ return `/tmp/devlens-screenshot-${timestamp}.png`;
49
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "outDir": "dist",
8
+ "rootDir": ".",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true
17
+ },
18
+ "include": ["src/**/*", "bin/**/*"],
19
+ "exclude": ["node_modules", "dist", "tests"]
20
+ }