@veolab/discoverylab 1.4.4 → 1.6.3

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.
@@ -0,0 +1,183 @@
1
+ import "./chunk-R5U7XKVJ.js";
2
+
3
+ // src/core/export/infographic.ts
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
5
+ import { join, dirname, extname } from "path";
6
+ import { fileURLToPath } from "url";
7
+ function findTemplate() {
8
+ const __dir = dirname(fileURLToPath(import.meta.url));
9
+ const possiblePaths = [
10
+ join(__dir, "infographic-template.html"),
11
+ join(__dir, "..", "export", "infographic-template.html"),
12
+ join(process.cwd(), "dist", "export", "infographic-template.html"),
13
+ join(process.cwd(), "src", "core", "export", "infographic-template.html")
14
+ ];
15
+ for (const p of possiblePaths) {
16
+ if (existsSync(p)) return p;
17
+ }
18
+ return null;
19
+ }
20
+ function imageToBase64(imagePath) {
21
+ if (!existsSync(imagePath)) return "";
22
+ const buffer = readFileSync(imagePath);
23
+ const ext = extname(imagePath).toLowerCase();
24
+ const mimeTypes = {
25
+ ".png": "image/png",
26
+ ".jpg": "image/jpeg",
27
+ ".jpeg": "image/jpeg",
28
+ ".webp": "image/webp",
29
+ ".gif": "image/gif"
30
+ };
31
+ const mime = mimeTypes[ext] || "image/png";
32
+ return `data:${mime};base64,${buffer.toString("base64")}`;
33
+ }
34
+ function stripMarkdown(text) {
35
+ return text.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/`(.+?)`/g, "$1").replace(/^#{1,3}\s+/gm, "").replace(/^-\s+/gm, "").replace(/^\d+\.\s+/gm, "").replace(/\[(.+?)\]\(.*?\)/g, "$1").trim();
36
+ }
37
+ function collectFrameImages(framesDir, videoPath, projectsDir, projectId) {
38
+ const imageExts = /\.(png|jpg|jpeg|webp|gif)$/i;
39
+ const dirs = [
40
+ framesDir,
41
+ ...videoPath ? [join(videoPath, "screenshots"), videoPath] : [],
42
+ ...projectsDir && projectId ? [
43
+ join(projectsDir, "maestro-recordings", projectId, "screenshots"),
44
+ join(projectsDir, "web-recordings", projectId, "screenshots")
45
+ ] : []
46
+ ];
47
+ for (const dir of dirs) {
48
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) continue;
49
+ const files = readdirSync(dir).filter((f) => imageExts.test(f)).sort().map((f) => join(dir, f));
50
+ if (files.length > 0) return files;
51
+ }
52
+ return [];
53
+ }
54
+ function buildInfographicData(project, frameFiles, frameOcr, annotations) {
55
+ let flowSteps = [];
56
+ let uiElements = [];
57
+ if (project.aiSummary) {
58
+ const flowMatch = project.aiSummary.match(/## (?:User Flow|Likely User Flow)\n([\s\S]*?)(?=\n##|\n$|$)/);
59
+ if (flowMatch) {
60
+ flowSteps = (flowMatch[1].match(/^\d+\.\s+(.+)$/gm) || []).map((s) => s.replace(/^\d+\.\s+/, ""));
61
+ }
62
+ const uiMatch = project.aiSummary.match(/## (?:UI Elements Found|Key UI Elements)\n([\s\S]*?)(?=\n##|\n$|$)/);
63
+ if (uiMatch) {
64
+ uiElements = (uiMatch[1].match(/^-\s+(.+)$/gm) || []).map((s) => s.replace(/^-\s+/, ""));
65
+ }
66
+ }
67
+ const elementsPerFrame = Math.max(1, Math.ceil(uiElements.length / Math.max(frameFiles.length, 1)));
68
+ const hotspotColors = ["#818CF8", "#34D399", "#F59E0B", "#EC4899", "#06B6D4", "#8B5CF6"];
69
+ const hotspotPositions = [
70
+ { x: 50, y: 8 },
71
+ // top center (nav bar)
72
+ { x: 15, y: 30 },
73
+ // left middle
74
+ { x: 85, y: 30 },
75
+ // right middle
76
+ { x: 50, y: 50 },
77
+ // center
78
+ { x: 50, y: 85 },
79
+ // bottom center (tab bar)
80
+ { x: 15, y: 70 }
81
+ // bottom left
82
+ ];
83
+ const frames = frameFiles.map((filePath, i) => {
84
+ const rawStepName = annotations?.[i]?.label || flowSteps[i] || `Step ${i + 1}`;
85
+ const stepName = stripMarkdown(rawStepName);
86
+ const ocr = frameOcr[i]?.ocrText || "";
87
+ const rawDesc = flowSteps[i] || ocr.slice(0, 100) || `Screen ${i + 1}`;
88
+ const description = stripMarkdown(rawDesc);
89
+ const hotspots = [];
90
+ const startIdx = i * elementsPerFrame;
91
+ const frameElements = uiElements.slice(startIdx, startIdx + elementsPerFrame).slice(0, 4);
92
+ frameElements.forEach((el, j) => {
93
+ const cleanEl = stripMarkdown(el);
94
+ const pos = hotspotPositions[j % hotspotPositions.length];
95
+ hotspots.push({
96
+ id: String.fromCharCode(65 + j),
97
+ x_percent: pos.x,
98
+ y_percent: pos.y,
99
+ label: cleanEl.slice(0, 20),
100
+ title: cleanEl.slice(0, 40),
101
+ description: cleanEl,
102
+ color: hotspotColors[j % hotspotColors.length]
103
+ });
104
+ });
105
+ if (hotspots.length === 0 && ocr) {
106
+ const ocrWords = ocr.split(/\s+/).filter((w) => w.length > 3).slice(0, 3);
107
+ ocrWords.forEach((w, j) => {
108
+ const pos = hotspotPositions[j % hotspotPositions.length];
109
+ hotspots.push({
110
+ id: String.fromCharCode(65 + j),
111
+ x_percent: pos.x,
112
+ y_percent: pos.y,
113
+ label: w.slice(0, 15),
114
+ title: w,
115
+ description: `Text found: "${w}"`,
116
+ color: hotspotColors[j % hotspotColors.length]
117
+ });
118
+ });
119
+ }
120
+ return {
121
+ id: `frame-${i}`,
122
+ step_name: stepName,
123
+ description,
124
+ base64: imageToBase64(filePath),
125
+ baseline_status: "not_validated",
126
+ hotspots
127
+ };
128
+ });
129
+ return {
130
+ name: project.marketingTitle || project.name,
131
+ platform: project.platform || "unknown",
132
+ recorded_at: project.createdAt instanceof Date ? project.createdAt.toISOString() : typeof project.createdAt === "string" ? project.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
133
+ frames
134
+ };
135
+ }
136
+ function generateInfographicHtmlString(data) {
137
+ const templatePath = findTemplate();
138
+ if (!templatePath) return null;
139
+ let html = readFileSync(templatePath, "utf-8");
140
+ html = html.replace("__TITLE__", data.name);
141
+ const dataJson = JSON.stringify(data);
142
+ html = html.replace(
143
+ "window.FLOW_DATA || { name: 'Flow', frames: [] }",
144
+ `window.FLOW_DATA || ${dataJson}`
145
+ );
146
+ return html;
147
+ }
148
+ function generateInfographicHtml(data, outputPath) {
149
+ try {
150
+ const templatePath = findTemplate();
151
+ if (!templatePath) {
152
+ return { success: false, error: "Infographic template not found" };
153
+ }
154
+ let html = readFileSync(templatePath, "utf-8");
155
+ html = html.replace("__TITLE__", data.name);
156
+ const dataJson = JSON.stringify(data);
157
+ html = html.replace(
158
+ "window.FLOW_DATA || { name: 'Flow', frames: [] }",
159
+ `window.FLOW_DATA || ${dataJson}`
160
+ );
161
+ const outDir = dirname(outputPath);
162
+ if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
163
+ writeFileSync(outputPath, html, "utf-8");
164
+ const stat = statSync(outputPath);
165
+ return {
166
+ success: true,
167
+ outputPath,
168
+ size: stat.size,
169
+ frameCount: data.frames.length
170
+ };
171
+ } catch (error) {
172
+ return {
173
+ success: false,
174
+ error: error instanceof Error ? error.message : String(error)
175
+ };
176
+ }
177
+ }
178
+ export {
179
+ buildInfographicData,
180
+ collectFrameImages,
181
+ generateInfographicHtml,
182
+ generateInfographicHtmlString
183
+ };
@@ -3,7 +3,7 @@ import {
3
3
  getServerPort,
4
4
  startServer,
5
5
  stopServer
6
- } from "./chunk-2UUMLAVR.js";
6
+ } from "./chunk-IVX2OSOJ.js";
7
7
  import "./chunk-34GGYFXX.js";
8
8
  import "./chunk-6GK5K6CS.js";
9
9
  import "./chunk-7R5YNOXE.js";
@@ -2,10 +2,12 @@ import {
2
2
  setupCheckTool,
3
3
  setupInitTool,
4
4
  setupInstallTool,
5
+ setupReplayStatusTool,
5
6
  setupStatusTool,
6
7
  setupTools
7
- } from "./chunk-CUBQRT5L.js";
8
+ } from "./chunk-HFN6BTVO.js";
8
9
  import "./chunk-XKX6NBHF.js";
