@veolab/discoverylab 1.2.2 → 1.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 (172) hide show
  1. package/.mcp.json +2 -2
  2. package/README.md +182 -0
  3. package/dist/{chunk-YEZ26ENO.js → chunk-3QRQEDWR.js} +313 -192
  4. package/dist/{chunk-E3N3P2AG.js → chunk-4L76GPRC.js} +1124 -51
  5. package/dist/{chunk-I6YD3QFM.js → chunk-FIL7IWEL.js} +5 -3
  6. package/dist/{chunk-G524UVBK.js → chunk-FNUN7EPB.js} +5 -5
  7. package/dist/chunk-GAKEFJ5T.js +481 -0
  8. package/dist/chunk-LB3RNE3O.js +109 -0
  9. package/dist/chunk-N6JJ2RGV.js +2680 -0
  10. package/dist/{chunk-7HZEDTS7.js → chunk-VRM42PML.js} +3471 -848
  11. package/dist/{chunk-TJ3H23LL.js → chunk-VVIOB362.js} +3 -1
  12. package/dist/{chunk-W3WJGYR6.js → chunk-XFVDP332.js} +8 -2
  13. package/dist/cli.js +397 -9
  14. package/dist/{db-ADBEBNH6.js → db-6WLEVKUV.js} +3 -1
  15. package/dist/esvp-GSISVXLC.js +52 -0
  16. package/dist/esvp-mobile-GC7MAGMI.js +20 -0
  17. package/dist/index.d.ts +123 -1
  18. package/dist/index.html +11689 -8690
  19. package/dist/index.js +67 -12
  20. package/dist/{ocr-UTWC7537.js → ocr-QDYNCSPE.js} +1 -1
  21. package/dist/{playwright-R7Y5HREH.js → playwright-VZ7PXDC5.js} +2 -2
  22. package/dist/runtime/esvp-host-runtime/darwin-arm64/esvp-host-runtime +0 -0
  23. package/dist/runtime/esvp-host-runtime/manifest.json +10 -0
  24. package/dist/server-FO3UVUZU.js +22 -0
  25. package/dist/{setup-B5YPNUE4.js → setup-2SQC5UHJ.js} +2 -2
  26. package/dist/{tools-YS4QHOTQ.js → tools-OCRMOQ4U.js} +61 -6
  27. package/package.json +36 -5
  28. package/dist/chunk-22OCFYHG.js +0 -6283
  29. package/dist/chunk-24VARQVO.js +0 -7818
  30. package/dist/chunk-2OGZX6C4.js +0 -588
  31. package/dist/chunk-2WCNIFRO.js +0 -6191
  32. package/dist/chunk-43U6UYV7.js +0 -590
  33. package/dist/chunk-4H2E3K2G.js +0 -7638
  34. package/dist/chunk-4KLG6DDE.js +0 -334
  35. package/dist/chunk-4MS6YW2B.js +0 -6490
  36. package/dist/chunk-4NNTRJOI.js +0 -7791
  37. package/dist/chunk-5F76VWME.js +0 -6397
  38. package/dist/chunk-5NEFN42O.js +0 -7791
  39. package/dist/chunk-63MEQ6UH.js +0 -7673
  40. package/dist/chunk-6H3NXFX3.js +0 -6861
  41. package/dist/chunk-7DOG2W4O.js +0 -6428
  42. package/dist/chunk-7IDQLLBW.js +0 -311
  43. package/dist/chunk-7NP64TGJ.js +0 -6822
  44. package/dist/chunk-AATLY4KT.js +0 -6505
  45. package/dist/chunk-C7QUR7XX.js +0 -6397
  46. package/dist/chunk-CGKCE6MC.js +0 -6279
  47. package/dist/chunk-D25V6IWE.js +0 -6487
  48. package/dist/chunk-EQOZSXAT.js +0 -6822
  49. package/dist/chunk-FPHD7HSQ.js +0 -6812
  50. package/dist/chunk-GGJJUCFK.js +0 -7160
  51. package/dist/chunk-GLHOY3NN.js +0 -7805
  52. package/dist/chunk-GML5MKQA.js +0 -6398
  53. package/dist/chunk-GOL6FUJL.js +0 -6045
  54. package/dist/chunk-GSWHWEYC.js +0 -1346
  55. package/dist/chunk-HDKEQOF5.js +0 -7788
  56. package/dist/chunk-HZGSWVVS.js +0 -7111
  57. package/dist/chunk-IGZ5TICZ.js +0 -334
  58. package/dist/chunk-IRKQG33A.js +0 -7054
  59. package/dist/chunk-JFTBF4JR.js +0 -6040
  60. package/dist/chunk-JVLVBPUJ.js +0 -6180
  61. package/dist/chunk-JY3KC67R.js +0 -6504
  62. package/dist/chunk-KUFBCBNJ.js +0 -6815
  63. package/dist/chunk-KV7KDJ43.js +0 -7639
  64. package/dist/chunk-L4SA5F5W.js +0 -6397
  65. package/dist/chunk-L5IJZV5F.js +0 -6822
  66. package/dist/chunk-MFFPQLU4.js +0 -7102
  67. package/dist/chunk-MJS2YKNR.js +0 -6397
  68. package/dist/chunk-MN6LCZHZ.js +0 -1320
  69. package/dist/chunk-NBAUZ7X2.js +0 -1336
  70. package/dist/chunk-NDBW6ELQ.js +0 -7638
  71. package/dist/chunk-O2HBSDI2.js +0 -6175
  72. package/dist/chunk-OFFIUYMG.js +0 -6341
  73. package/dist/chunk-OVCQGF2J.js +0 -1321
  74. package/dist/chunk-P4S7ZY6G.js +0 -7638
  75. package/dist/chunk-PBHUHSC3.js +0 -6002
  76. package/dist/chunk-PC4LR4ZI.js +0 -6359
  77. package/dist/chunk-PMTGGZ7R.js +0 -6397
  78. package/dist/chunk-PTXSB3UV.js +0 -497
  79. package/dist/chunk-PYUCY3U6.js +0 -1340
  80. package/dist/chunk-QJXXHOV7.js +0 -205
  81. package/dist/chunk-RDZDSOAL.js +0 -7750
  82. package/dist/chunk-RLW2OI2L.js +0 -6383
  83. package/dist/chunk-RUGHHO4K.js +0 -6395
  84. package/dist/chunk-SIOQVM2E.js +0 -6819
  85. package/dist/chunk-SR67SRIT.js +0 -1336
  86. package/dist/chunk-SSRXIO2V.js +0 -6822
  87. package/dist/chunk-SWSEKFON.js +0 -6487
  88. package/dist/chunk-TAODYZ52.js +0 -1393
  89. package/dist/chunk-TBG76CYG.js +0 -6395
  90. package/dist/chunk-V3CBINLD.js +0 -6812
  91. package/dist/chunk-VPYSLEGM.js +0 -6710
  92. package/dist/chunk-VY3BLXBW.js +0 -329
  93. package/dist/chunk-WTFOGVJQ.js +0 -6365
  94. package/dist/chunk-X64SFUT5.js +0 -6099
  95. package/dist/chunk-XIBF5LBD.js +0 -6395
  96. package/dist/chunk-XUKWS2CE.js +0 -7805
  97. package/dist/chunk-XZZKFF5V.js +0 -7787
  98. package/dist/chunk-Y5VDMSYC.js +0 -6701
  99. package/dist/chunk-YUBL36H4.js +0 -6605
  100. package/dist/chunk-YWVXFVSW.js +0 -6456
  101. package/dist/chunk-ZJFWMSZF.js +0 -7883
  102. package/dist/chunk-ZXZACOLD.js +0 -6822
  103. package/dist/db-IWIL65EX.js +0 -33
  104. package/dist/gridCompositor-ENKLFPWR.js +0 -409
  105. package/dist/playwright-A3OGSDRG.js +0 -38
  106. package/dist/playwright-ATDC4NYW.js +0 -38
  107. package/dist/playwright-E6EUFIJG.js +0 -38
  108. package/dist/server-2DXLKLFM.js +0 -13
  109. package/dist/server-2ICEWJVK.js +0 -13
  110. package/dist/server-2MQV3FNY.js +0 -13
  111. package/dist/server-2NGD7GE3.js +0 -13
  112. package/dist/server-2VKO76UK.js +0 -14
  113. package/dist/server-3BK2VFU7.js +0 -13
  114. package/dist/server-3FBHBA7L.js +0 -15
  115. package/dist/server-4LDOB3NX.js +0 -13
  116. package/dist/server-4YI44KDR.js +0 -13
  117. package/dist/server-64XMXA5P.js +0 -13
  118. package/dist/server-6IPHVUYT.js +0 -14
  119. package/dist/server-73ORHMJN.js +0 -13
  120. package/dist/server-73P7M3QB.js +0 -14
  121. package/dist/server-BPVRW5LJ.js +0 -14
  122. package/dist/server-BW4RKZIX.js +0 -13
  123. package/dist/server-CFS5SM5K.js +0 -13
  124. package/dist/server-DX7VYHHM.js +0 -13
  125. package/dist/server-F3YPX6ET.js +0 -13
  126. package/dist/server-FUXTR33I.js +0 -13
  127. package/dist/server-G2SY3DOS.js +0 -13
  128. package/dist/server-G32U7VOQ.js +0 -13
  129. package/dist/server-HKRIY7FP.js +0 -14
  130. package/dist/server-HON66OES.js +0 -15
  131. package/dist/server-IOOZK4NP.js +0 -14
  132. package/dist/server-IZEO7OJJ.js +0 -14
  133. package/dist/server-J52LMTBT.js +0 -13
  134. package/dist/server-JG7UKFGK.js +0 -14
  135. package/dist/server-JSCHEBOD.js +0 -13
  136. package/dist/server-K6KC4ZOM.js +0 -13
  137. package/dist/server-KJVRGWFE.js +0 -13
  138. package/dist/server-LCPB2L4U.js +0 -13
  139. package/dist/server-M7LDYKAJ.js +0 -13
  140. package/dist/server-MKVK6ZQQ.js +0 -13
  141. package/dist/server-MU52LCXT.js +0 -13
  142. package/dist/server-NM5CKDUU.js +0 -13
  143. package/dist/server-NPZN3FWO.js +0 -14
  144. package/dist/server-O5FIAHSY.js +0 -14
  145. package/dist/server-OESJUEYC.js +0 -13
  146. package/dist/server-ONSKQO4W.js +0 -13
  147. package/dist/server-P27BZXBL.js +0 -14
  148. package/dist/server-Q4FBWQUA.js +0 -13
  149. package/dist/server-RNQ7VUAL.js +0 -13
  150. package/dist/server-S6B5WUBT.js +0 -14
  151. package/dist/server-SRYNSGSP.js +0 -14
  152. package/dist/server-SUN3W2YK.js +0 -13
  153. package/dist/server-UA62LHZB.js +0 -13
  154. package/dist/server-UJB44EW5.js +0 -13
  155. package/dist/server-X3TLP6DX.js +0 -14
  156. package/dist/server-YT2UGEZK.js +0 -13
  157. package/dist/server-ZBPQ33V6.js +0 -14
  158. package/dist/setup-27CQAX6K.js +0 -17
  159. package/dist/setup-AQX4JQVR.js +0 -17
  160. package/dist/setup-EQTU7FI6.js +0 -17
  161. package/dist/tools-2KPB37GK.js +0 -178
  162. package/dist/tools-3H6IOWXV.js +0 -178
  163. package/dist/tools-3KYHPDCJ.js +0 -178
  164. package/dist/tools-75BAPCUM.js +0 -177
  165. package/dist/tools-BUVCUCRL.js +0 -178
  166. package/dist/tools-HDNODRS6.js +0 -178
  167. package/dist/tools-HP5MNY3D.js +0 -177
  168. package/dist/tools-L6PKKQPY.js +0 -179
  169. package/dist/tools-N5N2IO7V.js +0 -178
  170. package/dist/tools-NFJEZ2FF.js +0 -177
  171. package/dist/tools-OPULIER6.js +0 -178
  172. package/dist/tools-TLCKABUW.js +0 -178
@@ -0,0 +1,2680 @@
1
+ import {
2
+ buildAppLabNetworkProfile,
3
+ inferAppLabExternalProxyHost
4
+ } from "./chunk-LB3RNE3O.js";
5
+ import {
6
+ attachESVPNetworkTrace,
7
+ clearESVPNetwork,
8
+ configureESVPNetwork,
9
+ createESVPSession,
10
+ finishESVPSession,
11
+ getESVPArtifactContent,
12
+ getESVPConnection,
13
+ getESVPReplayConsistency,
14
+ getESVPSessionNetwork,
15
+ inspectESVPSession,
16
+ replayESVPSession,
17
+ runESVPActions
18
+ } from "./chunk-GAKEFJ5T.js";
19
+ import {
20
+ redactSensitiveTestInput
21
+ } from "./chunk-SLNJEF32.js";
22
+ import {
23
+ recognizeText
24
+ } from "./chunk-XFVDP332.js";
25
+
26
+ // src/core/analyze/aiActionDetector.ts
27
+ import Anthropic from "@anthropic-ai/sdk";
28
+ import { readFileSync, readdirSync } from "fs";
29
+ import { join } from "path";
30
+ function buildAnalysisPrompt(screenshotCount) {
31
+ return `You are analyzing a sequence of ${screenshotCount} mobile app screenshots taken during a user testing session. Your task is to detect what actions the user performed between each screenshot.
32
+
33
+ Analyze the visual changes between consecutive screenshots and identify:
34
+ 1. Taps on buttons, links, or UI elements
35
+ 2. Text input in fields
36
+ 3. Scroll/swipe gestures
37
+ 4. Navigation actions (back, etc.)
38
+ 5. App launches or screen transitions
39
+
40
+ For each detected action, determine:
41
+ - The type of action (tap, type, scroll, swipe, back, launch, wait)
42
+ - A short human description of what happened
43
+ - For tap actions, set "element" to the shortest exact visible label on screen when possible. Examples: "Looks", "View Plans", "Continue". Do not use descriptive phrases like "Looks tab in bottom navigation" unless that full phrase is literally visible.
44
+ - The approximate screen coordinates if it's a tap (as percentage of screen, e.g., x: 50, y: 30 means center-top)
45
+ - Any text that was typed
46
+ - Scroll/swipe direction
47
+
48
+ Respond in JSON format:
49
+ {
50
+ "appName": "detected app name or null",
51
+ "actions": [
52
+ {
53
+ "type": "tap|type|scroll|swipe|back|launch|wait",
54
+ "description": "what the action does",
55
+ "element": "button text or element description",
56
+ "text": "typed text if type action",
57
+ "coordinates": {"x": 50, "y": 75},
58
+ "direction": "up|down|left|right for scroll/swipe",
59
+ "confidence": 0.0-1.0
60
+ }
61
+ ],
62
+ "summary": "brief summary of the user flow"
63
+ }
64
+
65
+ Be conservative - only include actions you're confident about (confidence > 0.7).
66
+ Focus on meaningful interactions, not every tiny change.
67
+ Return ONLY valid JSON (no Markdown, no code fences, no extra commentary).`;
68
+ }
69
+ function buildVisibleActionSelectionPrompt(candidates) {
70
+ return `You are looking at a single mobile app screenshot.
71
+
72
+ Choose which of the candidate UI actions is currently visible and tappable on screen right now.
73
+
74
+ Rules:
75
+ - Prefer exact visible button/link/tab labels.
76
+ - If none of the candidates are clearly visible, return null.
77
+ - Do not guess based on what might happen next.
78
+
79
+ Respond in JSON only:
80
+ {
81
+ "selectedLabel": "one of the candidate labels exactly as provided, or null",
82
+ "confidence": 0.0-1.0,
83
+ "reason": "brief explanation"
84
+ }
85
+
86
+ Candidates:
87
+ ${candidates.map((candidate, index) => `${index + 1}. ${candidate}`).join("\n")}`;
88
+ }
89
+ function normalizeWhitespace(value) {
90
+ return String(value || "").replace(/\s+/g, " ").trim();
91
+ }
92
+ function extractQuotedLabel(value) {
93
+ const text = normalizeWhitespace(value);
94
+ if (!text) return "";
95
+ const match = text.match(/["“”']([^"“”']{2,80})["“”']/);
96
+ return normalizeWhitespace(match?.[1]);
97
+ }
98
+ function normalizeTapElementLabel(value) {
99
+ const raw = normalizeWhitespace(value);
100
+ if (!raw) return "";
101
+ const quoted = extractQuotedLabel(raw);
102
+ if (quoted) return quoted;
103
+ const variants = [];
104
+ const seen = /* @__PURE__ */ new Set();
105
+ const push = (candidate) => {
106
+ const normalized = normalizeWhitespace(candidate);
107
+ if (!normalized || seen.has(normalized)) return;
108
+ seen.add(normalized);
109
+ variants.push(normalized);
110
+ };
111
+ push(raw);
112
+ push(raw.replace(/\s+(?:in|inside|within|from|on)\s+.+$/i, ""));
113
+ push(raw.replace(/\s+(?:button|tab|icon|link|banner|card|item|field|input|modal|sheet|screen|section|row)$/i, ""));
114
+ push(
115
+ raw.split(/\s+/).filter((part) => !["my", "the", "a", "an", "to", "for", "of"].includes(part.toLowerCase())).join(" ")
116
+ );
117
+ return variants[variants.length - 1] || variants[0] || raw;
118
+ }
119
+ function normalizeDetectedAction(action) {
120
+ const normalized = {
121
+ ...action,
122
+ description: normalizeWhitespace(action.description),
123
+ element: normalizeWhitespace(action.element),
124
+ text: normalizeWhitespace(action.text)
125
+ };
126
+ if (normalized.type === "tap") {
127
+ const literalElement = normalizeTapElementLabel(normalized.element || normalized.description);
128
+ if (literalElement) {
129
+ normalized.element = literalElement;
130
+ }
131
+ }
132
+ return normalized;
133
+ }
134
+ function normalizeDetectedActions(actions) {
135
+ return (actions || []).map(normalizeDetectedAction);
136
+ }
137
+ function sanitizePreview(text, maxLength = 400) {
138
+ const normalized = String(text || "").replace(/\s+/g, " ").trim();
139
+ if (!normalized) return "(empty response)";
140
+ return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
141
+ }
142
+ function extractCodeFenceCandidates(text) {
143
+ const candidates = [];
144
+ const fenceRegex = /```(?:json)?\s*([\s\S]*?)```/gi;
145
+ let match = null;
146
+ while ((match = fenceRegex.exec(text)) !== null) {
147
+ if (match[1]?.trim()) candidates.push(match[1].trim());
148
+ }
149
+ return candidates;
150
+ }
151
+ function extractBraceObjectCandidates(text) {
152
+ const candidates = [];
153
+ let depth = 0;
154
+ let start = -1;
155
+ let inString = false;
156
+ let escaped = false;
157
+ for (let i = 0; i < text.length; i += 1) {
158
+ const ch = text[i];
159
+ if (escaped) {
160
+ escaped = false;
161
+ continue;
162
+ }
163
+ if (ch === "\\" && inString) {
164
+ escaped = true;
165
+ continue;
166
+ }
167
+ if (ch === '"') {
168
+ inString = !inString;
169
+ continue;
170
+ }
171
+ if (inString) continue;
172
+ if (ch === "{") {
173
+ if (depth === 0) start = i;
174
+ depth += 1;
175
+ continue;
176
+ }
177
+ if (ch === "}") {
178
+ if (depth > 0) depth -= 1;
179
+ if (depth === 0 && start >= 0) {
180
+ const candidate = text.slice(start, i + 1).trim();
181
+ if (candidate) candidates.push(candidate);
182
+ start = -1;
183
+ }
184
+ }
185
+ }
186
+ return candidates;
187
+ }
188
+ function coerceAnalysisResult(parsed) {
189
+ if (!parsed || typeof parsed !== "object") {
190
+ throw new Error("Parsed response is not an object");
191
+ }
192
+ const obj = parsed;
193
+ return {
194
+ actions: Array.isArray(obj.actions) ? obj.actions : [],
195
+ appName: typeof obj.appName === "string" ? obj.appName : void 0,
196
+ summary: typeof obj.summary === "string" ? obj.summary : "",
197
+ skipped: typeof obj.skipped === "boolean" ? obj.skipped : void 0
198
+ };
199
+ }
200
+ function parseAnalysisResponse(text) {
201
+ const raw = String(text || "").trim();
202
+ if (!raw) {
203
+ throw new Error("Could not parse JSON from response (empty response)");
204
+ }
205
+ const candidates = [raw, ...extractCodeFenceCandidates(raw), ...extractBraceObjectCandidates(raw)];
206
+ const seen = /* @__PURE__ */ new Set();
207
+ let lastError = null;
208
+ for (const candidate of candidates) {
209
+ const normalized = candidate.trim();
210
+ if (!normalized || seen.has(normalized)) continue;
211
+ seen.add(normalized);
212
+ try {
213
+ return coerceAnalysisResult(JSON.parse(normalized));
214
+ } catch (error) {
215
+ lastError = error;
216
+ }
217
+ }
218
+ const detail = lastError instanceof Error ? lastError.message : "unknown parse error";
219
+ throw new Error(`Could not parse JSON from response (${detail}). Preview: ${sanitizePreview(raw)}`);
220
+ }
221
+ function parseVisibleActionSelectionResponse(text) {
222
+ const raw = String(text || "").trim();
223
+ if (!raw) {
224
+ throw new Error("Could not parse visible action selection (empty response)");
225
+ }
226
+ const candidates = [raw, ...extractCodeFenceCandidates(raw), ...extractBraceObjectCandidates(raw)];
227
+ const seen = /* @__PURE__ */ new Set();
228
+ let lastError = null;
229
+ for (const candidate of candidates) {
230
+ const normalized = candidate.trim();
231
+ if (!normalized || seen.has(normalized)) continue;
232
+ seen.add(normalized);
233
+ try {
234
+ const parsed = JSON.parse(normalized);
235
+ return {
236
+ selectedLabel: typeof parsed.selectedLabel === "string" && parsed.selectedLabel.trim() ? parsed.selectedLabel.trim() : null,
237
+ confidence: typeof parsed.confidence === "number" ? parsed.confidence : void 0,
238
+ reason: typeof parsed.reason === "string" ? parsed.reason : void 0
239
+ };
240
+ } catch (error) {
241
+ lastError = error;
242
+ }
243
+ }
244
+ const detail = lastError instanceof Error ? lastError.message : "unknown parse error";
245
+ throw new Error(`Could not parse visible action selection (${detail}). Preview: ${sanitizePreview(raw)}`);
246
+ }
247
+ async function analyzeWithAnthropicVision(apiKey, screenshotPaths, prompt) {
248
+ const anthropic = new Anthropic({ apiKey });
249
+ const imageContents = screenshotPaths.map((imagePath) => {
250
+ const imageData = readFileSync(imagePath);
251
+ const base64 = imageData.toString("base64");
252
+ return {
253
+ type: "image",
254
+ source: {
255
+ type: "base64",
256
+ media_type: "image/png",
257
+ data: base64
258
+ }
259
+ };
260
+ });
261
+ const response = await anthropic.messages.create({
262
+ model: "claude-sonnet-4-20250514",
263
+ max_tokens: 4096,
264
+ messages: [
265
+ {
266
+ role: "user",
267
+ content: [
268
+ ...imageContents,
269
+ { type: "text", text: prompt }
270
+ ]
271
+ }
272
+ ]
273
+ });
274
+ const textContent = response.content.find((c) => c.type === "text");
275
+ if (!textContent || textContent.type !== "text") {
276
+ throw new Error("No text response from Claude");
277
+ }
278
+ return parseAnalysisResponse(textContent.text);
279
+ }
280
+ async function selectVisibleActionWithAnthropicVision(apiKey, screenshotPath, prompt) {
281
+ const anthropic = new Anthropic({ apiKey });
282
+ const imageData = readFileSync(screenshotPath);
283
+ const base64 = imageData.toString("base64");
284
+ const response = await anthropic.messages.create({
285
+ model: "claude-sonnet-4-20250514",
286
+ max_tokens: 1024,
287
+ messages: [
288
+ {
289
+ role: "user",
290
+ content: [
291
+ {
292
+ type: "image",
293
+ source: {
294
+ type: "base64",
295
+ media_type: "image/png",
296
+ data: base64
297
+ }
298
+ },
299
+ { type: "text", text: prompt }
300
+ ]
301
+ }
302
+ ]
303
+ });
304
+ const textContent = response.content.find((content) => content.type === "text");
305
+ if (!textContent || textContent.type !== "text") {
306
+ throw new Error("No text response from Claude");
307
+ }
308
+ return parseVisibleActionSelectionResponse(textContent.text);
309
+ }
310
+ async function analyzeScreenshotsForActions(screenshotsDir, maxScreenshots = 20, provider) {
311
+ const files = readdirSync(screenshotsDir).filter((f) => f.endsWith(".png")).sort();
312
+ if (files.length < 2) {
313
+ return {
314
+ actions: [],
315
+ summary: "Not enough screenshots for action detection"
316
+ };
317
+ }
318
+ const selectedFiles = files.length > maxScreenshots ? sampleArray(files, maxScreenshots) : files;
319
+ const screenshotPaths = selectedFiles.map((file) => join(screenshotsDir, file));
320
+ const prompt = buildAnalysisPrompt(selectedFiles.length);
321
+ const providerCandidates = (Array.isArray(provider) ? provider : [provider]).filter(
322
+ (p) => !!p
323
+ );
324
+ console.log(`[AIActionDetector] Analyzing ${selectedFiles.length} screenshots...`);
325
+ const strategyErrors = [];
326
+ const apiKey = process.env.ANTHROPIC_API_KEY;
327
+ if (apiKey) {
328
+ try {
329
+ console.log("[AIActionDetector] Using Anthropic API vision (fast path)");
330
+ const result = await analyzeWithAnthropicVision(apiKey, screenshotPaths, prompt);
331
+ result.actions = normalizeDetectedActions(result.actions);
332
+ result.actionDetectionProvider = "anthropic-api vision (claude-sonnet-4-20250514)";
333
+ console.log(`[AIActionDetector] Detected ${result.actions.length} actions`);
334
+ return result;
335
+ } catch (error) {
336
+ const message = error instanceof Error ? error.message : "Unknown error";
337
+ strategyErrors.push(`anthropic-api vision: ${message}`);
338
+ console.warn(`[AIActionDetector] Anthropic vision failed: ${message}`);
339
+ }
340
+ }
341
+ for (const candidate of providerCandidates) {
342
+ if (!candidate.sendMessageWithImages) continue;
343
+ try {
344
+ console.log(`[AIActionDetector] Using provider: ${candidate.name} (image support)`);
345
+ const response = await candidate.sendMessageWithImages(prompt, screenshotPaths);
346
+ const result = parseAnalysisResponse(response);
347
+ result.actions = normalizeDetectedActions(result.actions);
348
+ result.actionDetectionProvider = candidate.name;
349
+ console.log(`[AIActionDetector] Detected ${result.actions.length} actions`);
350
+ return result;
351
+ } catch (error) {
352
+ const message = error instanceof Error ? error.message : "Unknown error";
353
+ strategyErrors.push(`${candidate.name}: ${message}`);
354
+ console.warn(`[AIActionDetector] Provider failed (${candidate.name}): ${message}`);
355
+ }
356
+ }
357
+ if (strategyErrors.length > 0) {
358
+ const combined = strategyErrors.slice(0, 3).join(" | ");
359
+ console.error("[AIActionDetector] Analysis failed across all vision strategies:", combined);
360
+ return {
361
+ actions: [],
362
+ summary: `AI analysis failed: ${combined}`
363
+ };
364
+ }
365
+ console.warn("[AIActionDetector] \u26A0\uFE0F No vision-capable provider available");
366
+ console.warn("[AIActionDetector] Set ANTHROPIC_API_KEY, ensure Claude CLI is available, or configure a vision-capable Ollama model");
367
+ return {
368
+ actions: [],
369
+ summary: "No vision-capable AI provider available",
370
+ skipped: true
371
+ };
372
+ }
373
+ async function selectVisibleActionFromScreenshot(screenshotPath, candidateLabels, provider) {
374
+ const normalizedCandidates = Array.from(
375
+ new Set(
376
+ (candidateLabels || []).map((candidate) => normalizeWhitespace(candidate)).filter(Boolean)
377
+ )
378
+ );
379
+ if (normalizedCandidates.length === 0) {
380
+ return null;
381
+ }
382
+ const prompt = buildVisibleActionSelectionPrompt(normalizedCandidates);
383
+ const providerCandidates = (Array.isArray(provider) ? provider : [provider]).filter(
384
+ (p) => !!p
385
+ );
386
+ const strategyErrors = [];
387
+ const apiKey = process.env.ANTHROPIC_API_KEY;
388
+ if (apiKey) {
389
+ try {
390
+ const result = await selectVisibleActionWithAnthropicVision(apiKey, screenshotPath, prompt);
391
+ return {
392
+ ...result,
393
+ providerName: "anthropic-api vision (claude-sonnet-4-20250514)"
394
+ };
395
+ } catch (error) {
396
+ const message = error instanceof Error ? error.message : "Unknown error";
397
+ strategyErrors.push(`anthropic-api vision: ${message}`);
398
+ }
399
+ }
400
+ for (const candidate of providerCandidates) {
401
+ if (!candidate.sendMessageWithImages) continue;
402
+ try {
403
+ const response = await candidate.sendMessageWithImages(prompt, [screenshotPath]);
404
+ const result = parseVisibleActionSelectionResponse(response);
405
+ return {
406
+ ...result,
407
+ providerName: candidate.name
408
+ };
409
+ } catch (error) {
410
+ const message = error instanceof Error ? error.message : "Unknown error";
411
+ strategyErrors.push(`${candidate.name}: ${message}`);
412
+ }
413
+ }
414
+ if (strategyErrors.length > 0) {
415
+ console.warn("[AIActionDetector] Visible action selection failed:", strategyErrors.slice(0, 2).join(" | "));
416
+ }
417
+ return null;
418
+ }
419
+ function generateMaestroYaml(actions, appId, appName, actionDetectionProvider) {
420
+ const lines = [
421
+ `# Auto-generated Maestro test flow`,
422
+ `# Generated by DiscoveryLab AI Action Detector`,
423
+ `# ${(/* @__PURE__ */ new Date()).toISOString()}`,
424
+ ""
425
+ ];
426
+ if (actionDetectionProvider) {
427
+ lines.push(`# AI Action Detection Provider: ${actionDetectionProvider}`);
428
+ lines.push("");
429
+ }
430
+ lines.push("appId: " + (appId || "com.example.app # TODO: Set your app ID"));
431
+ lines.push("");
432
+ if (appName) {
433
+ lines.push(`# App: ${appName}`);
434
+ lines.push("");
435
+ }
436
+ lines.push("---");
437
+ lines.push("");
438
+ for (const action of actions) {
439
+ if (action.confidence < 0.7) continue;
440
+ let commentDescription = action.description;
441
+ switch (action.type) {
442
+ case "launch":
443
+ lines.push(`- launchApp`);
444
+ break;
445
+ case "tap":
446
+ if (action.element) {
447
+ const literalElement = normalizeTapElementLabel(action.element || commentDescription);
448
+ lines.push(`- tapOn:`);
449
+ lines.push(` text: "${escapeYaml(literalElement)}"`);
450
+ } else if (action.coordinates) {
451
+ lines.push(`- tapOn:`);
452
+ lines.push(` point: "${action.coordinates.x}%,${action.coordinates.y}%"`);
453
+ }
454
+ break;
455
+ case "type":
456
+ if (action.text) {
457
+ const safeInputText = redactSensitiveTestInput(action.text, {
458
+ actionType: "inputText",
459
+ description: action.description,
460
+ fieldHint: action.element
461
+ });
462
+ if (safeInputText !== action.text && commentDescription) {
463
+ commentDescription = commentDescription.split(action.text).join(safeInputText);
464
+ }
465
+ lines.push(`- inputText: "${escapeYaml(safeInputText)}"`);
466
+ }
467
+ break;
468
+ case "scroll":
469
+ case "swipe":
470
+ if (action.direction) {
471
+ const direction = action.direction === "up" ? "DOWN" : action.direction === "down" ? "UP" : action.direction === "left" ? "RIGHT" : "LEFT";
472
+ lines.push(`- scroll:`);
473
+ lines.push(` direction: ${direction}`);
474
+ }
475
+ break;
476
+ case "back":
477
+ lines.push(`- pressKey: back`);
478
+ break;
479
+ case "wait":
480
+ lines.push(`- extendedWaitUntil:`);
481
+ lines.push(` visible: ".*"`);
482
+ lines.push(` timeout: 5000`);
483
+ break;
484
+ }
485
+ if (commentDescription) {
486
+ lines[lines.length - 1] += ` # ${commentDescription}`;
487
+ }
488
+ lines.push("");
489
+ }
490
+ return lines.join("\n");
491
+ }
492
+ function sampleArray(arr, count) {
493
+ if (arr.length <= count) return arr;
494
+ const result = [];
495
+ const step = (arr.length - 1) / (count - 1);
496
+ for (let i = 0; i < count; i++) {
497
+ const index = Math.round(i * step);
498
+ result.push(arr[index]);
499
+ }
500
+ return result;
501
+ }
502
+ function escapeYaml(str) {
503
+ return str.replace(/"/g, '\\"').replace(/\n/g, "\\n");
504
+ }
505
+
506
+ // src/core/integrations/local-network-proxy.ts
507
+ import { networkInterfaces } from "os";
508
+
509
+ // src/core/integrations/esvp-host-runtime.ts
510
+ import { spawn } from "child_process";
511
+ import { existsSync } from "fs";
512
+ import { createServer } from "net";
513
+ import { homedir } from "os";
514
+ import { dirname, join as join2, resolve } from "path";
515
+ import { fileURLToPath } from "url";
516
+ var MODULE_DIR = dirname(fileURLToPath(import.meta.url));
517
+ var PROJECT_ROOT = resolve(MODULE_DIR, "../../..");
518
+ var runtimeState = null;
519
+ var runtimeStartPromise = null;
520
+ async function startHostRuntimeCaptureSession(input) {
521
+ const runtime = await ensureRuntimeProcess();
522
+ return runtimeRequest(runtime, "/sessions/start", {
523
+ method: "POST",
524
+ body: JSON.stringify({
525
+ sessionId: input.sessionId,
526
+ advertiseHost: input.advertiseHost,
527
+ bindHost: input.bindHost,
528
+ captureMode: input.captureMode || "external-proxy",
529
+ maxDurationMs: input.maxDurationMs ?? null,
530
+ maxBodyCaptureBytes: input.maxBodyCaptureBytes ?? 16384,
531
+ meta: input.meta || null
532
+ })
533
+ });
534
+ }
535
+ async function drainHostRuntimeCaptureSession(sessionId) {
536
+ const runtime = getRunningRuntimeProcess();
537
+ if (!runtime) {
538
+ throw new Error("ESVP host runtime is not running.");
539
+ }
540
+ return runtimeRequest(runtime, `/sessions/${encodeURIComponent(sessionId)}/drain`, {
541
+ method: "POST",
542
+ body: JSON.stringify({ reason: "manual-stop" })
543
+ });
544
+ }
545
+ async function panicStopHostRuntime(reason = "manual-emergency-stop") {
546
+ const runtime = getRunningRuntimeProcess();
547
+ if (!runtime) {
548
+ return {
549
+ stopped: 0,
550
+ sessions: []
551
+ };
552
+ }
553
+ return runtimeRequest(runtime, "/panic-stop", {
554
+ method: "POST",
555
+ body: JSON.stringify({ reason })
556
+ });
557
+ }
558
+ async function shutdownHostRuntime() {
559
+ const runtime = runtimeState;
560
+ runtimeState = null;
561
+ runtimeStartPromise = null;
562
+ if (!runtime) return;
563
+ try {
564
+ runtime.child.stdin?.end();
565
+ } catch {
566
+ }
567
+ try {
568
+ const exited = await waitForChildExit(runtime.child, 750);
569
+ if (exited) return;
570
+ } catch {
571
+ }
572
+ try {
573
+ runtime.child.kill("SIGTERM");
574
+ } catch {
575
+ }
576
+ }
577
+ async function ensureRuntimeProcess() {
578
+ if (isRuntimeProcessAlive(runtimeState)) {
579
+ return runtimeState;
580
+ }
581
+ if (runtimeStartPromise) return runtimeStartPromise;
582
+ runtimeStartPromise = startRuntimeProcess().finally(() => {
583
+ runtimeStartPromise = null;
584
+ });
585
+ runtimeState = await runtimeStartPromise;
586
+ return runtimeState;
587
+ }
588
+ async function startRuntimeProcess() {
589
+ const port = await findFreePort();
590
+ const token = randomToken();
591
+ const launch = resolveRuntimeLaunchCommand();
592
+ const args = [
593
+ ...launch.args,
594
+ "--host",
595
+ "127.0.0.1",
596
+ "--port",
597
+ String(port),
598
+ "--token",
599
+ token
600
+ ];
601
+ const child = spawn(launch.command, args, {
602
+ stdio: ["pipe", "pipe", "pipe"],
603
+ env: process.env
604
+ });
605
+ const state = {
606
+ child,
607
+ command: launch,
608
+ token,
609
+ baseUrl: `http://127.0.0.1:${port}`,
610
+ port
611
+ };
612
+ child.stdout?.on("data", (chunk) => {
613
+ const text = String(chunk || "").trim();
614
+ if (text) console.log(`[esvp-host-runtime] ${text}`);
615
+ });
616
+ child.stderr?.on("data", (chunk) => {
617
+ const text = String(chunk || "").trim();
618
+ if (text) console.error(`[esvp-host-runtime] ${text}`);
619
+ });
620
+ child.once("exit", () => {
621
+ if (runtimeState?.child === child) {
622
+ runtimeState = null;
623
+ }
624
+ });
625
+ child.once("error", () => {
626
+ if (runtimeState?.child === child) {
627
+ runtimeState = null;
628
+ }
629
+ });
630
+ try {
631
+ await waitForRuntimeHealth(state);
632
+ } catch (error) {
633
+ try {
634
+ child.kill("SIGTERM");
635
+ } catch {
636
+ }
637
+ throw error;
638
+ }
639
+ return state;
640
+ }
641
+ function resolveRuntimeLaunchCommand() {
642
+ const binary = resolveBundledOrBuiltRuntimeBinary();
643
+ if (binary) {
644
+ return {
645
+ kind: "binary",
646
+ command: binary,
647
+ args: []
648
+ };
649
+ }
650
+ for (const root of resolveProjectRoots()) {
651
+ const manifestPath = join2(root, "runtime", "Cargo.toml");
652
+ if (existsSync(manifestPath)) {
653
+ return {
654
+ kind: "cargo",
655
+ command: "cargo",
656
+ args: ["run", "--quiet", "--release", "--manifest-path", manifestPath, "-p", "esvp-host-runtime", "--"]
657
+ };
658
+ }
659
+ }
660
+ throw new Error(
661
+ "ESVP host runtime was not found. Build the Rust runtime or set DISCOVERYLAB_ESVP_HOST_RUNTIME_BIN."
662
+ );
663
+ }
664
+ function resolveBundledOrBuiltRuntimeBinary() {
665
+ const explicit = normalizeOptionalString(process.env.DISCOVERYLAB_ESVP_HOST_RUNTIME_BIN);
666
+ if (explicit && existsSync(explicit)) return explicit;
667
+ const target = resolveTargetTriple();
668
+ const binaryName = process.platform === "win32" ? "esvp-host-runtime.exe" : "esvp-host-runtime";
669
+ const rustTarget = resolveRustTargetTriple(target);
670
+ const candidates = [
671
+ join2(homedir(), ".discoverylab", "runtime", "esvp-host-runtime", target, binaryName),
672
+ ...resolveProjectRoots().flatMap((root) => [
673
+ join2(root, "dist", "runtime", "esvp-host-runtime", target, binaryName),
674
+ join2(root, "runtime", "esvp-host-runtime", target, binaryName),
675
+ join2(root, "runtime", "target", rustTarget, "release", binaryName),
676
+ join2(root, "runtime", "target", "release", binaryName)
677
+ ])
678
+ ];
679
+ for (const candidate of candidates) {
680
+ if (existsSync(candidate)) return candidate;
681
+ }
682
+ return null;
683
+ }
684
+ function resolveTargetTriple() {
685
+ const platform = process.platform;
686
+ const arch = normalizeTargetArch(process.arch);
687
+ if (platform === "darwin" && arch === "arm64") return "darwin-arm64";
688
+ if (platform === "darwin" && arch === "x64") return "darwin-x64";
689
+ if (platform === "linux" && arch === "arm64") return "linux-arm64";
690
+ if (platform === "linux" && arch === "x64") return "linux-x64";
691
+ if (platform === "win32" && arch === "x64") return "win32-x64";
692
+ throw new Error(`Unsupported platform/arch for ESVP host runtime: ${platform}/${arch}`);
693
+ }
694
+ function resolveRustTargetTriple(target) {
695
+ const mapping = {
696
+ "darwin-arm64": "aarch64-apple-darwin",
697
+ "darwin-x64": "x86_64-apple-darwin",
698
+ "linux-arm64": "aarch64-unknown-linux-gnu",
699
+ "linux-x64": "x86_64-unknown-linux-gnu",
700
+ "win32-x64": "x86_64-pc-windows-msvc"
701
+ };
702
+ const resolved = mapping[target];
703
+ if (!resolved) {
704
+ throw new Error(`Unsupported Rust target for ESVP host runtime: ${target}`);
705
+ }
706
+ return resolved;
707
+ }
708
+ async function waitForRuntimeHealth(runtime) {
709
+ let lastError = null;
710
+ for (let attempt = 0; attempt < 60; attempt += 1) {
711
+ if (runtime.child.exitCode != null) {
712
+ throw new Error(`ESVP host runtime exited before becoming healthy (code ${runtime.child.exitCode}).`);
713
+ }
714
+ try {
715
+ await runtimeRequest(runtime, "/health", { method: "GET" }, 1500);
716
+ return;
717
+ } catch (error) {
718
+ lastError = error;
719
+ await sleep(250);
720
+ }
721
+ }
722
+ throw new Error(`ESVP host runtime failed health checks: ${safeErrorMessage(lastError)}`);
723
+ }
724
+ async function runtimeRequest(runtime, path, init, timeoutMs = 5e3) {
725
+ const controller = new AbortController();
726
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
727
+ try {
728
+ const response = await fetch(`${runtime.baseUrl}${path}`, {
729
+ ...init,
730
+ headers: {
731
+ Authorization: `Bearer ${runtime.token}`,
732
+ "Content-Type": "application/json",
733
+ ...init.headers || {}
734
+ },
735
+ signal: controller.signal
736
+ });
737
+ const text = await response.text();
738
+ const payload = text.trim() ? tryParseJson(text) : null;
739
+ if (!response.ok) {
740
+ const message = (payload && typeof payload === "object" && payload !== null ? payload.error || payload.message : null) || `Host runtime request failed (${response.status})`;
741
+ throw new Error(String(message));
742
+ }
743
+ return payload;
744
+ } catch (error) {
745
+ if (error instanceof DOMException && error.name === "AbortError") {
746
+ throw new Error(`ESVP host runtime request timed out after ${timeoutMs}ms: ${path}`);
747
+ }
748
+ throw error;
749
+ } finally {
750
+ clearTimeout(timeout);
751
+ }
752
+ }
753
+ function tryParseJson(text) {
754
+ try {
755
+ return JSON.parse(text);
756
+ } catch {
757
+ return { raw: text };
758
+ }
759
+ }
760
+ async function findFreePort() {
761
+ return new Promise((resolve2, reject) => {
762
+ const server = createServer();
763
+ server.unref();
764
+ server.once("error", reject);
765
+ server.listen(0, "127.0.0.1", () => {
766
+ const address = server.address();
767
+ const port = address && typeof address === "object" ? Number(address.port) : NaN;
768
+ server.close((error) => {
769
+ if (error) {
770
+ reject(error);
771
+ return;
772
+ }
773
+ if (!Number.isFinite(port) || port <= 0) {
774
+ reject(new Error("Failed to allocate a free port for the ESVP host runtime."));
775
+ return;
776
+ }
777
+ resolve2(port);
778
+ });
779
+ });
780
+ });
781
+ }
782
+ function randomToken() {
783
+ return `rt_${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
784
+ }
785
+ function resolveProjectRoots() {
786
+ return [.../* @__PURE__ */ new Set([PROJECT_ROOT, process.cwd()])];
787
+ }
788
+ function normalizeTargetArch(arch) {
789
+ if (arch === "x64") return "x64";
790
+ return arch;
791
+ }
792
+ function getRunningRuntimeProcess() {
793
+ return isRuntimeProcessAlive(runtimeState) ? runtimeState : null;
794
+ }
795
+ function isRuntimeProcessAlive(runtime) {
796
+ return Boolean(runtime && runtime.child.exitCode == null && !runtime.child.killed);
797
+ }
798
+ function waitForChildExit(child, timeoutMs) {
799
+ return new Promise((resolve2) => {
800
+ const timeout = setTimeout(() => {
801
+ cleanup();
802
+ resolve2(false);
803
+ }, timeoutMs);
804
+ const cleanup = () => {
805
+ clearTimeout(timeout);
806
+ child.off("exit", onExit);
807
+ child.off("error", onError);
808
+ };
809
+ const onExit = () => {
810
+ cleanup();
811
+ resolve2(true);
812
+ };
813
+ const onError = () => {
814
+ cleanup();
815
+ resolve2(true);
816
+ };
817
+ child.once("exit", onExit);
818
+ child.once("error", onError);
819
+ });
820
+ }
821
+ function sleep(ms) {
822
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
823
+ }
824
+ function normalizeOptionalString(value) {
825
+ if (typeof value !== "string") return null;
826
+ const trimmed = value.trim();
827
+ return trimmed || null;
828
+ }
829
+ function safeErrorMessage(error) {
830
+ return error instanceof Error ? error.message : String(error);
831
+ }
832
+
833
+ // src/core/integrations/local-network-proxy.ts
834
+ var activeProxies = /* @__PURE__ */ new Map();
835
+ var finalizationsInFlight = /* @__PURE__ */ new Map();
836
+ var cleanupRegistered = false;
837
+ async function ensureLocalCaptureProxyProfile(input) {
838
+ const profile = cloneProfile(input.profile);
839
+ if (!profile) {
840
+ return {
841
+ profile,
842
+ captureProxy: null,
843
+ usesExternalProxy: false,
844
+ appLabOwnedProxy: false
845
+ };
846
+ }
847
+ const captureMode = String(profile.capture?.mode || "").trim().toLowerCase();
848
+ const usesExternalProxy = captureMode === "external-proxy";
849
+ if (!usesExternalProxy) {
850
+ return {
851
+ profile,
852
+ captureProxy: null,
853
+ usesExternalProxy,
854
+ appLabOwnedProxy: false
855
+ };
856
+ }
857
+ if (hasExplicitProxy(profile)) {
858
+ return {
859
+ profile,
860
+ captureProxy: null,
861
+ usesExternalProxy,
862
+ appLabOwnedProxy: false
863
+ };
864
+ }
865
+ if (input.allowAppLabOwnedProxy === false) {
866
+ throw new Error(
867
+ "App Lab proxy emergency lock is enabled. Unlock it in Settings or provide an explicit external proxy host/port."
868
+ );
869
+ }
870
+ const proxy = await startLocalCaptureProxy({
871
+ sessionId: input.sessionId,
872
+ platform: input.platform,
873
+ deviceId: input.deviceId,
874
+ captureMode: resolveAppLabCaptureMode(profile),
875
+ lifecycle: input.lifecycle || null
876
+ });
877
+ profile.proxy = {
878
+ host: proxy.host,
879
+ port: proxy.port,
880
+ protocol: "http"
881
+ };
882
+ return {
883
+ profile,
884
+ captureProxy: proxy,
885
+ usesExternalProxy,
886
+ appLabOwnedProxy: true
887
+ };
888
+ }
889
+ async function stopLocalCaptureProxy(sessionId) {
890
+ const existing = activeProxies.get(sessionId) || null;
891
+ if (!existing) {
892
+ return {
893
+ captureProxy: null,
894
+ trace: null
895
+ };
896
+ }
897
+ activeProxies.delete(sessionId);
898
+ const drained = await drainHostRuntimeCaptureSession(sessionId).catch(() => null);
899
+ return {
900
+ captureProxy: normalizeCaptureProxyState(drained?.captureProxy, existing.captureProxy, false),
901
+ trace: normalizeTracePayload(drained?.trace || null)
902
+ };
903
+ }
904
+ function listLocalCaptureProxyStates() {
905
+ return [...activeProxies.values()].map((record) => normalizeCaptureProxyState(record.captureProxy, null, true)).filter(Boolean);
906
+ }
907
+ async function finalizeAllLocalCaptureProxySessions(input = {}) {
908
+ const sessionIds = [...activeProxies.keys()];
909
+ const reason = normalizeOptionalString2(input.reason) || "manual-emergency-stop";
910
+ const results = await Promise.all(
911
+ sessionIds.map(async (sessionId) => {
912
+ try {
913
+ return {
914
+ sessionId,
915
+ result: await finalizeLocalCaptureProxySession({
916
+ sourceSessionId: sessionId,
917
+ ...activeProxies.get(sessionId)?.lifecycle || {},
918
+ clearNetwork: true,
919
+ cleanupMeta: {
920
+ ...activeProxies.get(sessionId)?.lifecycle?.cleanupMeta || {},
921
+ finalize_reason: reason
922
+ }
923
+ })
924
+ };
925
+ } catch (error) {
926
+ return {
927
+ sessionId,
928
+ result: {
929
+ captureProxy: normalizeCaptureProxyState(activeProxies.get(sessionId)?.captureProxy || null, null, false),
930
+ traceAttached: false,
931
+ cleanupSessionId: null,
932
+ clearResult: null,
933
+ clearedAt: null,
934
+ finishResult: null,
935
+ errors: [safeErrorMessage2(error)]
936
+ }
937
+ };
938
+ }
939
+ })
940
+ );
941
+ return {
942
+ total: sessionIds.length,
943
+ finalized: results.length,
944
+ results
945
+ };
946
+ }
947
+ async function finalizeLocalCaptureProxySession(input) {
948
+ const existing = finalizationsInFlight.get(input.sourceSessionId);
949
+ if (existing) return existing;
950
+ const promise = finalizeLocalCaptureProxySessionInternal(input).finally(() => {
951
+ finalizationsInFlight.delete(input.sourceSessionId);
952
+ });
953
+ finalizationsInFlight.set(input.sourceSessionId, promise);
954
+ return promise;
955
+ }
956
+ async function finalizeLocalCaptureProxySessionInternal(input) {
957
+ const errors = [];
958
+ let clearResult = null;
959
+ let cleanupSessionId = null;
960
+ let clearedAt = null;
961
+ if (input.clearNetwork !== false) {
962
+ try {
963
+ clearResult = await clearESVPNetwork(input.sourceSessionId, input.serverUrl);
964
+ clearedAt = (/* @__PURE__ */ new Date()).toISOString();
965
+ } catch (error) {
966
+ errors.push(safeErrorMessage2(error));
967
+ const cleanup = await runFallbackNetworkCleanupSession({
968
+ executor: input.executor,
969
+ deviceId: input.deviceId,
970
+ sourceSessionId: input.sourceSessionId,
971
+ serverUrl: input.serverUrl,
972
+ cleanupMeta: input.cleanupMeta
973
+ });
974
+ cleanupSessionId = cleanup.cleanupSessionId;
975
+ if (cleanup.clearResult) {
976
+ clearResult = cleanup.clearResult;
977
+ clearedAt = (/* @__PURE__ */ new Date()).toISOString();
978
+ errors.length = 0;
979
+ }
980
+ if (cleanup.errors.length > 0) {
981
+ errors.splice(0, errors.length, ...cleanup.errors);
982
+ }
983
+ }
984
+ }
985
+ const stopped = await stopLocalCaptureProxy(input.sourceSessionId).catch((error) => {
986
+ errors.push(safeErrorMessage2(error));
987
+ return {
988
+ captureProxy: null,
989
+ trace: null
990
+ };
991
+ });
992
+ let traceAttached = false;
993
+ if (stopped.trace) {
994
+ try {
995
+ await attachESVPNetworkTrace(input.sourceSessionId, stopped.trace, input.serverUrl);
996
+ traceAttached = true;
997
+ } catch (error) {
998
+ errors.push(safeErrorMessage2(error));
999
+ }
1000
+ }
1001
+ const finishResult = await finishESVPSession(
1002
+ input.sourceSessionId,
1003
+ {
1004
+ captureLogcat: input.captureLogcat
1005
+ },
1006
+ input.serverUrl
1007
+ ).catch((error) => {
1008
+ errors.push(safeErrorMessage2(error));
1009
+ return null;
1010
+ });
1011
+ return {
1012
+ captureProxy: stopped.captureProxy,
1013
+ traceAttached,
1014
+ cleanupSessionId,
1015
+ clearResult,
1016
+ clearedAt,
1017
+ finishResult: isObjectRecord(finishResult) ? finishResult : null,
1018
+ errors
1019
+ };
1020
+ }
1021
+ function cloneProfile(profile) {
1022
+ if (!profile || typeof profile !== "object") return profile;
1023
+ return JSON.parse(JSON.stringify(profile));
1024
+ }
1025
+ function hasExplicitProxy(profile) {
1026
+ const proxy = profile.proxy;
1027
+ if (!proxy || typeof proxy !== "object" || Array.isArray(proxy)) return false;
1028
+ const normalizedProxy = proxy;
1029
+ const host = typeof normalizedProxy.host === "string" ? normalizedProxy.host.trim() : "";
1030
+ const port = Number(normalizedProxy.port);
1031
+ return Boolean(host) && Number.isFinite(port) && port > 0;
1032
+ }
1033
+ async function startLocalCaptureProxy(input) {
1034
+ const existing = activeProxies.get(input.sessionId);
1035
+ if (existing) return existing.captureProxy;
1036
+ const advertiseHost = resolveAdvertiseHost(input);
1037
+ const bindHost = resolveBindHost(advertiseHost);
1038
+ const maxDurationMs = input.lifecycle?.maxDurationMs ?? readProxyMaxDurationMs();
1039
+ const started = await startHostRuntimeCaptureSession({
1040
+ sessionId: input.sessionId,
1041
+ advertiseHost,
1042
+ bindHost,
1043
+ captureMode: input.captureMode || "external-proxy",
1044
+ maxDurationMs,
1045
+ maxBodyCaptureBytes: 16384,
1046
+ meta: {
1047
+ platform: normalizeOptionalString2(input.platform) || null,
1048
+ deviceId: normalizeOptionalString2(input.deviceId) || null,
1049
+ source: "applab-discovery"
1050
+ }
1051
+ });
1052
+ const captureProxy = normalizeCaptureProxyState(started.captureProxy, null, true);
1053
+ if (!captureProxy) {
1054
+ throw new Error("Host runtime did not return a capture proxy state.");
1055
+ }
1056
+ captureProxy.mitm = normalizeMitmState(started.mitm || null, null);
1057
+ activeProxies.set(input.sessionId, {
1058
+ sessionId: input.sessionId,
1059
+ captureProxy,
1060
+ lifecycle: input.lifecycle || null
1061
+ });
1062
+ registerCleanup();
1063
+ return captureProxy;
1064
+ }
1065
+ function resolveAdvertiseHost(input) {
1066
+ const explicit = normalizeOptionalString2(process.env.DISCOVERYLAB_NETWORK_PROXY_HOST);
1067
+ if (explicit) return explicit;
1068
+ const platform = String(input.platform || "").trim().toLowerCase();
1069
+ if (platform === "ios") return "127.0.0.1";
1070
+ if (platform === "android") {
1071
+ const deviceId = String(input.deviceId || "").trim();
1072
+ if (deviceId.startsWith("emulator-")) return "10.0.2.2";
1073
+ return inferLanIpAddress() || "127.0.0.1";
1074
+ }
1075
+ return "127.0.0.1";
1076
+ }
1077
+ function resolveBindHost(advertiseHost) {
1078
+ const explicit = normalizeOptionalString2(process.env.DISCOVERYLAB_NETWORK_PROXY_BIND_HOST);
1079
+ if (explicit) return explicit;
1080
+ if (advertiseHost === "127.0.0.1" || advertiseHost === "10.0.2.2") return "127.0.0.1";
1081
+ return "0.0.0.0";
1082
+ }
1083
+ function inferLanIpAddress() {
1084
+ const interfaces = networkInterfaces();
1085
+ for (const entries of Object.values(interfaces)) {
1086
+ for (const entry of entries || []) {
1087
+ if (!entry || entry.internal) continue;
1088
+ if (entry.family === "IPv4") return entry.address;
1089
+ }
1090
+ }
1091
+ return null;
1092
+ }
1093
+ function registerCleanup() {
1094
+ if (cleanupRegistered) return;
1095
+ cleanupRegistered = true;
1096
+ const cleanup = async (reason) => {
1097
+ const sessionIds = [...activeProxies.keys()];
1098
+ await Promise.allSettled(
1099
+ sessionIds.map((sessionId) => {
1100
+ const lifecycle = activeProxies.get(sessionId)?.lifecycle || null;
1101
+ if (lifecycle) {
1102
+ return finalizeLocalCaptureProxySession({
1103
+ sourceSessionId: sessionId,
1104
+ executor: lifecycle.executor,
1105
+ deviceId: lifecycle.deviceId,
1106
+ serverUrl: lifecycle.serverUrl,
1107
+ captureLogcat: lifecycle.captureLogcat,
1108
+ clearNetwork: true,
1109
+ cleanupMeta: {
1110
+ ...lifecycle.cleanupMeta || {},
1111
+ finalize_reason: reason
1112
+ }
1113
+ });
1114
+ }
1115
+ return stopLocalCaptureProxy(sessionId).catch(() => null);
1116
+ })
1117
+ );
1118
+ activeProxies.clear();
1119
+ await panicStopHostRuntime(reason).catch(() => null);
1120
+ await shutdownHostRuntime().catch(() => null);
1121
+ };
1122
+ process.once("beforeExit", () => {
1123
+ void cleanup("process-exit");
1124
+ });
1125
+ process.once("SIGINT", () => {
1126
+ void cleanup("sigint").finally(() => process.exit(0));
1127
+ });
1128
+ process.once("SIGTERM", () => {
1129
+ void cleanup("sigterm").finally(() => process.exit(0));
1130
+ });
1131
+ }
1132
+ function normalizeCaptureProxyState(value, fallback, active) {
1133
+ const record = isObjectRecord(value) ? value : null;
1134
+ const host = normalizeOptionalString2(record?.host) || fallback?.host || null;
1135
+ const port = Number(record?.port ?? fallback?.port ?? NaN);
1136
+ if (!host || !Number.isFinite(port) || port <= 0) {
1137
+ return fallback ? {
1138
+ ...fallback,
1139
+ active,
1140
+ port: Number.isFinite(fallback.port) ? fallback.port : null
1141
+ } : null;
1142
+ }
1143
+ return {
1144
+ id: normalizeOptionalString2(record?.id) || fallback?.id || `runtime-${Math.random().toString(36).slice(2, 10)}`,
1145
+ sessionId: normalizeOptionalString2(record?.sessionId) || fallback?.sessionId || "",
1146
+ active,
1147
+ bindHost: normalizeOptionalString2(record?.bindHost) || fallback?.bindHost || host,
1148
+ host,
1149
+ port,
1150
+ url: normalizeOptionalString2(record?.url) || fallback?.url || `http://${host}:${port}`,
1151
+ startedAt: normalizeOptionalString2(record?.startedAt) || fallback?.startedAt || (/* @__PURE__ */ new Date()).toISOString(),
1152
+ entryCount: clampInt(record?.entryCount ?? fallback?.entryCount ?? 0, 0, Number.MAX_SAFE_INTEGER, 0),
1153
+ captureMode: normalizeOptionalString2(record?.captureMode) === "external-mitm" || fallback?.captureMode === "external-mitm" ? "external-mitm" : "external-proxy",
1154
+ source: normalizeOptionalString2(record?.source) === "applab-external-mitm" || fallback?.source === "applab-external-mitm" ? "applab-external-mitm" : "applab-external-proxy",
1155
+ mitm: normalizeMitmState(record?.mitm, fallback?.mitm || null)
1156
+ };
1157
+ }
1158
+ function resolveAppLabCaptureMode(profile) {
1159
+ const capture = profile.capture;
1160
+ if (!capture || typeof capture !== "object" || Array.isArray(capture)) return "external-proxy";
1161
+ const applabMode = String(capture.applabMode || "").trim().toLowerCase();
1162
+ return applabMode === "external-mitm" ? "external-mitm" : "external-proxy";
1163
+ }
1164
+ function normalizeTracePayload(value) {
1165
+ if (!isObjectRecord(value)) return null;
1166
+ return value;
1167
+ }
1168
+ function normalizeMitmState(value, fallback) {
1169
+ const record = isObjectRecord(value) ? value : null;
1170
+ if (!record) return fallback || null;
1171
+ return {
1172
+ enabled: record.enabled === true || fallback?.enabled === true,
1173
+ rootCertPath: normalizeOptionalString2(record.rootCertPath) || fallback?.rootCertPath || null,
1174
+ platform: normalizeOptionalString2(record.platform) || fallback?.platform || null,
1175
+ deviceId: normalizeOptionalString2(record.deviceId) || fallback?.deviceId || null,
1176
+ certificateInstalled: typeof record.certificateInstalled === "boolean" ? record.certificateInstalled : fallback?.certificateInstalled,
1177
+ certificateInstallMethod: normalizeOptionalString2(record.certificateInstallMethod) || fallback?.certificateInstallMethod || null,
1178
+ warnings: Array.isArray(record.warnings) ? record.warnings.map((item) => String(item)).filter(Boolean) : fallback?.warnings || [],
1179
+ errors: Array.isArray(record.errors) ? record.errors.map((item) => String(item)).filter(Boolean) : fallback?.errors || []
1180
+ };
1181
+ }
1182
+ function normalizeOptionalString2(value) {
1183
+ if (typeof value !== "string") return null;
1184
+ const trimmed = value.trim();
1185
+ return trimmed ? trimmed : null;
1186
+ }
1187
+ function clampInt(value, min, max, fallback) {
1188
+ const numeric = Number(value);
1189
+ if (!Number.isFinite(numeric)) return fallback;
1190
+ return Math.max(min, Math.min(max, Math.round(numeric)));
1191
+ }
1192
+ function readProxyMaxDurationMs() {
1193
+ const raw = normalizeOptionalString2(process.env.DISCOVERYLAB_NETWORK_PROXY_MAX_DURATION_MS);
1194
+ if (raw == null) return 15 * 60 * 1e3;
1195
+ const durationMs = Number(raw);
1196
+ if (!Number.isFinite(durationMs)) return 15 * 60 * 1e3;
1197
+ if (durationMs <= 0) return null;
1198
+ return clampInt(durationMs, 3e4, 24 * 60 * 60 * 1e3, 15 * 60 * 1e3);
1199
+ }
1200
+ async function runFallbackNetworkCleanupSession(input) {
1201
+ const errors = [];
1202
+ const deviceId = normalizeOptionalString2(input.deviceId);
1203
+ const executor = normalizeOptionalExecutor(input.executor);
1204
+ if (!deviceId || !executor) {
1205
+ return {
1206
+ cleanupSessionId: null,
1207
+ clearResult: null,
1208
+ errors
1209
+ };
1210
+ }
1211
+ let cleanupSessionId = null;
1212
+ try {
1213
+ const created = await createESVPSession(
1214
+ {
1215
+ executor,
1216
+ deviceId,
1217
+ meta: {
1218
+ source: "applab-discovery-network-cleanup",
1219
+ cleanup_for_session_id: input.sourceSessionId,
1220
+ ...input.cleanupMeta || {}
1221
+ }
1222
+ },
1223
+ input.serverUrl
1224
+ );
1225
+ cleanupSessionId = normalizeOptionalString2(created?.session?.id || created?.id);
1226
+ if (!cleanupSessionId) {
1227
+ throw new Error("Failed to create a cleanup ESVP session.");
1228
+ }
1229
+ const cleared = await clearESVPNetwork(cleanupSessionId, input.serverUrl);
1230
+ await finishESVPSession(cleanupSessionId, { captureLogcat: false }, input.serverUrl).catch(() => null);
1231
+ return {
1232
+ cleanupSessionId,
1233
+ clearResult: isObjectRecord(cleared) ? cleared : null,
1234
+ errors
1235
+ };
1236
+ } catch (error) {
1237
+ errors.push(safeErrorMessage2(error));
1238
+ if (cleanupSessionId) {
1239
+ await finishESVPSession(cleanupSessionId, { captureLogcat: false }, input.serverUrl).catch(() => null);
1240
+ }
1241
+ return {
1242
+ cleanupSessionId,
1243
+ clearResult: null,
1244
+ errors
1245
+ };
1246
+ }
1247
+ }
1248
+ function normalizeOptionalExecutor(value) {
1249
+ const normalized = normalizeOptionalString2(value);
1250
+ if (!normalized) return null;
1251
+ if (normalized === "adb" || normalized === "ios-sim" || normalized === "maestro-ios" || normalized === "fake") {
1252
+ return normalized;
1253
+ }
1254
+ return null;
1255
+ }
1256
+ function safeErrorMessage2(error) {
1257
+ return error instanceof Error ? error.message : String(error);
1258
+ }
1259
+ function isObjectRecord(value) {
1260
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1261
+ }
1262
+
1263
+ // src/core/integrations/local-app-http-trace.ts
1264
+ import { randomBytes, randomUUID } from "crypto";
1265
+ var activeCollectorsBySession = /* @__PURE__ */ new Map();
1266
+ var activeCollectorsById = /* @__PURE__ */ new Map();
1267
+ function startLocalAppHttpTraceCollector(input) {
1268
+ const existing = activeCollectorsBySession.get(input.sessionId);
1269
+ if (existing) {
1270
+ return existing.state;
1271
+ }
1272
+ const collectorId = randomUUID();
1273
+ const host = inferAppLabExternalProxyHost({
1274
+ platform: input.platform,
1275
+ deviceId: input.deviceId,
1276
+ explicitHost: null
1277
+ }) || "127.0.0.1";
1278
+ const ingestPath = `/api/testing/mobile/recordings/${encodeURIComponent(input.recordingId)}/esvp/app-http-trace/${encodeURIComponent(collectorId)}`;
1279
+ const bootstrapPath = `/api/testing/mobile/app-http-trace/bootstrap?appId=${encodeURIComponent(String(input.appId || ""))}&recordingId=${encodeURIComponent(input.recordingId)}`;
1280
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1281
+ const state = {
1282
+ id: collectorId,
1283
+ sessionId: input.sessionId,
1284
+ recordingId: input.recordingId,
1285
+ appId: normalizeOptionalString3(input.appId),
1286
+ platform: normalizePlatform(input.platform),
1287
+ deviceId: normalizeOptionalString3(input.deviceId),
1288
+ active: true,
1289
+ host,
1290
+ port: input.serverPort,
1291
+ bootstrapPath,
1292
+ bootstrapUrl: `http://${host}:${input.serverPort}${bootstrapPath}`,
1293
+ ingestPath,
1294
+ ingestUrl: `http://${host}:${input.serverPort}${ingestPath}`,
1295
+ startedAt,
1296
+ entryCount: 0,
1297
+ maxBodyCaptureBytes: normalizePositiveNumber(input.maxBodyCaptureBytes) || 16384,
1298
+ source: "applab-local-app-http-trace",
1299
+ traceKind: "app_http_trace"
1300
+ };
1301
+ activeCollectorsBySession.set(input.sessionId, {
1302
+ state,
1303
+ token: randomBytes(18).toString("hex"),
1304
+ entries: [],
1305
+ createdAt: Date.now()
1306
+ });
1307
+ activeCollectorsById.set(state.id, input.sessionId);
1308
+ return state;
1309
+ }
1310
+ function resolveLocalAppHttpTraceCollectorById(collectorId) {
1311
+ const sessionId = activeCollectorsById.get(collectorId);
1312
+ if (!sessionId) return null;
1313
+ return activeCollectorsBySession.get(sessionId)?.state || null;
1314
+ }
1315
+ function getLocalAppHttpTraceBootstrap(input) {
1316
+ const appId = normalizeOptionalString3(input.appId);
1317
+ const recordingId = normalizeOptionalString3(input.recordingId);
1318
+ let bestMatch = null;
1319
+ for (const record of activeCollectorsBySession.values()) {
1320
+ if (!record.state.active) continue;
1321
+ if (appId && record.state.appId === appId) {
1322
+ if (!bestMatch || record.createdAt > bestMatch.createdAt) bestMatch = record;
1323
+ continue;
1324
+ }
1325
+ if (!appId && recordingId && record.state.recordingId === recordingId) {
1326
+ if (!bestMatch || record.createdAt > bestMatch.createdAt) bestMatch = record;
1327
+ }
1328
+ }
1329
+ if (!bestMatch) return null;
1330
+ return {
1331
+ ...bestMatch.state,
1332
+ bootstrap: buildBootstrapConfig(bestMatch)
1333
+ };
1334
+ }
1335
+ function ingestLocalAppHttpTrace(input) {
1336
+ const sessionId = activeCollectorsById.get(input.collectorId);
1337
+ if (!sessionId) {
1338
+ return {
1339
+ collector: null,
1340
+ accepted: 0
1341
+ };
1342
+ }
1343
+ const record = activeCollectorsBySession.get(sessionId);
1344
+ if (!record || !record.state.active) {
1345
+ return {
1346
+ collector: null,
1347
+ accepted: 0
1348
+ };
1349
+ }
1350
+ if (record.token !== String(input.authToken || "").trim()) {
1351
+ return {
1352
+ collector: null,
1353
+ accepted: 0
1354
+ };
1355
+ }
1356
+ const entries = extractEntries(input.payload);
1357
+ if (entries.length > 0) {
1358
+ record.entries.push(...entries);
1359
+ record.state = {
1360
+ ...record.state,
1361
+ entryCount: record.entries.length
1362
+ };
1363
+ }
1364
+ activeCollectorsBySession.set(sessionId, record);
1365
+ return {
1366
+ collector: record.state,
1367
+ accepted: entries.length
1368
+ };
1369
+ }
1370
+ async function finalizeLocalAppHttpTraceCollector(input) {
1371
+ const record = activeCollectorsBySession.get(input.sourceSessionId);
1372
+ if (!record) {
1373
+ return {
1374
+ collector: null,
1375
+ traceAttached: false,
1376
+ errors: []
1377
+ };
1378
+ }
1379
+ activeCollectorsBySession.delete(input.sourceSessionId);
1380
+ activeCollectorsById.delete(record.state.id);
1381
+ const finalState = {
1382
+ ...record.state,
1383
+ active: false,
1384
+ entryCount: record.entries.length
1385
+ };
1386
+ if (record.entries.length === 0) {
1387
+ return {
1388
+ collector: finalState,
1389
+ traceAttached: false,
1390
+ errors: []
1391
+ };
1392
+ }
1393
+ try {
1394
+ await attachESVPNetworkTrace(
1395
+ input.sourceSessionId,
1396
+ {
1397
+ trace_kind: "app_http_trace",
1398
+ label: "App Lab Local App HTTP Trace",
1399
+ format: "json",
1400
+ source: finalState.source,
1401
+ payload: {
1402
+ session_id: input.sourceSessionId,
1403
+ collector_id: finalState.id,
1404
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
1405
+ entries: record.entries
1406
+ },
1407
+ artifactMeta: {
1408
+ trace_kind: "app_http_trace",
1409
+ collector_id: finalState.id,
1410
+ entry_count: record.entries.length,
1411
+ source: finalState.source
1412
+ }
1413
+ },
1414
+ input.serverUrl
1415
+ );
1416
+ return {
1417
+ collector: finalState,
1418
+ traceAttached: true,
1419
+ errors: []
1420
+ };
1421
+ } catch (error) {
1422
+ return {
1423
+ collector: finalState,
1424
+ traceAttached: false,
1425
+ errors: [error instanceof Error ? error.message : String(error)]
1426
+ };
1427
+ }
1428
+ }
1429
+ function buildBootstrapConfig(record) {
1430
+ return {
1431
+ traceKind: "app_http_trace",
1432
+ source: "applab-local-app-http-trace",
1433
+ sessionId: record.state.sessionId,
1434
+ recordingId: record.state.recordingId,
1435
+ appId: record.state.appId,
1436
+ ingestUrl: record.state.ingestUrl,
1437
+ ingestPath: record.state.ingestPath,
1438
+ authHeader: "x-applab-trace-token",
1439
+ token: record.token,
1440
+ maxBodyCaptureBytes: record.state.maxBodyCaptureBytes
1441
+ };
1442
+ }
1443
+ function extractEntries(payload) {
1444
+ if (Array.isArray(payload)) {
1445
+ return payload.filter(isObject);
1446
+ }
1447
+ if (!isObject(payload)) {
1448
+ return [];
1449
+ }
1450
+ const nestedCandidates = [
1451
+ payload,
1452
+ isObject(payload.payload) ? payload.payload : null,
1453
+ isObject(payload.data) ? payload.data : null,
1454
+ isObject(payload.trace) ? payload.trace : null,
1455
+ isObject(payload.result) ? payload.result : null
1456
+ ].filter(Boolean);
1457
+ for (const candidate of nestedCandidates) {
1458
+ for (const key of ["entries", "events", "requests", "items"]) {
1459
+ if (Array.isArray(candidate[key])) {
1460
+ return candidate[key].filter(isObject);
1461
+ }
1462
+ }
1463
+ }
1464
+ return [payload];
1465
+ }
1466
+ function normalizePositiveNumber(value) {
1467
+ const parsed = Number(value);
1468
+ if (!Number.isFinite(parsed) || parsed <= 0) return null;
1469
+ return Math.round(parsed);
1470
+ }
1471
+ function normalizeOptionalString3(value) {
1472
+ if (typeof value !== "string") return null;
1473
+ const normalized = value.trim();
1474
+ return normalized || null;
1475
+ }
1476
+ function normalizePlatform(value) {
1477
+ const normalized = String(value || "").trim().toLowerCase();
1478
+ if (normalized === "ios" || normalized === "android") return normalized;
1479
+ return null;
1480
+ }
1481
+ function isObject(value) {
1482
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1483
+ }
1484
+
1485
+ // src/core/integrations/esvp-mobile.ts
1486
+ function createMobileNetworkCaptureMeta(input) {
1487
+ const resourceTypes = Array.isArray(input?.resourceTypes) && input.resourceTypes.length > 0 ? input.resourceTypes.map((value) => String(value)) : ["mobile-http-trace"];
1488
+ return {
1489
+ truncated: input?.truncated === true,
1490
+ maxEntries: Number.isFinite(input?.maxEntries) ? Number(input?.maxEntries) : 1200,
1491
+ resourceTypes,
1492
+ ...input
1493
+ };
1494
+ }
1495
+ function diagnoseESVPNetworkState(opts) {
1496
+ if (!opts.sourceSessionId) {
1497
+ return {
1498
+ status: "no_session",
1499
+ message: "No ESVP session attached to this project."
1500
+ };
1501
+ }
1502
+ if (opts.networkSupported === false) {
1503
+ const executorLabel = opts.executor === "maestro-ios" ? "iOS (Maestro)" : opts.executor || "this executor";
1504
+ return {
1505
+ status: "not_supported",
1506
+ message: `Network capture not supported for ${executorLabel}. Use external proxy or attach HAR manually.`
1507
+ };
1508
+ }
1509
+ if (!opts.configuredAt) {
1510
+ return {
1511
+ status: "not_configured",
1512
+ message: "Network profile was never configured for this session. Network capture requires configuring a profile before running actions."
1513
+ };
1514
+ }
1515
+ const proxyEntryCount = opts.managedProxy?.entry_count ?? 0;
1516
+ const traceCount = opts.traceCount ?? 0;
1517
+ if ((proxyEntryCount > 0 || traceCount > 0) && (!opts.entryCount || opts.entryCount === 0)) {
1518
+ const count = proxyEntryCount || traceCount;
1519
+ return {
1520
+ status: "has_traces_need_sync",
1521
+ message: `ESVP session has ${count} captured request${count !== 1 ? "s" : ""}. Click 'Sync Network Trace' to pull them.`,
1522
+ traceCount: count
1523
+ };
1524
+ }
1525
+ if (proxyEntryCount === 0 && traceCount === 0 && (!opts.entryCount || opts.entryCount === 0)) {
1526
+ return {
1527
+ status: "proxy_no_traffic",
1528
+ message: opts.captureMode === "external-proxy" ? "External proxy capture is configured but no network_trace has been attached yet." : "Managed proxy was active but captured zero requests. The device may not have routed traffic through the proxy.",
1529
+ detail: opts.captureMode === "external-proxy" ? void 0 : opts.managedProxy ? `Proxy bound to ${opts.managedProxy.bind_host || "?"}:${opts.managedProxy.port || "?"}` : void 0
1530
+ };
1531
+ }
1532
+ if (opts.entryCount && opts.entryCount > 0) {
1533
+ return {
1534
+ status: "synced",
1535
+ message: `${opts.entryCount} network entries synced.`,
1536
+ entryCount: opts.entryCount
1537
+ };
1538
+ }
1539
+ return {
1540
+ status: "unknown",
1541
+ message: "Unable to determine network capture state."
1542
+ };
1543
+ }
1544
+ async function collectESVPSessionNetworkData(sessionId, serverUrl) {
1545
+ const inspection = await inspectESVPSession(
1546
+ sessionId,
1547
+ {
1548
+ includeArtifacts: true,
1549
+ includeTranscript: false
1550
+ },
1551
+ serverUrl
1552
+ );
1553
+ const artifacts = Array.isArray(inspection?.artifacts) ? inspection.artifacts : [];
1554
+ const networkState = await getESVPSessionNetwork(sessionId, serverUrl).catch(() => null);
1555
+ const publicNetworkState = isObject2(networkState?.network) ? networkState.network : null;
1556
+ const normalized = await normalizeESVPNetworkArtifacts(sessionId, artifacts, serverUrl);
1557
+ const traceKinds = Array.isArray(publicNetworkState?.trace_kinds) ? publicNetworkState.trace_kinds.map((value) => String(value)) : normalized.traceKinds;
1558
+ const networkCapture = createMobileNetworkCaptureMeta({
1559
+ ...normalized.networkCapture,
1560
+ traceKinds,
1561
+ source: "esvp-mobile",
1562
+ sessionId,
1563
+ managedProxy: publicNetworkState?.managed_proxy || null,
1564
+ effectiveProfile: publicNetworkState?.effective_profile || null
1565
+ });
1566
+ return {
1567
+ networkEntries: normalized.networkEntries,
1568
+ networkCapture,
1569
+ traceKinds,
1570
+ networkState: publicNetworkState
1571
+ };
1572
+ }
1573
+ async function validateMaestroRecordingWithESVP(recording, optionsOrServerUrl) {
1574
+ const options = normalizeValidationOptions(optionsOrServerUrl);
1575
+ const executor = recording.platform === "android" ? "adb" : "maestro-ios";
1576
+ const connection = await getESVPConnection(options.serverUrl);
1577
+ const normalizedAppId = normalizeRecordingAppId(recording.appId);
1578
+ const bootstrap = await buildRecordingBootstrapActions(recording, {
1579
+ appId: normalizedAppId,
1580
+ screenshotPath: options.bootstrapScreenshotPath
1581
+ });
1582
+ const effectiveRecordingActions = [...bootstrap.actions, ...recording.actions || []];
1583
+ const translated = translateMaestroActionsToESVP(effectiveRecordingActions, { appId: normalizedAppId });
1584
+ const shouldCaptureLogcat = resolveCaptureLogcat(executor, options.captureLogcat);
1585
+ const requestedNetworkProfile = normalizeRequestedNetworkProfile(options.network, {
1586
+ platform: recording.platform,
1587
+ deviceId: recording.deviceId
1588
+ });
1589
+ if (translated.actions.length === 0) {
1590
+ return {
1591
+ supported: false,
1592
+ reason: "No actions compatible with the public ESVP contract were found in this Maestro recording.",
1593
+ connectionMode: connection.mode,
1594
+ serverUrl: connection.serverUrl,
1595
+ executor,
1596
+ translatedActions: translated.actions,
1597
+ skippedActions: translated.skipped,
1598
+ networkEntries: [],
1599
+ networkCapture: createMobileNetworkCaptureMeta({
1600
+ source: "esvp-mobile",
1601
+ sessionId: null
1602
+ }),
1603
+ traceKinds: [],
1604
+ bootstrap: bootstrap.summary,
1605
+ recovery: null
1606
+ };
1607
+ }
1608
+ let translatedActions = translated.actions;
1609
+ let skippedActions = [...translated.skipped];
1610
+ let recovery = null;
1611
+ let execution = await runValidationSourceSession(
1612
+ recording,
1613
+ {
1614
+ executor,
1615
+ appId: normalizedAppId,
1616
+ translatedActions,
1617
+ requestedNetworkProfile,
1618
+ captureLogcat: shouldCaptureLogcat,
1619
+ serverUrl: options.serverUrl,
1620
+ appTraceServerPort: options.appTraceServerPort,
1621
+ metaSource: "applab-discovery-maestro-validation",
1622
+ allowAppLabOwnedProxyAutostart: options.allowAppLabOwnedProxyAutostart
1623
+ }
1624
+ );
1625
+ const initialSourceSessionId = execution.sourceSessionId;
1626
+ if (executor === "maestro-ios" && didESVPRunFail(execution.run)) {
1627
+ const recoveryPlan = await buildIOSVisibleTextRecoveryPlan(
1628
+ execution.sourceSessionId,
1629
+ translatedActions,
1630
+ options.serverUrl,
1631
+ options.recoveryVisionProvider
1632
+ );
1633
+ if (recoveryPlan) {
1634
+ const retryExecution = await runValidationSourceSession(
1635
+ recording,
1636
+ {
1637
+ executor,
1638
+ appId: normalizedAppId,
1639
+ translatedActions: recoveryPlan.prunedActions,
1640
+ requestedNetworkProfile,
1641
+ captureLogcat: shouldCaptureLogcat,
1642
+ serverUrl: options.serverUrl,
1643
+ appTraceServerPort: options.appTraceServerPort,
1644
+ metaSource: "applab-discovery-maestro-validation-recovered",
1645
+ extraMeta: {
1646
+ recovery_strategy: recoveryPlan.strategy,
1647
+ recovery_source_session_id: execution.sourceSessionId,
1648
+ recovery_pruned_action_count: recoveryPlan.prunedSkipped.length
1649
+ },
1650
+ allowAppLabOwnedProxyAutostart: options.allowAppLabOwnedProxyAutostart
1651
+ }
1652
+ );
1653
+ if (shouldPreferRecoveredExecution(execution.run, retryExecution.run)) {
1654
+ execution = retryExecution;
1655
+ translatedActions = recoveryPlan.prunedActions;
1656
+ skippedActions = [...skippedActions, ...recoveryPlan.prunedSkipped];
1657
+ recovery = {
1658
+ applied: true,
1659
+ strategy: recoveryPlan.strategy,
1660
+ initialSourceSessionId,
1661
+ finalSourceSessionId: retryExecution.sourceSessionId,
1662
+ prunedActionCount: recoveryPlan.prunedSkipped.length,
1663
+ resumedFromActionIndex: recoveryPlan.resumeFromIndex,
1664
+ matchedActionIndex: recoveryPlan.matchedActionIndex,
1665
+ matchedActionLabel: recoveryPlan.matchedActionLabel,
1666
+ checkpointScreenshotPath: recoveryPlan.checkpointScreenshotPath,
1667
+ visibleTextPreview: recoveryPlan.visibleTextPreview
1668
+ };
1669
+ }
1670
+ }
1671
+ }
1672
+ const sourceSessionId = execution.sourceSessionId;
1673
+ const replay = options.replay === false ? null : await replayESVPSession(
1674
+ sourceSessionId,
1675
+ {
1676
+ executor,
1677
+ deviceId: recording.deviceId,
1678
+ captureLogcat: shouldCaptureLogcat,
1679
+ meta: {
1680
+ source: "applab-discovery-maestro-replay",
1681
+ recording_id: recording.id,
1682
+ ...recovery?.applied ? { recovery_strategy: recovery.strategy } : {}
1683
+ }
1684
+ },
1685
+ options.serverUrl
1686
+ );
1687
+ const replaySessionId = replay?.replay_session?.id ? String(replay.replay_session.id) : null;
1688
+ const replayConsistency = replaySessionId ? await getESVPReplayConsistency(replaySessionId, options.serverUrl).catch(() => null) : null;
1689
+ const networkData = await collectESVPSessionNetworkData(sourceSessionId, options.serverUrl).catch(() => ({
1690
+ networkEntries: [],
1691
+ networkCapture: createMobileNetworkCaptureMeta({
1692
+ source: "esvp-mobile",
1693
+ sessionId: sourceSessionId
1694
+ }),
1695
+ traceKinds: [],
1696
+ networkState: null
1697
+ }));
1698
+ const networkState = mergeNetworkState(
1699
+ networkData.networkState,
1700
+ extractNetworkStateFromConfig(execution.networkConfigResult)
1701
+ );
1702
+ if (networkState && execution.cleanupError && !networkState.last_error) {
1703
+ networkState.last_error = execution.cleanupError;
1704
+ }
1705
+ if (networkState && execution.clearedAt && !networkState.cleared_at) {
1706
+ networkState.cleared_at = execution.clearedAt;
1707
+ }
1708
+ return {
1709
+ supported: true,
1710
+ connectionMode: connection.mode,
1711
+ serverUrl: connection.serverUrl,
1712
+ executor,
1713
+ translatedActions,
1714
+ skippedActions,
1715
+ sourceSessionId,
1716
+ replaySessionId,
1717
+ runSummary: execution.run,
1718
+ checkpointComparison: replay?.checkpoint_comparison || null,
1719
+ replayConsistency: replayConsistency?.replay_consistency || null,
1720
+ networkEntries: networkData.networkEntries,
1721
+ networkCapture: networkData.networkCapture,
1722
+ traceKinds: networkData.traceKinds,
1723
+ networkState,
1724
+ managedProxy: networkState?.managed_proxy || null,
1725
+ captureProxy: execution.captureProxy || null,
1726
+ appTraceCollector: execution.appTraceCollector || null,
1727
+ networkProfileApplied: isObject2(execution.networkConfigResult?.applied) ? execution.networkConfigResult?.applied : null,
1728
+ bootstrap: bootstrap.summary,
1729
+ recovery
1730
+ };
1731
+ }
1732
+ async function runValidationSourceSession(recording, input) {
1733
+ const created = await createESVPSession(
1734
+ {
1735
+ executor: input.executor,
1736
+ deviceId: recording.deviceId,
1737
+ meta: {
1738
+ source: input.metaSource,
1739
+ recording_id: recording.id,
1740
+ recording_name: recording.name,
1741
+ recording_platform: recording.platform,
1742
+ recording_device_name: recording.deviceName || null,
1743
+ ...input.appId ? { appId: input.appId, app_id: input.appId } : {},
1744
+ ...input.extraMeta || {}
1745
+ }
1746
+ },
1747
+ input.serverUrl
1748
+ );
1749
+ const sourceSessionId = String(created?.session?.id || created?.id || "");
1750
+ if (!sourceSessionId) {
1751
+ throw new Error("Failed to create an ESVP session for Maestro validation.");
1752
+ }
1753
+ const appTraceMode = getRequestedAppLabCaptureMode(input.requestedNetworkProfile) === "app-http-trace";
1754
+ const preparedNetworkProfile = appTraceMode ? {
1755
+ profile: input.requestedNetworkProfile,
1756
+ captureProxy: null,
1757
+ usesExternalProxy: false,
1758
+ appLabOwnedProxy: false
1759
+ } : await ensureLocalCaptureProxyProfile({
1760
+ sessionId: sourceSessionId,
1761
+ profile: input.requestedNetworkProfile,
1762
+ platform: recording.platform,
1763
+ deviceId: recording.deviceId,
1764
+ allowAppLabOwnedProxy: input.allowAppLabOwnedProxyAutostart,
1765
+ lifecycle: {
1766
+ executor: input.executor,
1767
+ deviceId: recording.deviceId,
1768
+ serverUrl: input.serverUrl,
1769
+ captureLogcat: input.captureLogcat,
1770
+ cleanupMeta: {
1771
+ recording_id: recording.id,
1772
+ recording_name: recording.name,
1773
+ recording_platform: recording.platform
1774
+ }
1775
+ }
1776
+ });
1777
+ const appTraceCollector = appTraceMode ? startLocalAppHttpTraceCollector({
1778
+ sessionId: sourceSessionId,
1779
+ recordingId: recording.id,
1780
+ appId: input.appId || recording.appId,
1781
+ platform: recording.platform,
1782
+ deviceId: recording.deviceId,
1783
+ serverPort: input.appTraceServerPort || 3847
1784
+ }) : null;
1785
+ let networkConfigResult = null;
1786
+ if (preparedNetworkProfile.profile && !appTraceMode) {
1787
+ networkConfigResult = await configureESVPNetwork(
1788
+ sourceSessionId,
1789
+ preparedNetworkProfile.profile,
1790
+ input.serverUrl
1791
+ );
1792
+ }
1793
+ let run = null;
1794
+ let runError = null;
1795
+ try {
1796
+ run = await runESVPActions(
1797
+ sourceSessionId,
1798
+ {
1799
+ actions: input.translatedActions,
1800
+ finish: false,
1801
+ captureLogcat: input.captureLogcat,
1802
+ checkpointAfterEach: true
1803
+ },
1804
+ input.serverUrl
1805
+ );
1806
+ } catch (error) {
1807
+ runError = error;
1808
+ }
1809
+ const finalization = appTraceMode ? await finalizeLocalAppHttpTraceCollector({
1810
+ sourceSessionId,
1811
+ serverUrl: input.serverUrl
1812
+ }) : await finalizeLocalCaptureProxySession({
1813
+ sourceSessionId,
1814
+ executor: input.executor,
1815
+ deviceId: recording.deviceId,
1816
+ serverUrl: input.serverUrl,
1817
+ captureLogcat: input.captureLogcat,
1818
+ clearNetwork: Boolean(preparedNetworkProfile.profile),
1819
+ cleanupMeta: {
1820
+ recording_id: recording.id,
1821
+ recording_name: recording.name,
1822
+ recording_platform: recording.platform
1823
+ }
1824
+ });
1825
+ const finalizationCollector = "collector" in finalization ? finalization.collector || null : null;
1826
+ const finalizationClearedAt = "clearedAt" in finalization ? finalization.clearedAt : null;
1827
+ if (runError) {
1828
+ throw runError;
1829
+ }
1830
+ return {
1831
+ sourceSessionId,
1832
+ run,
1833
+ networkConfigResult,
1834
+ captureProxy: preparedNetworkProfile.captureProxy,
1835
+ appTraceCollector: finalizationCollector || appTraceCollector,
1836
+ cleanupError: finalization.errors[0] || null,
1837
+ clearedAt: finalizationClearedAt
1838
+ };
1839
+ }
1840
+ function didESVPRunFail(run) {
1841
+ return Boolean(run?.failed || run?.session?.status === "failed");
1842
+ }
1843
+ function getESVPRunCheckpointCount(run) {
1844
+ return Number.isFinite(run?.session?.checkpoint_count) ? Number(run.session.checkpoint_count) : 0;
1845
+ }
1846
+ function shouldPreferRecoveredExecution(initialRun, retryRun) {
1847
+ if (!didESVPRunFail(retryRun)) return true;
1848
+ return getESVPRunCheckpointCount(retryRun) > getESVPRunCheckpointCount(initialRun);
1849
+ }
1850
+ function getTranslatedActionId(action, index) {
1851
+ if (typeof action?.checkpointLabel === "string" && action.checkpointLabel.trim()) {
1852
+ return action.checkpointLabel.trim();
1853
+ }
1854
+ return `esvp_action_${String(index + 1).padStart(3, "0")}`;
1855
+ }
1856
+ function normalizeMatchText(value) {
1857
+ return String(value || "").normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
1858
+ }
1859
+ function normalizeCandidateLabel(value) {
1860
+ return String(value || "").replace(/\s+/g, " ").trim();
1861
+ }
1862
+ function isLikelyBootstrapAnchorLabel(value) {
1863
+ const label = normalizeCandidateLabel(value);
1864
+ if (!label) return false;
1865
+ if (label.length > 24) return false;
1866
+ if (/@|https?:\/\/|www\./i.test(label)) return false;
1867
+ if (/\d/.test(label)) return false;
1868
+ const words = label.split(/\s+/).filter(Boolean);
1869
+ if (words.length === 0 || words.length > 3) return false;
1870
+ const lowered = label.toLowerCase();
1871
+ if (["preferred name", "phone number", "enable notifications", "tap to see subscription options"].includes(lowered)) {
1872
+ return false;
1873
+ }
1874
+ return true;
1875
+ }
1876
+ async function inferBootstrapAnchorFromScreenshot(screenshotPath) {
1877
+ const targetPath = normalizeCandidateLabel(screenshotPath);
1878
+ if (!targetPath) return null;
1879
+ const ocrResult = await recognizeText(targetPath, {
1880
+ recognitionLevel: "accurate",
1881
+ languages: ["en-US", "pt-BR"]
1882
+ }).catch(() => null);
1883
+ const text = typeof ocrResult?.text === "string" ? ocrResult.text : "";
1884
+ if (!ocrResult?.success || !text.trim()) {
1885
+ return null;
1886
+ }
1887
+ const lines = text.split(/\n+/).map((line) => normalizeCandidateLabel(line)).filter(Boolean);
1888
+ const counts = /* @__PURE__ */ new Map();
1889
+ const ordered = [];
1890
+ for (const line of lines) {
1891
+ if (!isLikelyBootstrapAnchorLabel(line)) continue;
1892
+ const normalized = normalizeMatchText(line);
1893
+ if (!normalized) continue;
1894
+ counts.set(normalized, (counts.get(normalized) || 0) + 1);
1895
+ if (!ordered.includes(normalized)) {
1896
+ ordered.push(normalized);
1897
+ }
1898
+ }
1899
+ for (const normalized of ordered) {
1900
+ if ((counts.get(normalized) || 0) < 2) continue;
1901
+ const original = lines.find((line) => normalizeMatchText(line) === normalized);
1902
+ if (original) {
1903
+ return original;
1904
+ }
1905
+ }
1906
+ return null;
1907
+ }
1908
+ async function buildRecordingBootstrapActions(recording, options) {
1909
+ const actions = [];
1910
+ const launchAppId = normalizeRecordingAppId(options.appId || recording.appId);
1911
+ const hasExplicitLaunch = (recording.actions || []).some((action) => action?.type === "launch");
1912
+ if (launchAppId && !hasExplicitLaunch) {
1913
+ actions.push({
1914
+ id: "bootstrap_launch",
1915
+ type: "launch",
1916
+ timestamp: Date.now(),
1917
+ description: `Launch ${launchAppId}`,
1918
+ text: launchAppId,
1919
+ appId: launchAppId
1920
+ });
1921
+ }
1922
+ const initialAnchorLabel = await inferBootstrapAnchorFromScreenshot(options.screenshotPath);
1923
+ const firstActionLabel = normalizeMatchText((recording.actions || [])[0]?.text);
1924
+ if (initialAnchorLabel && normalizeMatchText(initialAnchorLabel) !== firstActionLabel) {
1925
+ actions.push({
1926
+ id: "bootstrap_anchor_001",
1927
+ type: "tap",
1928
+ timestamp: Date.now(),
1929
+ description: `Return to ${initialAnchorLabel} screen`,
1930
+ text: initialAnchorLabel
1931
+ });
1932
+ }
1933
+ return {
1934
+ actions,
1935
+ summary: {
1936
+ applied: actions.length > 0,
1937
+ launchInserted: actions.some((action) => action.type === "launch"),
1938
+ initialAnchorLabel: initialAnchorLabel || null,
1939
+ actionCount: actions.length,
1940
+ screenshotPath: options.screenshotPath || null
1941
+ }
1942
+ };
1943
+ }
1944
+ function previewVisibleText(value, maxLength = 220) {
1945
+ const normalized = String(value || "").replace(/\s+/g, " ").trim();
1946
+ if (!normalized) return "";
1947
+ return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
1948
+ }
1949
+ function getActionTextCandidates(action) {
1950
+ const args = isObject2(action?.args) ? action.args : {};
1951
+ const candidates = [];
1952
+ const seen = /* @__PURE__ */ new Set();
1953
+ const push = (value) => {
1954
+ const normalized = String(value || "").trim();
1955
+ if (!normalized || seen.has(normalized)) return;
1956
+ seen.add(normalized);
1957
+ candidates.push(normalized);
1958
+ };
1959
+ push(args.text);
1960
+ push(args.selector);
1961
+ if (Array.isArray(args.selectors)) {
1962
+ for (const selector of args.selectors) {
1963
+ if (!isObject2(selector)) continue;
1964
+ push(selector.selector);
1965
+ push(selector.label);
1966
+ }
1967
+ }
1968
+ return candidates;
1969
+ }
1970
+ function scoreActionAgainstVisibleText(action, visibleText) {
1971
+ const haystack = normalizeMatchText(visibleText);
1972
+ if (!haystack) return 0;
1973
+ let bestScore = 0;
1974
+ for (const candidate of getActionTextCandidates(action)) {
1975
+ const needle = normalizeMatchText(candidate);
1976
+ if (!needle) continue;
1977
+ if (haystack.includes(needle)) {
1978
+ bestScore = Math.max(bestScore, 1);
1979
+ continue;
1980
+ }
1981
+ const tokens = needle.split(" ").filter((part) => part.length > 1);
1982
+ if (tokens.length === 0) continue;
1983
+ const matchedTokens = tokens.filter((part) => haystack.includes(part)).length;
1984
+ if (!matchedTokens) continue;
1985
+ const score = matchedTokens / tokens.length * 0.45;
1986
+ if (score > bestScore) {
1987
+ bestScore = score;
1988
+ }
1989
+ }
1990
+ return bestScore;
1991
+ }
1992
+ function getLatestScreenshotArtifactPath(artifacts) {
1993
+ for (let index = artifacts.length - 1; index >= 0; index -= 1) {
1994
+ const artifact = artifacts[index];
1995
+ if (artifact?.kind !== "screenshot") continue;
1996
+ if (typeof artifact.abs_path === "string" && artifact.abs_path.trim()) {
1997
+ return artifact.abs_path.trim();
1998
+ }
1999
+ }
2000
+ return null;
2001
+ }
2002
+ async function buildIOSVisibleTextRecoveryPlan(sourceSessionId, translatedActions, serverUrl, recoveryVisionProvider) {
2003
+ const inspection = await inspectESVPSession(
2004
+ sourceSessionId,
2005
+ {
2006
+ includeArtifacts: true,
2007
+ includeTranscript: false
2008
+ },
2009
+ serverUrl
2010
+ ).catch(() => null);
2011
+ const completedCount = Number.isFinite(inspection?.session?.checkpoint_count) ? Number(inspection.session.checkpoint_count) : 0;
2012
+ if (completedCount <= 0 || completedCount >= translatedActions.length) {
2013
+ return null;
2014
+ }
2015
+ const artifacts = Array.isArray(inspection?.artifacts) ? inspection.artifacts : [];
2016
+ const checkpointScreenshotPath = getLatestScreenshotArtifactPath(artifacts);
2017
+ if (!checkpointScreenshotPath) {
2018
+ return null;
2019
+ }
2020
+ const ocrResult = await recognizeText(checkpointScreenshotPath, {
2021
+ recognitionLevel: "accurate",
2022
+ languages: ["en-US", "pt-BR"]
2023
+ }).catch(() => null);
2024
+ const visibleText = typeof ocrResult?.text === "string" ? ocrResult.text : "";
2025
+ if (!ocrResult?.success || !visibleText.trim()) {
2026
+ return null;
2027
+ }
2028
+ const currentIndex = completedCount;
2029
+ const currentScore = scoreActionAgainstVisibleText(translatedActions[currentIndex], visibleText);
2030
+ if (currentScore >= 0.7) {
2031
+ return null;
2032
+ }
2033
+ const maxLookahead = Math.min(translatedActions.length, currentIndex + 3);
2034
+ let matchedActionIndex = -1;
2035
+ let matchedActionScore = 0;
2036
+ for (let index = currentIndex + 1; index < maxLookahead; index += 1) {
2037
+ const score = scoreActionAgainstVisibleText(translatedActions[index], visibleText);
2038
+ if (score > matchedActionScore) {
2039
+ matchedActionScore = score;
2040
+ matchedActionIndex = index;
2041
+ }
2042
+ }
2043
+ if (matchedActionIndex <= currentIndex || matchedActionScore < 0.7) {
2044
+ const visibleActionCandidates = translatedActions.slice(currentIndex, maxLookahead).map((action) => getActionTextCandidates(action)[0] || String(action?.name || "action")).filter(Boolean);
2045
+ const visionSelection = await selectVisibleActionFromScreenshot(
2046
+ checkpointScreenshotPath,
2047
+ visibleActionCandidates,
2048
+ recoveryVisionProvider
2049
+ ).catch(() => null);
2050
+ if (!visionSelection?.selectedLabel) {
2051
+ return null;
2052
+ }
2053
+ const selectedNormalized = normalizeMatchText(visionSelection.selectedLabel);
2054
+ matchedActionIndex = translatedActions.findIndex((action, index) => {
2055
+ if (index < currentIndex || index >= maxLookahead) return false;
2056
+ return getActionTextCandidates(action).some((candidate) => normalizeMatchText(candidate) === selectedNormalized);
2057
+ });
2058
+ if (matchedActionIndex <= currentIndex) {
2059
+ return null;
2060
+ }
2061
+ }
2062
+ const prunedActions = translatedActions.filter((_, index) => index < currentIndex || index >= matchedActionIndex);
2063
+ if (prunedActions.length === translatedActions.length) {
2064
+ return null;
2065
+ }
2066
+ const prunedSkipped = translatedActions.slice(currentIndex, matchedActionIndex).map((action, offset) => ({
2067
+ actionId: getTranslatedActionId(action, currentIndex + offset),
2068
+ type: String(action?.name || "action"),
2069
+ reason: "pruned after checkpoint OCR matched a later visible UI action"
2070
+ }));
2071
+ return {
2072
+ strategy: "ios-visible-text-reconciliation",
2073
+ prunedActions,
2074
+ prunedSkipped,
2075
+ checkpointScreenshotPath,
2076
+ visibleTextPreview: previewVisibleText(visibleText),
2077
+ resumeFromIndex: currentIndex,
2078
+ matchedActionIndex,
2079
+ matchedActionLabel: getActionTextCandidates(translatedActions[matchedActionIndex])[0] || null
2080
+ };
2081
+ }
2082
+ function normalizeValidationOptions(input) {
2083
+ if (typeof input === "string") {
2084
+ return {
2085
+ serverUrl: input
2086
+ };
2087
+ }
2088
+ return input || {};
2089
+ }
2090
+ function normalizeRecordingAppId(appId) {
2091
+ const value = String(appId || "").trim();
2092
+ if (!value) return void 0;
2093
+ if (value === "com.example.app") return void 0;
2094
+ if (value.includes("# TODO")) return void 0;
2095
+ return value;
2096
+ }
2097
+ function buildTextQueryVariants(value) {
2098
+ const raw = String(value || "").trim().replace(/\s+/g, " ");
2099
+ if (!raw) return [];
2100
+ const variants = [];
2101
+ const seen = /* @__PURE__ */ new Set();
2102
+ const push = (candidate) => {
2103
+ const normalized = String(candidate || "").trim().replace(/\s+/g, " ");
2104
+ if (!normalized || seen.has(normalized)) return;
2105
+ seen.add(normalized);
2106
+ variants.push(normalized);
2107
+ };
2108
+ push(raw);
2109
+ const withoutContext = raw.replace(/\s+(?:in|inside|within|from|on)\s+.+$/i, "").trim();
2110
+ push(withoutContext);
2111
+ const withoutUiNoun = withoutContext.replace(/\s+(?:button|tab|icon|link|banner|card|item|field|input|modal|sheet|screen|section|row)$/i, "").trim();
2112
+ push(withoutUiNoun);
2113
+ const withoutCommonWords = withoutUiNoun.split(/\s+/).filter((part) => !["my", "the", "a", "an", "to", "for", "of"].includes(part.toLowerCase())).join(" ");
2114
+ push(withoutCommonWords);
2115
+ const firstSegment = withoutUiNoun.split(/\s*[\/>|-]\s*/)[0]?.trim();
2116
+ push(firstSegment);
2117
+ return variants;
2118
+ }
2119
+ function choosePrimaryTextQuery(value) {
2120
+ const variants = buildTextQueryVariants(value);
2121
+ if (variants.length === 0) return "";
2122
+ return variants[variants.length - 1] || variants[0] || "";
2123
+ }
2124
+ function resolveCaptureLogcat(executor, captureLogcat) {
2125
+ if (typeof captureLogcat === "boolean") return captureLogcat;
2126
+ return executor === "adb";
2127
+ }
2128
+ function normalizeRequestedNetworkProfile(input, context) {
2129
+ return buildAppLabNetworkProfile(input, context);
2130
+ }
2131
+ function getRequestedAppLabCaptureMode(profile) {
2132
+ if (!profile || !isObject2(profile.capture)) return "";
2133
+ const capture = profile.capture;
2134
+ const applabMode = typeof capture.applabMode === "string" ? capture.applabMode.trim().toLowerCase() : "";
2135
+ if (applabMode) return applabMode;
2136
+ return typeof capture.mode === "string" ? capture.mode.trim().toLowerCase() : "";
2137
+ }
2138
+ function extractNetworkStateFromConfig(config) {
2139
+ if (!isObject2(config?.network)) return null;
2140
+ return config.network;
2141
+ }
2142
+ function mergeNetworkState(primary, fallback) {
2143
+ if (!primary) return fallback;
2144
+ if (!fallback) return primary;
2145
+ return {
2146
+ ...fallback,
2147
+ ...primary,
2148
+ active_profile: primary.active_profile ?? fallback.active_profile ?? null,
2149
+ effective_profile: primary.effective_profile ?? fallback.effective_profile ?? null,
2150
+ configured_at: primary.configured_at ?? fallback.configured_at ?? null,
2151
+ managed_proxy: primary.managed_proxy ?? fallback.managed_proxy ?? null
2152
+ };
2153
+ }
2154
+ function translateMaestroActionsToESVP(actions, options = {}) {
2155
+ const translated = [];
2156
+ const skipped = [];
2157
+ const pushTranslated = (action, source, reason) => {
2158
+ if (action) {
2159
+ translated.push(action);
2160
+ return;
2161
+ }
2162
+ skipped.push({
2163
+ actionId: source.id,
2164
+ type: source.type,
2165
+ reason: reason || "unsupported"
2166
+ });
2167
+ };
2168
+ for (const action of actions || []) {
2169
+ switch (action.type) {
2170
+ case "launch": {
2171
+ const appId = String(action.appId || action.text || options.appId || "").trim();
2172
+ pushTranslated(
2173
+ appId ? {
2174
+ name: "launch",
2175
+ args: { appId },
2176
+ checkpointAfter: true,
2177
+ checkpointLabel: `launch:${appId}`
2178
+ } : null,
2179
+ action,
2180
+ "launch is missing an appId"
2181
+ );
2182
+ break;
2183
+ }
2184
+ case "tap": {
2185
+ if (typeof action.x === "number" && typeof action.y === "number") {
2186
+ pushTranslated(
2187
+ {
2188
+ name: "tap",
2189
+ args: {
2190
+ x: Math.round(action.x),
2191
+ y: Math.round(action.y)
2192
+ },
2193
+ checkpointAfter: true,
2194
+ checkpointLabel: action.id
2195
+ },
2196
+ action
2197
+ );
2198
+ } else if (typeof action.text === "string" && action.text.trim()) {
2199
+ const selector = choosePrimaryTextQuery(action.text);
2200
+ const selectorVariants = buildTextQueryVariants(action.text);
2201
+ pushTranslated(
2202
+ {
2203
+ name: "tap",
2204
+ args: {
2205
+ text: selector,
2206
+ selector,
2207
+ selectors: selectorVariants.map((candidate) => ({
2208
+ selector: candidate,
2209
+ label: candidate
2210
+ }))
2211
+ },
2212
+ checkpointAfter: true,
2213
+ checkpointLabel: action.id
2214
+ },
2215
+ action
2216
+ );
2217
+ } else {
2218
+ pushTranslated(null, action, "tap is missing coordinates or a selector that can be translated to ESVP");
2219
+ }
2220
+ break;
2221
+ }
2222
+ case "swipe":
2223
+ case "scroll": {
2224
+ if (typeof action.x === "number" && typeof action.y === "number" && typeof action.endX === "number" && typeof action.endY === "number") {
2225
+ pushTranslated(
2226
+ {
2227
+ name: "swipe",
2228
+ args: {
2229
+ x1: Math.round(action.x),
2230
+ y1: Math.round(action.y),
2231
+ x2: Math.round(action.endX),
2232
+ y2: Math.round(action.endY),
2233
+ durationMs: Math.max(120, Math.round(action.duration || 280))
2234
+ },
2235
+ checkpointAfter: true,
2236
+ checkpointLabel: action.id
2237
+ },
2238
+ action
2239
+ );
2240
+ } else if (action.direction) {
2241
+ pushTranslated(
2242
+ {
2243
+ name: "swipe",
2244
+ args: {
2245
+ direction: String(action.direction).toLowerCase()
2246
+ },
2247
+ checkpointAfter: true,
2248
+ checkpointLabel: action.id
2249
+ },
2250
+ action
2251
+ );
2252
+ } else {
2253
+ pushTranslated(null, action, "swipe/scroll is missing coordinates or direction");
2254
+ }
2255
+ break;
2256
+ }
2257
+ case "input": {
2258
+ const text = typeof action.text === "string" ? action.text : "";
2259
+ pushTranslated(
2260
+ text ? {
2261
+ name: "type",
2262
+ args: { text },
2263
+ checkpointAfter: true,
2264
+ checkpointLabel: action.id
2265
+ } : null,
2266
+ action,
2267
+ "input is missing text"
2268
+ );
2269
+ break;
2270
+ }
2271
+ case "back":
2272
+ pushTranslated(
2273
+ {
2274
+ name: "back",
2275
+ checkpointAfter: true,
2276
+ checkpointLabel: action.id
2277
+ },
2278
+ action
2279
+ );
2280
+ break;
2281
+ case "home":
2282
+ pushTranslated(
2283
+ {
2284
+ name: "home",
2285
+ checkpointAfter: true,
2286
+ checkpointLabel: action.id
2287
+ },
2288
+ action
2289
+ );
2290
+ break;
2291
+ case "pressKey": {
2292
+ const key = typeof action.text === "string" ? action.text : "";
2293
+ pushTranslated(
2294
+ key ? {
2295
+ name: "keyevent",
2296
+ args: { key },
2297
+ checkpointAfter: true,
2298
+ checkpointLabel: action.id
2299
+ } : null,
2300
+ action,
2301
+ "pressKey is missing a key value"
2302
+ );
2303
+ break;
2304
+ }
2305
+ case "wait": {
2306
+ const ms = typeof action.seconds === "number" ? Math.max(0, Math.round(action.seconds * 1e3)) : 1e3;
2307
+ pushTranslated(
2308
+ {
2309
+ name: "wait",
2310
+ args: { ms },
2311
+ checkpointAfter: true,
2312
+ checkpointLabel: action.id
2313
+ },
2314
+ action
2315
+ );
2316
+ break;
2317
+ }
2318
+ default:
2319
+ pushTranslated(null, action, `action type "${action.type}" is not supported by the ESVP adapter yet`);
2320
+ break;
2321
+ }
2322
+ }
2323
+ return {
2324
+ actions: translated,
2325
+ skipped
2326
+ };
2327
+ }
2328
+ async function normalizeESVPNetworkArtifacts(sessionId, artifacts, serverUrl) {
2329
+ const networkArtifacts = (artifacts || []).filter((artifact) => artifact?.kind === "network_trace" && typeof artifact?.path === "string");
2330
+ const traceKinds = Array.from(
2331
+ new Set(
2332
+ networkArtifacts.map((artifact) => artifact?.meta && typeof artifact.meta === "object" ? String(artifact.meta.trace_kind || "") : "").filter(Boolean)
2333
+ )
2334
+ );
2335
+ const entries = [];
2336
+ for (const artifact of networkArtifacts) {
2337
+ const artifactPath = String(artifact.path || "");
2338
+ if (!artifactPath) continue;
2339
+ try {
2340
+ const payload = await getESVPArtifactContent(sessionId, artifactPath, serverUrl);
2341
+ const meta = artifact.meta && typeof artifact.meta === "object" ? artifact.meta : {};
2342
+ const traceKind = typeof meta.trace_kind === "string" ? meta.trace_kind : null;
2343
+ entries.push(...normalizeNetworkPayloadToEntries(payload, {
2344
+ traceKind,
2345
+ artifactPath,
2346
+ artifactMeta: meta
2347
+ }));
2348
+ } catch {
2349
+ }
2350
+ }
2351
+ return {
2352
+ networkEntries: entries.sort((a, b) => (a.startedAt || 0) - (b.startedAt || 0)),
2353
+ networkCapture: createMobileNetworkCaptureMeta({
2354
+ source: "esvp-mobile",
2355
+ truncated: false,
2356
+ maxEntries: Math.max(entries.length, 1200),
2357
+ resourceTypes: traceKinds.length > 0 ? traceKinds : ["mobile-http-trace"]
2358
+ }),
2359
+ traceKinds
2360
+ };
2361
+ }
2362
+ function normalizeNetworkPayloadToEntries(payload, context) {
2363
+ const payloadRecord = parseEmbeddedObject(payload);
2364
+ const nestedPayloadRecord = payloadRecord ? parseEmbeddedObject(payloadRecord.payload) : null;
2365
+ const payloadLog = payloadRecord ? parseEmbeddedObject(payloadRecord.log) : null;
2366
+ const nestedPayloadLog = nestedPayloadRecord ? parseEmbeddedObject(nestedPayloadRecord.log) : null;
2367
+ if (context.traceKind === "har") {
2368
+ if (payloadLog && Array.isArray(payloadLog.entries)) {
2369
+ return normalizeHarEntries(payloadLog.entries, context);
2370
+ }
2371
+ if (nestedPayloadLog && Array.isArray(nestedPayloadLog.entries)) {
2372
+ return normalizeHarEntries(nestedPayloadLog.entries, context);
2373
+ }
2374
+ }
2375
+ const candidates = extractTraceCandidates(payload);
2376
+ const normalized = candidates.map((candidate, index) => normalizeHttpTraceCandidate(candidate, index, context)).filter((entry) => entry !== null);
2377
+ return normalized;
2378
+ }
2379
+ function normalizeHarEntries(entries, context) {
2380
+ const normalized = [];
2381
+ for (const [index, entry] of (entries || []).entries()) {
2382
+ const requestUrl = String(entry?.request?.url || "").trim();
2383
+ if (!requestUrl) continue;
2384
+ const parsed = parseRequestUrl(requestUrl);
2385
+ const startedAt = Date.parse(String(entry?.startedDateTime || ""));
2386
+ const durationMs = numberOrNull(entry?.time);
2387
+ const finishedAt = Number.isFinite(startedAt) && durationMs != null ? startedAt + durationMs : null;
2388
+ const responseSize = numberOrNull(entry?.response?.content?.size) ?? numberOrNull(entry?.response?.bodySize);
2389
+ const requestHeaders = toHeaderMap(entry?.request?.headers);
2390
+ const responseHeaders = toHeaderMap(entry?.response?.headers);
2391
+ normalized.push({
2392
+ id: `net_${sanitizeIdentifier(context.artifactPath)}_${String(index + 1).padStart(4, "0")}`,
2393
+ url: parsed.url,
2394
+ origin: parsed.origin,
2395
+ hostname: parsed.hostname,
2396
+ pathname: parsed.pathname,
2397
+ routeKey: parsed.routeKey,
2398
+ method: String(entry?.request?.method || "GET").toUpperCase(),
2399
+ resourceType: "har",
2400
+ startedAt: Number.isFinite(startedAt) ? startedAt : Date.now(),
2401
+ finishedAt,
2402
+ durationMs,
2403
+ status: numberOrNull(entry?.response?.status),
2404
+ ok: typeof entry?.response?.status === "number" ? entry.response.status < 400 : null,
2405
+ queryKeys: parsed.queryKeys,
2406
+ requestContentType: requestHeaders["content-type"] || null,
2407
+ responseContentType: responseHeaders["content-type"] || null,
2408
+ responseSize,
2409
+ failureText: typeof entry?.response?._error === "string" ? entry.response._error : null,
2410
+ requestHeaders,
2411
+ responseHeaders,
2412
+ requestBodyPreview: null,
2413
+ responseBodyPreview: null,
2414
+ requestBodyBytes: null,
2415
+ responseBodyBytes: responseSize
2416
+ });
2417
+ }
2418
+ return normalized;
2419
+ }
2420
+ function normalizeHttpTraceCandidate(candidate, index, context) {
2421
+ if (!isObject2(candidate)) return null;
2422
+ const request = isObject2(candidate.request) ? candidate.request : candidate;
2423
+ const response = isObject2(candidate.response) ? candidate.response : null;
2424
+ const url = firstString(
2425
+ candidate.url,
2426
+ request.url,
2427
+ context.artifactMeta.url
2428
+ );
2429
+ if (!url) return null;
2430
+ const parsed = parseRequestUrl(url);
2431
+ const method = firstString(candidate.method, request.method, context.artifactMeta.method) || "GET";
2432
+ const status = firstNumber(
2433
+ candidate.status,
2434
+ candidate.statusCode,
2435
+ response?.status,
2436
+ response?.statusCode,
2437
+ context.artifactMeta.status_code
2438
+ );
2439
+ const durationMs = firstNumber(
2440
+ candidate.durationMs,
2441
+ candidate.duration_ms,
2442
+ response?.durationMs,
2443
+ response?.duration_ms,
2444
+ response?.time_ms,
2445
+ candidate.timeMs
2446
+ );
2447
+ const startedAt = firstTimestamp(
2448
+ candidate.startedAt,
2449
+ candidate.started_at,
2450
+ candidate.timestamp,
2451
+ candidate.ts,
2452
+ request.startedAt,
2453
+ request.timestamp,
2454
+ request.ts
2455
+ ) ?? Date.now();
2456
+ const finishedAt = durationMs != null ? startedAt + durationMs : firstTimestamp(candidate.finishedAt, candidate.finished_at) ?? null;
2457
+ const requestHeaders = mergeHeaderObjects(
2458
+ candidate.requestHeaders,
2459
+ candidate.request_headers,
2460
+ request.headers
2461
+ );
2462
+ const responseHeaders = mergeHeaderObjects(
2463
+ candidate.responseHeaders,
2464
+ candidate.response_headers,
2465
+ response?.headers
2466
+ );
2467
+ const requestBodyPreview = firstString(
2468
+ candidate.requestBodyPreview,
2469
+ candidate.request_body_preview,
2470
+ request.bodyPreview,
2471
+ request.body_preview
2472
+ );
2473
+ const responseBodyPreview = firstString(
2474
+ candidate.responseBodyPreview,
2475
+ candidate.response_body_preview,
2476
+ response?.bodyPreview,
2477
+ response?.body_preview
2478
+ );
2479
+ const requestBodyBytes = firstNumber(
2480
+ candidate.requestBodyBytes,
2481
+ candidate.request_body_bytes,
2482
+ request.bodyBytes,
2483
+ request.body_bytes
2484
+ );
2485
+ const responseSize = firstNumber(
2486
+ candidate.responseBodyBytes,
2487
+ candidate.response_body_bytes,
2488
+ candidate.responseSize,
2489
+ candidate.response_size,
2490
+ response?.size,
2491
+ response?.bodyBytes,
2492
+ response?.body_bytes,
2493
+ response?.contentLength,
2494
+ responseHeaders["content-length"]
2495
+ );
2496
+ const failureText = firstString(
2497
+ candidate.failureText,
2498
+ candidate.error,
2499
+ response?.error,
2500
+ response?.failureText
2501
+ );
2502
+ return {
2503
+ id: `net_${sanitizeIdentifier(context.artifactPath)}_${String(index + 1).padStart(4, "0")}`,
2504
+ url: parsed.url,
2505
+ origin: parsed.origin,
2506
+ hostname: parsed.hostname,
2507
+ pathname: parsed.pathname,
2508
+ routeKey: parsed.routeKey,
2509
+ method: String(method).toUpperCase(),
2510
+ resourceType: firstString(candidate.resourceType, candidate.kind, context.traceKind) || "request",
2511
+ startedAt,
2512
+ finishedAt,
2513
+ durationMs,
2514
+ status,
2515
+ ok: status != null ? status < 400 : null,
2516
+ queryKeys: parsed.queryKeys,
2517
+ requestContentType: firstString(
2518
+ candidate.requestContentType,
2519
+ request.contentType,
2520
+ requestHeaders["content-type"]
2521
+ ),
2522
+ responseContentType: firstString(
2523
+ candidate.responseContentType,
2524
+ response?.contentType,
2525
+ responseHeaders["content-type"]
2526
+ ),
2527
+ responseSize,
2528
+ failureText,
2529
+ requestHeaders,
2530
+ responseHeaders,
2531
+ requestBodyPreview,
2532
+ responseBodyPreview,
2533
+ requestBodyBytes,
2534
+ responseBodyBytes: responseSize
2535
+ };
2536
+ }
2537
+ function extractTraceCandidates(payload) {
2538
+ if (Array.isArray(payload)) return payload;
2539
+ const payloadRecord = parseEmbeddedObject(payload);
2540
+ if (!payloadRecord) return [];
2541
+ const nestedCandidates = [
2542
+ payloadRecord,
2543
+ parseEmbeddedObject(payloadRecord.payload),
2544
+ parseEmbeddedObject(payloadRecord.data),
2545
+ parseEmbeddedObject(payloadRecord.trace),
2546
+ parseEmbeddedObject(payloadRecord.result)
2547
+ ].filter(Boolean);
2548
+ for (const candidate of nestedCandidates) {
2549
+ if (Array.isArray(candidate.entries)) return candidate.entries;
2550
+ if (Array.isArray(candidate.requests)) return candidate.requests;
2551
+ if (Array.isArray(candidate.events)) return candidate.events;
2552
+ if (Array.isArray(candidate.items)) return candidate.items;
2553
+ }
2554
+ return [payload];
2555
+ }
2556
+ function parseRequestUrl(rawUrl) {
2557
+ try {
2558
+ const parsed = new URL(rawUrl);
2559
+ const pathname = parsed.pathname || "/";
2560
+ const queryKeys = Array.from(new Set(Array.from(parsed.searchParams.keys()).filter(Boolean))).sort();
2561
+ return {
2562
+ url: buildDisplayUrl(parsed.origin, pathname),
2563
+ origin: parsed.origin,
2564
+ hostname: parsed.hostname,
2565
+ pathname,
2566
+ routeKey: `${parsed.hostname}${pathname}`,
2567
+ queryKeys
2568
+ };
2569
+ } catch {
2570
+ const [baseWithoutQuery] = String(rawUrl).split("?");
2571
+ const [baseWithoutFragment] = String(baseWithoutQuery || rawUrl).split("#");
2572
+ return {
2573
+ url: baseWithoutFragment || rawUrl,
2574
+ origin: "",
2575
+ hostname: "unknown host",
2576
+ pathname: "/",
2577
+ routeKey: rawUrl,
2578
+ queryKeys: []
2579
+ };
2580
+ }
2581
+ }
2582
+ function buildDisplayUrl(origin, pathname) {
2583
+ if (origin) return `${origin}${pathname || "/"}`;
2584
+ return pathname || "/";
2585
+ }
2586
+ function sanitizeIdentifier(value) {
2587
+ return String(value || "").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "trace";
2588
+ }
2589
+ function toHeaderMap(entries) {
2590
+ if (!Array.isArray(entries)) return {};
2591
+ return entries.reduce((acc, item) => {
2592
+ if (!isObject2(item)) return acc;
2593
+ const key = firstString(item.name, item.key);
2594
+ if (!key) return acc;
2595
+ const value = firstString(item.value) || "";
2596
+ acc[key.toLowerCase()] = value;
2597
+ return acc;
2598
+ }, {});
2599
+ }
2600
+ function headerObject(value) {
2601
+ if (!value) return {};
2602
+ if (Array.isArray(value)) return toHeaderMap(value);
2603
+ if (!isObject2(value)) return {};
2604
+ return Object.entries(value).reduce((acc, [key, entryValue]) => {
2605
+ acc[String(key).toLowerCase()] = String(entryValue);
2606
+ return acc;
2607
+ }, {});
2608
+ }
2609
+ function mergeHeaderObjects(...values) {
2610
+ return values.reduce((acc, value) => {
2611
+ const headers = headerObject(value);
2612
+ if (!headers || Object.keys(headers).length === 0) return acc;
2613
+ return {
2614
+ ...acc,
2615
+ ...headers
2616
+ };
2617
+ }, {});
2618
+ }
2619
+ function firstString(...values) {
2620
+ for (const value of values) {
2621
+ if (typeof value === "string" && value.trim()) return value.trim();
2622
+ }
2623
+ return null;
2624
+ }
2625
+ function firstNumber(...values) {
2626
+ for (const value of values) {
2627
+ const numeric = numberOrNull(value);
2628
+ if (numeric != null) return numeric;
2629
+ }
2630
+ return null;
2631
+ }
2632
+ function firstTimestamp(...values) {
2633
+ for (const value of values) {
2634
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2635
+ if (typeof value === "string" && value.trim()) {
2636
+ const direct = Number(value);
2637
+ if (Number.isFinite(direct)) return direct;
2638
+ const parsed = Date.parse(value);
2639
+ if (Number.isFinite(parsed)) return parsed;
2640
+ }
2641
+ }
2642
+ return null;
2643
+ }
2644
+ function numberOrNull(value) {
2645
+ if (value == null || value === "") return null;
2646
+ const numeric = Number(value);
2647
+ return Number.isFinite(numeric) ? numeric : null;
2648
+ }
2649
+ function parseEmbeddedObject(value) {
2650
+ if (isObject2(value)) return value;
2651
+ if (typeof value !== "string") return null;
2652
+ try {
2653
+ const parsed = JSON.parse(value);
2654
+ return isObject2(parsed) ? parsed : null;
2655
+ } catch {
2656
+ return null;
2657
+ }
2658
+ }
2659
+ function isObject2(value) {
2660
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2661
+ }
2662
+
2663
+ export {
2664
+ analyzeScreenshotsForActions,
2665
+ generateMaestroYaml,
2666
+ ensureLocalCaptureProxyProfile,
2667
+ listLocalCaptureProxyStates,
2668
+ finalizeAllLocalCaptureProxySessions,
2669
+ finalizeLocalCaptureProxySession,
2670
+ startLocalAppHttpTraceCollector,
2671
+ resolveLocalAppHttpTraceCollectorById,
2672
+ getLocalAppHttpTraceBootstrap,
2673
+ ingestLocalAppHttpTrace,
2674
+ finalizeLocalAppHttpTraceCollector,
2675
+ createMobileNetworkCaptureMeta,
2676
+ diagnoseESVPNetworkState,
2677
+ collectESVPSessionNetworkData,
2678
+ validateMaestroRecordingWithESVP,
2679
+ translateMaestroActionsToESVP
2680
+ };