10
+ import "./chunk-GRU332L4.js";
9
11
  import "./chunk-6EGBXRDK.js";
10
12
  import "./chunk-YYOK2RF7.js";
11
13
  import "./chunk-R5U7XKVJ.js";
@@ -13,6 +15,7 @@ export {
13
15
  setupCheckTool,
14
16
  setupInitTool,
15
17
  setupInstallTool,
18
+ setupReplayStatusTool,
16
19
  setupStatusTool,
17
20
  setupTools
18
21
  };
@@ -107,16 +107,17 @@ import {
107
107
  uiStatusTool,
108
108
  uiTools,
109
109
  videoInfoTool
110
- } from "./chunk-HB3YPWF3.js";
110
+ } from "./chunk-5AISGCS4.js";
111
+ import "./chunk-34GGYFXX.js";
111
112
  import "./chunk-PMCXEA7J.js";
112
113
  import {
113
114
  setupCheckTool,
114
115
  setupInitTool,
116
+ setupReplayStatusTool,
115
117
  setupStatusTool,
116
118
  setupTools
117
- } from "./chunk-CUBQRT5L.js";
119
+ } from "./chunk-HFN6BTVO.js";
118
120
  import "./chunk-XKX6NBHF.js";
119
- import "./chunk-34GGYFXX.js";
120
121
  import "./chunk-7R5YNOXE.js";
121
122
  import "./chunk-3ERJNXYM.js";
122
123
  import "./chunk-LB3RNE3O.js";
@@ -217,6 +218,7 @@ export {
217
218
  projectTools,
218
219
  setupCheckTool,
219
220
  setupInitTool,
221
+ setupReplayStatusTool,
220
222
  setupStatusTool,
221
223
  setupTools,
222
224
  startRecordingTool,
@@ -0,0 +1,116 @@
1
+ # ESVP Protocol Integration
2
+
3
+ DiscoveryLab includes a built-in client for the open [ESVP protocol](https://esvp.dev) by Entropy Lab — enabling reproducible mobile sessions, automated replay, and network-aware validation.
4
+
5
+ ## Configuration
6
+
7
+ | Mode | How |
8
+ |------|-----|
9
+ | **Remote** | Set `ESVP_BASE_URL=http://your-esvp-host:8787` |
10
+ | **Local** | Leave `ESVP_BASE_URL` unset — DiscoveryLab boots an embedded local runtime |
11
+ | **Dev (custom module)** | Set `DISCOVERYLAB_ESVP_LOCAL_MODULE=/path/to/esvp-server-reference/server.js` |
12
+
13
+ ## MCP Tools
14
+
15
+ | Tool | Description |
16
+ |------|-------------|
17
+ | `dlab.esvp.status` | Check control-plane health |
18
+ | `dlab.esvp.devices` | List available devices |
19
+ | `dlab.esvp.sessions.list` | List sessions |
20
+ | `dlab.esvp.session.create` | Create session (ios-sim, maestro-ios, adb) |
21
+ | `dlab.esvp.session.get` | Get session details |
22
+ | `dlab.esvp.session.inspect` | Inspect session state |
23
+ | `dlab.esvp.session.transcript` | Get session transcript |
24
+ | `dlab.esvp.session.artifacts.list` | List artifacts |
25
+ | `dlab.esvp.session.artifact.get` | Get specific artifact |
26
+ | `dlab.esvp.session.actions` | Run actions on session |
27
+ | `dlab.esvp.session.checkpoint` | Create checkpoint |
28
+ | `dlab.esvp.session.finish` | Finish session |
29
+ | `dlab.esvp.replay.run` | Replay recording |
30
+ | `dlab.esvp.replay.validate` | Validate replay |
31
+ | `dlab.esvp.session.network` | Get network data |
32
+ | `dlab.esvp.network.configure` | Configure network proxy |
33
+ | `dlab.esvp.network.trace.attach` | Attach network trace |
34
+ | `dlab.project.esvp.current` | Get current project ESVP state |
35
+ | `dlab.project.esvp.validate` | Validate project recording |
36
+ | `dlab.project.esvp.replay` | Replay project recording |
37
+ | `dlab.project.esvp.sync_network` | Sync network traces |
38
+ | `dlab.project.esvp.app_trace_bootstrap` | Bootstrap app tracing |
39
+
40
+ ## CLI Commands
41
+
42
+ ```bash
43
+ discoverylab esvp status
44
+ discoverylab esvp devices
45
+ discoverylab esvp sessions
46
+ discoverylab esvp create
47
+ discoverylab esvp get <sessionId>
48
+ discoverylab esvp inspect <sessionId>
49
+ discoverylab esvp transcript <sessionId>
50
+ discoverylab esvp artifacts <sessionId>
51
+ discoverylab esvp artifact <sessionId> <artifactPath>
52
+ discoverylab esvp actions <sessionId>
53
+ discoverylab esvp checkpoint <sessionId>
54
+ discoverylab esvp finish <sessionId>
55
+ discoverylab esvp replay-run <sessionId>
56
+ discoverylab esvp replay-validate <sessionId>
57
+ discoverylab esvp replay-consistency <sessionId>
58
+ discoverylab esvp network <sessionId>
59
+ discoverylab esvp network-configure <sessionId>
60
+ discoverylab esvp network-clear <sessionId>
61
+ discoverylab esvp trace-attach <sessionId>
62
+ ```
63
+
64
+ ## Network Proxy
65
+
66
+ DiscoveryLab defaults to `external-proxy` mode — the proxy runs locally and ESVP only persists traces.
67
+
68
+ ### Host Rules
69
+
70
+ | Setup | Proxy Address |
71
+ |-------|--------------|
72
+ | macOS + iOS Simulator | `127.0.0.1` |
73
+ | macOS/Linux + Android Emulator | `10.0.2.2` |
74
+ | Physical Android | Host LAN IP |
75
+
76
+ ### Environment Variables
77
+
78
+ | Variable | Purpose |
79
+ |----------|---------|
80
+ | `DISCOVERYLAB_NETWORK_PROXY_PORT` | Proxy port |
81
+ | `DISCOVERYLAB_NETWORK_PROXY_HOST` | Proxy host for device |
82
+ | `DISCOVERYLAB_NETWORK_PROXY_BIND_HOST` | Bind address (LAN) |
83
+ | `DISCOVERYLAB_NETWORK_PROXY_PROTOCOL` | Protocol (http/https) |
84
+ | `DISCOVERYLAB_NETWORK_PROXY_BYPASS` | Bypass patterns |
85
+ | `DISCOVERYLAB_NETWORK_PROXY_MAX_DURATION_MS` | Auto-finalize timeout (default: 15min) |
86
+
87
+ ### Safety
88
+
89
+ - Proxies auto-finalize after 15 minutes to prevent stale proxy state
90
+ - Settings UI has an emergency lock and "Disable Active Proxy Now" button
91
+ - Server shutdown auto-cleans active proxies
92
+
93
+ ## Example Prompts
94
+
95
+ ```
96
+ Check my ESVP control-plane health
97
+ Create an ios-sim ESVP session and take a screenshot
98
+ Replay this iOS recording with an external proxy
99
+ Configure a proxy on this session and attach the HTTP trace
100
+ ```
101
+
102
+ ## Programmatic Usage
103
+
104
+ ```ts
105
+ import { createESVPSession, runESVPActions } from '@veolab/discoverylab';
106
+
107
+ const created = await createESVPSession({
108
+ executor: 'ios-sim',
109
+ meta: { source: 'demo' },
110
+ });
111
+
112
+ await runESVPActions(created.session.id, {
113
+ actions: [{ name: 'screenshot' }],
114
+ finish: true,
115
+ });
116
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veolab/discoverylab",
3
- "version": "1.4.4",
3
+ "version": "1.6.3",
4
4
  "description": "AI-powered app testing & evidence generator - Claude Code Plugin",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "dev": "tsx watch src/cli.ts serve",
13
- "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean && cp src/web/index.html dist/index.html && mkdir -p dist/visualizations && cp src/core/visualizations/templates/*.html dist/visualizations/ && node scripts/build-host-runtime.mjs --best-effort",
13
+ "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean && cp src/web/index.html dist/index.html && mkdir -p dist/visualizations dist/export && cp src/core/visualizations/templates/*.html dist/visualizations/ && cp src/core/export/infographic-template.html dist/export/ && node scripts/build-host-runtime.mjs --best-effort",
14
14
  "build:host-runtime": "node scripts/build-host-runtime.mjs",
15
15
  "pack:local": "npm run build && npm run build:host-runtime && node scripts/verify-host-runtime-bundle.mjs && npm pack",
16
16
  "prepack": "npm run build && npm run build:host-runtime && node scripts/verify-host-runtime-bundle.mjs",
@@ -68,7 +68,7 @@
68
68
  "claude-plugin": {
69
69
  "name": "DiscoveryLab",
70
70
  "description": "AI-powered app testing & evidence generator",
71
- "version": "1.4.4",
71
+ "version": "1.6.3",
72
72
  "tools": [
73
73
  "dlab.capture.screen",
74
74
  "dlab.capture.emulator",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: knowledge-brain
3
- description: Search app knowledge from captured flows - UI, screens, behaviors
3
+ description: Search and visualize app knowledge from captured flows
4
4
  context: fork
5
5
  agent: general-purpose
6
6
  always: true
@@ -11,71 +11,72 @@ tags:
11
11
  - app-flow
12
12
  - ui
13
13
  - screens
14
+ - brain
14
15
  ---
15
16
 
16
17
  # DiscoveryLab Knowledge Brain
17
18
 
18
- DiscoveryLab captures app recordings and analyzes them with OCR + AI, building a knowledge base of every flow, screen, and UI element. This skill lets you query that knowledge.
19
+ DiscoveryLab captures app recordings and analyzes them with AI, building a knowledge base of every flow, screen, and UI element. This skill lets you query and visualize that knowledge.
19
20
 
20
- ## When to Use
21
+ ## Default behavior: Visual first
21
22
 
22
- Use this whenever the user asks about:
23
- - How a specific screen or flow works in their app
24
- - What UI elements exist on a page (buttons, inputs, labels)
25
- - The user journey through a feature (onboarding, checkout, settings)
26
- - Comparing different captures of the same flow
27
- - Any reference to app screens, recordings, or captured content
28
- - Context about what was tested or recorded
29
-
30
- Also use proactively when:
31
- - The user is working on code related to a feature that was captured
32
- - You need visual context about the app to give better answers
33
- - The user mentions a Jira ticket that might be linked to a project
23
+ When the user asks about an app flow, **show it visually by default** using `dlab.knowledge.open`. The returned HTML renders as an interactive canvas with animated frame player, annotations, and navigation. Only fall back to text if the user explicitly asks for text or if no frames are available.
34
24
 
35
25
  ## Tools
36
26
 
37
- ### `dlab.knowledge.search`
38
- Semantic search across all captured projects. Matches against: project names, AI analysis summaries, OCR text from every screen, tags, and linked tickets.
27
+ ### `dlab.knowledge.open` (primary - visual)
28
+ Opens an interactive infographic of an app flow. Returns self-contained HTML.
39
29
 
40
30
  ```
41
- dlab.knowledge.search { query: "login" }
42
- dlab.knowledge.search { query: "paywall" }
43
- dlab.knowledge.search { query: "onboarding flow" }
31
+ dlab.knowledge.open { query: "login" }
32
+ dlab.knowledge.open { query: "onboarding flow" }
33
+ dlab.knowledge.open { projectId: "abc123" }
34
+ ```
35
+
36
+ Use when:
37
+ - User asks "how does the login work?"
38
+ - User asks "show me the onboarding"
39
+ - User says "what does the settings screen look like?"
40
+ - Any question about a captured flow where visual context helps
41
+
42
+ ### `dlab.knowledge.search` (text answers)
43
+ Search across all projects. Returns text with overview, flow steps, UI elements.
44
+
45
+ ```
46
+ dlab.knowledge.search { query: "checkout" }
44
47
  dlab.knowledge.search { query: "PROJ-123" }
45
- dlab.knowledge.search { query: "settings profile" }
46
48
  ```
47
49
 
48
- Returns per match:
49
- - App overview (what the screen/flow does)
50
- - User flow steps (numbered sequence)
51
- - UI elements found (buttons, inputs, navigation)
52
- - OCR text sample (actual text visible on screens)
53
- - Project ID for deeper lookup
50
+ Use when:
51
+ - User asks a specific factual question ("what buttons are on the paywall?")
52
+ - User references a Jira ticket
53
+ - You need quick context without opening a visual
54
54
 
55
- ### `dlab.knowledge.summary`
56
- High-level overview of the entire knowledge base - all projects grouped by app with stats.
55
+ ### `dlab.knowledge.summary` (overview)
56
+ Lists all captured knowledge grouped by app.
57
57
 
58
58
  ```
59
59
  dlab.knowledge.summary {}
60
60
  ```
61
61
 
62
- Use this when:
63
- - The user asks "what do we have captured?"
64
- - You need to orient yourself on what apps/flows exist
65
- - Starting a new conversation and need context
62
+ Use when:
63
+ - User asks "what do we have captured?"
64
+ - Starting a new conversation
65
+ - Need to orient on available projects
66
66
 
67
- ### `dlab.project.get`
68
- Full details on a specific project found via search. Use when the search result summary is not enough.
67
+ ### `dlab.project.import` (sharing)
68
+ Import a shared .applab project file.
69
69
 
70
70
  ```
71
- dlab.project.get { projectId: "<id from search results>" }
71
+ dlab.project.import { filePath: "/path/to/project.applab" }
72
72
  ```
73
73
 
74
- ## How to Respond
74
+ Use when someone shares a .applab file.
75
+
76
+ ## How to respond
75
77
 
76
- 1. **Search first** - never say "I don't have information about that" without searching
77
- 2. **No match?** - run `dlab.knowledge.summary` to show the user what's available
78
- 3. **Multiple results** - present the most recent first, note if there are different versions
79
- 4. **Cite the source** - mention which project/recording the information comes from
80
- 5. **Suggest captures** - if the user asks about a flow that doesn't exist, suggest they capture it with DiscoveryLab
81
- 6. **Be specific** - use the OCR text and UI elements from results to give precise answers, not generic ones
78
+ 1. **Visual first** - use `dlab.knowledge.open` by default for flow questions
79
+ 2. **Search first** - never say "I don't know" without searching
80
+ 3. **No match?** - run `dlab.knowledge.summary` to show what's available
81
+ 4. **Cite the source** - mention which project the information comes from
82
+ 5. **Suggest captures** - if a flow doesn't exist, suggest capturing it