@veolab/discoverylab 1.6.3 → 1.6.6

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 (29) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/assets/applab-bundle-icon.png +0 -0
  4. package/assets/icons/icons8-claude-150.png +0 -0
  5. package/assets/icons/icons8-claude-500.png +0 -0
  6. package/dist/{chunk-HFN6BTVO.js → chunk-JAA53ES7.js} +1 -1
  7. package/dist/{chunk-XKX6NBHF.js → chunk-TWRWARU4.js} +52 -2
  8. package/dist/{chunk-IVX2OSOJ.js → chunk-XDUFCPOC.js} +198 -69
  9. package/dist/{chunk-5AISGCS4.js → chunk-YFF3M76J.js} +162 -33
  10. package/dist/cli.js +29 -18
  11. package/dist/export/infographic-template.html +392 -128
  12. package/dist/index.d.ts +30 -6
  13. package/dist/index.html +75 -21
  14. package/dist/index.js +4 -4
  15. package/dist/{infographic-GQAHEOAA.js → infographic-OSDIJM5M.js} +119 -17
  16. package/dist/mcpb/node_modules/@anthropic-ai/sdk/src/lib/.keep +4 -0
  17. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/better_sqlite3.node.d +1 -0
  18. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/better_sqlite3/src/better_sqlite3.o.d +133 -0
  19. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/deps/locate_sqlite3.stamp.d +1 -0
  20. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/sqlite3/gen/sqlite3/sqlite3.o.d +4 -0
  21. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/test_extension/deps/test_extension.o.d +7 -0
  22. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/sqlite3.a.d +1 -0
  23. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/test_extension.node.d +1 -0
  24. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/ba23eeee118cd63e16015df367567cb043fed872.intermediate.d +1 -0
  25. package/dist/{server-W3JQ5RG7.js → server-GXNAKM4H.js} +1 -1
  26. package/dist/{server-OVOACIOJ.js → server-WN6DCCUA.js} +1 -1
  27. package/dist/{setup-F7MGEFIM.js → setup-SMN7FJNZ.js} +2 -2
  28. package/dist/{tools-VYFNRUS4.js → tools-6BTUMR3G.js} +3 -3
  29. package/package.json +8 -2
@@ -12,7 +12,7 @@
12
12
  "name": "discoverylab",
13
13
  "source": ".",
14
14
  "description": "AI-powered app testing & marketing asset generator. Record mobile/web apps, run automated tests with Maestro & Playwright, and generate professional screenshots, GIFs, and test reports.",
15
- "version": "1.6.3",
15
+ "version": "1.6.4",
16
16
  "author": {
17
17
  "name": "Anderson Melo"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "discoverylab",
3
3
  "description": "AI-powered app testing & marketing asset generator. Record mobile/web apps, run automated tests with Maestro & Playwright, and generate professional screenshots, GIFs, and test reports.",
4
- "version": "1.6.3",
4
+ "version": "1.6.4",
5
5
  "author": {
6
6
  "name": "Anderson Melo",
7
7
  "email": "anderson.90@gmail.com"
Binary file
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createJsonResult,
3
3
  createTextResult
4
- } from "./chunk-XKX6NBHF.js";
4
+ } from "./chunk-TWRWARU4.js";
5
5
  import {
6
6
  LOCAL_ESVP_SERVER_URL,
7
7
  getESVPHealth,
@@ -6,6 +6,7 @@ import {
6
6
  import { z } from "zod";
7
7
  var MCPServer = class {
8
8
  tools = /* @__PURE__ */ new Map();
9
+ resources = /* @__PURE__ */ new Map();
9
10
  serverInfo = {
10
11
  name: "discoverylab",
11
12
  version: APP_VERSION
@@ -18,6 +19,15 @@ var MCPServer = class {
18
19
  this.registerTool(tool);
19
20
  }
20
21
  }
22
+ registerResource(resource) {
23
+ this.resources.set(resource.uri, resource);
24
+ }
25
+ upsertResourceContents(uri, resource) {
26
+ this.resources.set(uri, {
27
+ uri,
28
+ ...resource
29
+ });
30
+ }
21
31
  async handleRequest(request) {
22
32
  const { id, method, params } = request;
23
33
  try {
@@ -28,6 +38,10 @@ var MCPServer = class {
28
38
  return this.handleToolsList(id);
29
39
  case "tools/call":
30
40
  return this.handleToolCall(id, params);
41
+ case "resources/list":
42
+ return this.handleResourcesList(id);
43
+ case "resources/read":
44
+ return this.handleResourcesRead(id, params);
31
45
  case "ping":
32
46
  return { jsonrpc: "2.0", id, result: { pong: true } };
33
47
  default:
@@ -54,7 +68,8 @@ var MCPServer = class {
54
68
  protocolVersion: "2024-11-05",
55
69
  serverInfo: this.serverInfo,
56
70
  capabilities: {
57
- tools: {}
71
+ tools: {},
72
+ resources: {}
58
73
  }
59
74
  }
60
75
  };
@@ -63,7 +78,8 @@ var MCPServer = class {
63
78
  const tools = Array.from(this.tools.values()).map((tool) => ({
64
79
  name: tool.name,
65
80
  description: tool.description,
66
- inputSchema: this.zodToJsonSchema(tool.inputSchema)
81
+ inputSchema: this.zodToJsonSchema(tool.inputSchema),
82
+ ...tool._meta ? { _meta: tool._meta } : {}
67
83
  }));
68
84
  return {
69
85
  jsonrpc: "2.0",
@@ -71,6 +87,40 @@ var MCPServer = class {
71
87
  result: { tools }
72
88
  };
73
89
  }
90
+ handleResourcesList(id) {
91
+ const resources = Array.from(this.resources.values()).map((resource) => ({
92
+ uri: resource.uri,
93
+ name: resource.name,
94
+ ...resource.title ? { title: resource.title } : {},
95
+ ...resource.description ? { description: resource.description } : {},
96
+ mimeType: resource.mimeType
97
+ }));
98
+ return {
99
+ jsonrpc: "2.0",
100
+ id,
101
+ result: { resources }
102
+ };
103
+ }
104
+ handleResourcesRead(id, params) {
105
+ const uri = typeof params?.uri === "string" ? params.uri : "";
106
+ const resource = this.resources.get(uri);
107
+ if (!resource) {
108
+ return {
109
+ jsonrpc: "2.0",
110
+ id,
111
+ error: { code: -32602, message: `Resource not found: ${uri}` }
112
+ };
113
+ }
114
+ const contents = Array.isArray(resource.contents) && resource.contents.length > 0 ? resource.contents : [{
115
+ uri: resource.uri,
116
+ mimeType: resource.mimeType
117
+ }];
118
+ return {
119
+ jsonrpc: "2.0",
120
+ id,
121
+ result: { contents }
122
+ };
123
+ }
74
124
  async handleToolCall(id, params) {
75
125
  const { name, arguments: args } = params;
76
126
  const tool = this.tools.get(name);
@@ -67,6 +67,7 @@ import {
67
67
  import {
68
68
  DATA_DIR,
69
69
  EXPORTS_DIR,
70
+ FRAMES_DIR,
70
71
  PROJECTS_DIR,
71
72
  frames,
72
73
  getDatabase,
@@ -3707,7 +3708,7 @@ app.post("/api/upload", async (c) => {
3707
3708
  const { exec: exec2 } = await import("child_process");
3708
3709
  const { promisify } = await import("util");
3709
3710
  const execAsync = promisify(exec2);
3710
- const { PROJECTS_DIR: PROJECTS_DIR2, FRAMES_DIR } = await import("./db-5ECN3O7F.js");
3711
+ const { PROJECTS_DIR: PROJECTS_DIR2, FRAMES_DIR: FRAMES_DIR2 } = await import("./db-5ECN3O7F.js");
3711
3712
  const id = crypto.randomUUID();
3712
3713
  const now = /* @__PURE__ */ new Date();
3713
3714
  const projectDir = join6(PROJECTS_DIR2, id);
@@ -3736,7 +3737,7 @@ app.post("/api/upload", async (c) => {
3736
3737
  let framePaths = [];
3737
3738
  try {
3738
3739
  if (isVideo) {
3739
- const projectFramesDir = join6(FRAMES_DIR, id);
3740
+ const projectFramesDir = join6(FRAMES_DIR2, id);
3740
3741
  if (!fsExists(projectFramesDir)) {
3741
3742
  mkdirSync5(projectFramesDir, { recursive: true });
3742
3743
  }
@@ -4503,8 +4504,8 @@ app.post("/api/analyze/:id", async (c) => {
4503
4504
  const { promisify } = await import("util");
4504
4505
  const { mkdirSync: mkdirSync5, readdirSync: readdirSync5 } = await import("fs");
4505
4506
  const execAsync = promisify(exec2);
4506
- const { FRAMES_DIR } = await import("./db-5ECN3O7F.js");
4507
- const projectFramesDir = join6(FRAMES_DIR, id);
4507
+ const { FRAMES_DIR: FRAMES_DIR2 } = await import("./db-5ECN3O7F.js");
4508
+ const projectFramesDir = join6(FRAMES_DIR2, id);
4508
4509
  if (!existsSync5(projectFramesDir)) {
4509
4510
  mkdirSync5(projectFramesDir, { recursive: true });
4510
4511
  }
@@ -5007,11 +5008,11 @@ app.get("/api/grid/project-frames/:id", async (c) => {
5007
5008
  return c.json({ error: "Project not found" }, 404);
5008
5009
  }
5009
5010
  const project = result[0];
5010
- const { FRAMES_DIR } = await import("./db-5ECN3O7F.js");
5011
+ const { FRAMES_DIR: FRAMES_DIR2 } = await import("./db-5ECN3O7F.js");
5011
5012
  const { readdirSync: readdirSync5 } = await import("fs");
5012
5013
  const availableFrames = [];
5013
5014
  const { isBlankFrame } = await import("./frames-2NFCSKXQ.js");
5014
- const projectFramesDir = join6(FRAMES_DIR, id);
5015
+ const projectFramesDir = join6(FRAMES_DIR2, id);
5015
5016
  if (existsSync5(projectFramesDir)) {
5016
5017
  const frameFiles = readdirSync5(projectFramesDir).filter((f) => /\.(png|jpg|jpeg)$/i.test(f)).sort();
5017
5018
  for (const f of frameFiles) {
@@ -5138,18 +5139,43 @@ function resolveProjectBundleRecordingDir(originalVideoPath, resolvedVideoPath)
5138
5139
  }
5139
5140
  return null;
5140
5141
  }
5141
- function copyProjectExportArtifacts(sourceDir, destinationDir) {
5142
- if (!existsSync5(sourceDir) || !statSync3(sourceDir).isDirectory()) {
5143
- return 0;
5142
+ function collectProjectExportFramePaths(projectId, recordingBaseDir) {
5143
+ const candidateDirs = [
5144
+ join6(FRAMES_DIR, projectId),
5145
+ join6(PROJECTS_DIR, projectId, "frames"),
5146
+ recordingBaseDir ? join6(recordingBaseDir, "screenshots") : null,
5147
+ recordingBaseDir
5148
+ ].filter((value, index, items) => !!value && items.indexOf(value) === index);
5149
+ const framePaths = [];
5150
+ const seen = /* @__PURE__ */ new Set();
5151
+ for (const dirPath of candidateDirs) {
5152
+ if (!existsSync5(dirPath) || !statSync3(dirPath).isDirectory()) {
5153
+ continue;
5154
+ }
5155
+ const files = readdirSync4(dirPath).filter((entry) => /\.(png|jpg|jpeg|webp)$/i.test(entry)).filter((entry) => !entry.startsWith("._")).sort();
5156
+ for (const entry of files) {
5157
+ const absolutePath = join6(dirPath, entry);
5158
+ if (seen.has(absolutePath)) {
5159
+ continue;
5160
+ }
5161
+ seen.add(absolutePath);
5162
+ framePaths.push(absolutePath);
5163
+ }
5144
5164
  }
5145
- let copiedCount = 0;
5146
- for (const entry of readdirSync4(sourceDir)) {
5147
- if (/\.(applab|esvp)$/i.test(entry)) continue;
5148
- if (copyPathIntoExportBundle(join6(sourceDir, entry), join6(destinationDir, entry))) {
5149
- copiedCount += 1;
5165
+ return framePaths;
5166
+ }
5167
+ function resolveAppLabBundleIconPath() {
5168
+ const candidates = [
5169
+ join6(__dirname, "..", "..", "assets", "applab-bundle-icon.png"),
5170
+ join6(__dirname, "..", "assets", "applab-bundle-icon.png"),
5171
+ join6(process.cwd(), "assets", "applab-bundle-icon.png")
5172
+ ];
5173
+ for (const candidate of candidates) {
5174
+ if (existsSync5(candidate)) {
5175
+ return candidate;
5150
5176
  }
5151
5177
  }
5152
- return copiedCount;
5178
+ return null;
5153
5179
  }
5154
5180
  async function runExportCommand(command, args, cwd) {
5155
5181
  await new Promise((resolve, reject) => {
@@ -5209,6 +5235,56 @@ function normalizePersistedLocalESVPServerUrl(serverUrl, connectionMode) {
5209
5235
  function resolvePersistedLocalESVPServerUrl(serverUrl, esvp) {
5210
5236
  return normalizePersistedLocalESVPServerUrl(serverUrl, "local") || resolveProjectESVPServerUrl(esvp) || LOCAL_ESVP_SERVER_URL;
5211
5237
  }
5238
+ function cloneJsonRecord(value) {
5239
+ if (!value) return value;
5240
+ try {
5241
+ return JSON.parse(JSON.stringify(value));
5242
+ } catch {
5243
+ return value;
5244
+ }
5245
+ }
5246
+ function detachPortableESVPForExport(esvp, snapshotAttached = false) {
5247
+ if (!esvp) {
5248
+ return { value: null, detached: false };
5249
+ }
5250
+ const cloned = cloneJsonRecord(esvp) || {};
5251
+ const connectionMode = typeof cloned.connectionMode === "string" ? cloned.connectionMode.trim().toLowerCase() : "";
5252
+ const serverUrl = typeof cloned.serverUrl === "string" ? cloned.serverUrl.trim() : "";
5253
+ const shouldDetach = connectionMode === "local" || !serverUrl;
5254
+ if (!shouldDetach) {
5255
+ return { value: cloned, detached: false };
5256
+ }
5257
+ cloned.currentSessionId = null;
5258
+ cloned.serverUrl = null;
5259
+ cloned.detachedForExport = true;
5260
+ cloned.snapshotAttached = snapshotAttached;
5261
+ if (cloned.network && typeof cloned.network === "object") {
5262
+ const network = cloned.network;
5263
+ network.sourceSessionId = null;
5264
+ network.activeCaptureSessionId = null;
5265
+ network.detachedForExport = true;
5266
+ }
5267
+ if (cloned.validation && typeof cloned.validation === "object") {
5268
+ const validation = cloned.validation;
5269
+ validation.sourceSessionId = null;
5270
+ validation.replaySessionId = null;
5271
+ validation.detachedForExport = true;
5272
+ }
5273
+ return { value: cloned, detached: true };
5274
+ }
5275
+ function detachPortableSessionDataForExport(sessionData, detachedEsvp, detached = false) {
5276
+ if (!sessionData) return null;
5277
+ const cloned = cloneJsonRecord(sessionData) || {};
5278
+ if (detached) {
5279
+ cloned.esvp = detachedEsvp;
5280
+ if (cloned.networkCapture && typeof cloned.networkCapture === "object") {
5281
+ const networkCapture = cloned.networkCapture;
5282
+ networkCapture.sessionId = null;
5283
+ networkCapture.detachedForExport = true;
5284
+ }
5285
+ }
5286
+ return cloned;
5287
+ }
5212
5288
  function isESVPReplayValidationSupported(result) {
5213
5289
  if (!result) return true;
5214
5290
  if (result.supported === false) return false;
@@ -5282,7 +5358,6 @@ app.post("/api/export", async (c) => {
5282
5358
  const projectFrames = await db.select().from(frames).where(eq(frames.projectId, projectId)).orderBy(frames.frameNumber);
5283
5359
  const exportRecords = await db.select().from(projectExports).where(eq(projectExports.projectId, projectId)).orderBy(desc(projectExports.createdAt));
5284
5360
  const recordingBaseDir = resolveProjectBundleRecordingDir(rawProject.videoPath, resolvedVideoPath);
5285
- const exportArtifactsDir = join6(EXPORTS_DIR, projectId);
5286
5361
  const sessionPath = recordingBaseDir ? join6(recordingBaseDir, "session.json") : null;
5287
5362
  let sessionData = null;
5288
5363
  let networkEntries = [];
@@ -5307,8 +5382,7 @@ app.post("/api/export", async (c) => {
5307
5382
  const summaryPath = rawProject.aiSummary ? "analysis/app-intelligence.md" : null;
5308
5383
  const ocrPath = rawProject.ocrText ? "analysis/ocr.txt" : null;
5309
5384
  const thumbnailName = rawProject.thumbnailPath ? basename3(rawProject.thumbnailPath) : null;
5310
- const resolvedMediaName = resolvedVideoPath ? basename3(resolvedVideoPath) : null;
5311
- const bundledFrames = projectFrames.map((frame) => {
5385
+ let bundledFrames = projectFrames.map((frame) => {
5312
5386
  const extensionMatch = basename3(frame.imagePath).match(/(\.[^.]+)$/);
5313
5387
  const extension = extensionMatch ? extensionMatch[1] : ".png";
5314
5388
  const relativeImagePath = `frames/frame-${String(frame.frameNumber).padStart(4, "0")}${extension}`;
@@ -5318,19 +5392,39 @@ app.post("/api/export", async (c) => {
5318
5392
  imagePath: relativeImagePath
5319
5393
  };
5320
5394
  });
5321
- const mediaFiles = [];
5322
- if (resolvedVideoPath && existsSync5(resolvedVideoPath) && !statSync3(resolvedVideoPath).isDirectory()) {
5323
- const relativePath = `media/${resolvedMediaName}`;
5324
- copyPathIntoExportBundle(resolvedVideoPath, join6(bundleRoot, relativePath));
5325
- mediaFiles.push({ role: "primary-media", path: relativePath });
5395
+ if (bundledFrames.length === 0) {
5396
+ const fallbackFramePaths = collectProjectExportFramePaths(projectId, recordingBaseDir);
5397
+ bundledFrames = fallbackFramePaths.map((framePath, index) => {
5398
+ const extensionMatch = basename3(framePath).match(/(\.[^.]+)$/);
5399
+ const extension = extensionMatch ? extensionMatch[1] : ".png";
5400
+ const relativeImagePath = `frames/frame-${String(index + 1).padStart(4, "0")}${extension}`;
5401
+ copyPathIntoExportBundle(framePath, join6(bundleRoot, relativeImagePath));
5402
+ return {
5403
+ id: `${projectId}-fallback-frame-${index + 1}`,
5404
+ projectId,
5405
+ frameNumber: index + 1,
5406
+ imagePath: relativeImagePath,
5407
+ ocrText: null,
5408
+ timestamp,
5409
+ createdAt: new Date(timestamp),
5410
+ isKeyFrame: null
5411
+ };
5412
+ });
5326
5413
  }
5414
+ const mediaFiles = [];
5327
5415
  if (rawProject.thumbnailPath && existsSync5(rawProject.thumbnailPath) && thumbnailName) {
5328
5416
  const relativePath = `media/${thumbnailName}`;
5329
5417
  copyPathIntoExportBundle(rawProject.thumbnailPath, join6(bundleRoot, relativePath));
5330
5418
  mediaFiles.push({ role: "thumbnail", path: relativePath });
5331
5419
  }
5332
- const recordingIncluded = recordingBaseDir ? copyPathIntoExportBundle(recordingBaseDir, join6(bundleRoot, "recording")) : false;
5333
- const exportArtifactCount = copyProjectExportArtifacts(exportArtifactsDir, join6(bundleRoot, "exports"));
5420
+ const canonicalBundleIconPath = resolveAppLabBundleIconPath();
5421
+ const bundleIconRelativePath = canonicalBundleIconPath ? "media/icon.png" : null;
5422
+ if (canonicalBundleIconPath && bundleIconRelativePath) {
5423
+ copyPathIntoExportBundle(canonicalBundleIconPath, join6(bundleRoot, bundleIconRelativePath));
5424
+ mediaFiles.push({ role: "icon", path: bundleIconRelativePath });
5425
+ }
5426
+ const recordingIncluded = false;
5427
+ const exportArtifactCount = 0;
5334
5428
  const esvpSessionId = resolveProjectESVPSessionId(esvp);
5335
5429
  const esvpServerUrl = resolveProjectESVPServerUrl(esvp);
5336
5430
  let esvpSnapshot = null;
@@ -5350,11 +5444,23 @@ app.post("/api/export", async (c) => {
5350
5444
  };
5351
5445
  }
5352
5446
  }
5447
+ const detachedEsvpResult = detachPortableESVPForExport(esvp, !!esvpSnapshot);
5448
+ const exportedESVP = detachedEsvpResult.value;
5449
+ const exportedSessionData = detachPortableSessionDataForExport(
5450
+ sessionData,
5451
+ exportedESVP,
5452
+ detachedEsvpResult.detached
5453
+ );
5454
+ const exportedESVPSessionId = detachedEsvpResult.detached ? null : esvpSessionId;
5353
5455
  const packagedProject = {
5354
5456
  ...normalizedProject,
5355
- videoPath: resolvedMediaName ? `media/${resolvedMediaName}` : normalizedProject.videoPath,
5457
+ videoPath: null,
5356
5458
  thumbnailPath: thumbnailName ? `media/${thumbnailName}` : normalizedProject.thumbnailPath,
5357
- frames: bundledFrames
5459
+ frames: bundledFrames,
5460
+ icon: bundleIconRelativePath ? {
5461
+ path: bundleIconRelativePath,
5462
+ kind: "app-icon"
5463
+ } : null
5358
5464
  };
5359
5465
  writeExportJson(join6(bundleRoot, "manifest.json"), {
5360
5466
  bundleVersion: 1,
@@ -5368,6 +5474,10 @@ app.post("/api/export", async (c) => {
5368
5474
  id: rawProject.id,
5369
5475
  name: rawProject.name,
5370
5476
  platform: rawProject.platform || null,
5477
+ icon: bundleIconRelativePath ? {
5478
+ path: bundleIconRelativePath,
5479
+ kind: "app-icon"
5480
+ } : null,
5371
5481
  frameCount: bundledFrames.length,
5372
5482
  hasRecordingFolder: recordingIncluded,
5373
5483
  hasNetworkTrace: networkEntries.length > 0 || !!networkCapture,
@@ -5381,13 +5491,13 @@ app.post("/api/export", async (c) => {
5381
5491
  recordingFolder: recordingIncluded,
5382
5492
  networkEntries: networkEntries.length,
5383
5493
  exportArtifacts: exportArtifactCount,
5384
- esvpSessionId: esvpSessionId || null
5494
+ esvpSessionId: exportedESVPSessionId || null
5385
5495
  }
5386
5496
  });
5387
5497
  writeExportJson(join6(bundleRoot, "metadata", "project.json"), packagedProject);
5388
5498
  writeExportJson(join6(bundleRoot, "metadata", "exports.json"), exportRecords);
5389
- if (sessionData) {
5390
- writeExportJson(join6(bundleRoot, "metadata", "session.json"), sessionData);
5499
+ if (exportedSessionData) {
5500
+ writeExportJson(join6(bundleRoot, "metadata", "session.json"), exportedSessionData);
5391
5501
  }
5392
5502
  if (rawProject.taskHubLinks) {
5393
5503
  writeExportJson(join6(bundleRoot, "taskhub", "links.json"), normalizedProject.taskHubLinks);
@@ -5413,24 +5523,12 @@ app.post("/api/export", async (c) => {
5413
5523
  if (networkCapture) {
5414
5524
  writeExportJson(join6(bundleRoot, "network", "capture.json"), networkCapture);
5415
5525
  }
5416
- if (esvp) {
5417
- writeExportJson(join6(bundleRoot, "network", "esvp.json"), esvp);
5526
+ if (exportedESVP) {
5527
+ writeExportJson(join6(bundleRoot, "network", "esvp.json"), exportedESVP);
5418
5528
  }
5419
5529
  if (esvpSnapshot) {
5420
5530
  writeExportJson(join6(bundleRoot, "esvp", "snapshot.json"), esvpSnapshot);
5421
5531
  }
5422
- const projectExportsDir = join6(EXPORTS_DIR, projectId);
5423
- if (existsSync5(projectExportsDir) && statSync3(projectExportsDir).isDirectory()) {
5424
- const rendersDir = join6(bundleRoot, "renders");
5425
- mkdirSync4(rendersDir, { recursive: true });
5426
- const exportFiles = readdirSync4(projectExportsDir);
5427
- for (const f of exportFiles) {
5428
- const src = join6(projectExportsDir, f);
5429
- if (statSync3(src).isFile()) {
5430
- cpSync(src, join6(rendersDir, f));
5431
- }
5432
- }
5433
- }
5434
5532
  const templateContentPath = join6(PROJECTS_DIR, projectId, "template-content.json");
5435
5533
  if (existsSync5(templateContentPath)) {
5436
5534
  mkdirSync4(join6(bundleRoot, "templates"), { recursive: true });
@@ -5443,12 +5541,16 @@ app.post("/api/export", async (c) => {
5443
5541
  "This package bundles the local project context for sharing or re-analysis.",
5444
5542
  "",
5445
5543
  "Included when available:",
5446
- "- original media and thumbnail",
5447
- "- recording folder with session data, screenshots, and test script",
5544
+ "- selected thumbnail and analyzed frames",
5545
+ "- lightweight project/session metadata",
5448
5546
  "- OCR text and app intelligence summary",
5449
5547
  "- network trace, capture metadata, and ESVP snapshot",
5450
- "- previously generated export assets such as grids and renders",
5451
- "- Task Hub links, requirements, and test map"
5548
+ "- Task Hub links, requirements, and test map",
5549
+ "",
5550
+ "Excluded by default to keep the bundle Claude-friendly:",
5551
+ "- original long-form media",
5552
+ "- recording folder",
5553
+ "- generated export assets and renders"
5452
5554
  ].join("\n"));
5453
5555
  outputPath = join6(exportDir, `export-${timestamp}.${format}`);
5454
5556
  mimeType = "application/zip";
@@ -5609,8 +5711,8 @@ app.get("/api/visualization/:projectId/:templateId", async (c) => {
5609
5711
  const db = getDatabase();
5610
5712
  const [project] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
5611
5713
  if (!project) return c.json({ error: "Project not found" }, 404);
5612
- const { FRAMES_DIR } = await import("./db-5ECN3O7F.js");
5613
- const projectFramesDir = join6(FRAMES_DIR, projectId);
5714
+ const { FRAMES_DIR: FRAMES_DIR2 } = await import("./db-5ECN3O7F.js");
5715
+ const projectFramesDir = join6(FRAMES_DIR2, projectId);
5614
5716
  const dbFrames = await db.select().from(frames).where(eq(frames.projectId, projectId)).orderBy(frames.frameNumber).limit(10);
5615
5717
  let frameImages = [];
5616
5718
  if (dbFrames.length > 0) {
@@ -5940,27 +6042,54 @@ app.post("/api/export/infographic", async (c) => {
5940
6042
  const [project] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
5941
6043
  if (!project) return c.json({ error: "Project not found" }, 404);
5942
6044
  const { FRAMES_DIR: fDir, EXPORTS_DIR: eDir, PROJECTS_DIR: pDir } = await import("./db-5ECN3O7F.js");
5943
- const { collectFrameImages, buildInfographicData, generateInfographicHtml } = await import("./infographic-GQAHEOAA.js");
6045
+ const { buildInfographicData, generateInfographicHtml, resolveInfographicFrameInputs } = await import("./infographic-OSDIJM5M.js");
5944
6046
  const dbFrames = await db.select().from(frames).where(eq(frames.projectId, projectId)).orderBy(frames.frameNumber).limit(20);
5945
- let frameFiles;
5946
- let frameOcr;
5947
- if (dbFrames.length > 0) {
5948
- frameFiles = dbFrames.map((f) => f.imagePath);
5949
- frameOcr = dbFrames;
5950
- } else {
5951
- frameFiles = collectFrameImages(join6(fDir, projectId), project.videoPath, pDir, projectId);
5952
- frameOcr = frameFiles.map(() => ({ ocrText: null }));
5953
- }
5954
- if (frameFiles.length === 0) {
5955
- return c.json({ error: "No frames found. Run analyzer first." }, 400);
6047
+ const resolvedFrames = resolveInfographicFrameInputs(
6048
+ dbFrames,
6049
+ join6(fDir, projectId),
6050
+ project.videoPath,
6051
+ pDir,
6052
+ projectId
6053
+ );
6054
+ if (resolvedFrames.frameFiles.length === 0) {
6055
+ return c.json({
6056
+ error: resolvedFrames.candidateCount > 0 ? "No readable frames found for infographic export." : "No frames found. Run analyzer first.",
6057
+ debug: {
6058
+ frameCandidates: resolvedFrames.candidateCount,
6059
+ validFrames: 0,
6060
+ source: resolvedFrames.source,
6061
+ invalidFrames: resolvedFrames.invalidFrames.slice(0, 5)
6062
+ }
6063
+ }, 400);
5956
6064
  }
5957
6065
  const cached = annotationCache.get(projectId);
5958
6066
  const annotations = cached?.steps?.map((s) => ({ label: s }));
5959
- const data = buildInfographicData(project, frameFiles, frameOcr, annotations);
6067
+ const data = buildInfographicData(project, resolvedFrames.frameFiles, resolvedFrames.frameOcr, annotations);
6068
+ if (data.frames.length === 0) {
6069
+ return c.json({
6070
+ error: "Infographic export produced no embeddable frames.",
6071
+ debug: {
6072
+ frameCandidates: resolvedFrames.candidateCount,
6073
+ validFrames: resolvedFrames.frameFiles.length,
6074
+ source: resolvedFrames.source,
6075
+ invalidFrames: resolvedFrames.invalidFrames.slice(0, 5)
6076
+ }
6077
+ }, 400);
6078
+ }
5960
6079
  const slug = (project.marketingTitle || project.name || projectId).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
5961
6080
  const outputPath = join6(eDir, `${slug}-infographic.html`);
5962
6081
  const result = generateInfographicHtml(data, outputPath);
5963
- if (!result.success) return c.json({ error: result.error }, 500);
6082
+ if (!result.success) {
6083
+ return c.json({
6084
+ error: result.error,
6085
+ debug: {
6086
+ frameCandidates: resolvedFrames.candidateCount,
6087
+ validFrames: resolvedFrames.frameFiles.length,
6088
+ source: resolvedFrames.source,
6089
+ invalidFrames: resolvedFrames.invalidFrames.slice(0, 5)
6090
+ }
6091
+ }, 500);
6092
+ }
5964
6093
  if (open) {
5965
6094
  const { exec: exec2 } = await import("child_process");
5966
6095
  exec2(`open "${result.outputPath}"`);
@@ -6006,9 +6135,9 @@ app.get("/api/export/document/:projectId", async (c) => {
6006
6135
  const projectFrames = await db.select().from(frames).where(eq(frames.projectId, projectId)).orderBy(frames.frameNumber).limit(20);
6007
6136
  let frameData = projectFrames.map((f) => ({ imagePath: f.imagePath, ocrText: f.ocrText }));
6008
6137
  if (frameData.length === 0 && project.videoPath) {
6009
- const { FRAMES_DIR } = await import("./db-5ECN3O7F.js");
6138
+ const { FRAMES_DIR: FRAMES_DIR2 } = await import("./db-5ECN3O7F.js");
6010
6139
  const dirs = [
6011
- join6(FRAMES_DIR, projectId),
6140
+ join6(FRAMES_DIR2, projectId),
6012
6141
  join6(project.videoPath, "screenshots"),
6013
6142
  join6(PROJECTS_DIR, "maestro-recordings", projectId, "screenshots"),
6014
6143
  join6(PROJECTS_DIR, "web-recordings", projectId, "screenshots")
@@ -6085,7 +6214,7 @@ app.post("/api/export/batch", async (c) => {
6085
6214
  if (!manifest.destination?.type) {
6086
6215
  return c.json({ error: "Destination type required" }, 400);
6087
6216
  }
6088
- const { FRAMES_DIR } = await import("./db-5ECN3O7F.js");
6217
+ const { FRAMES_DIR: FRAMES_DIR2 } = await import("./db-5ECN3O7F.js");
6089
6218
  const dataProvider = {
6090
6219
  async getProject(projectId) {
6091
6220
  const db2 = getDatabase();
@@ -6093,7 +6222,7 @@ app.post("/api/export/batch", async (c) => {
6093
6222
  return p || null;
6094
6223
  },
6095
6224
  getFramesDir(projectId) {
6096
- return join6(FRAMES_DIR, projectId);
6225
+ return join6(FRAMES_DIR2, projectId);
6097
6226
  }
6098
6227
  };
6099
6228
  const result = await executeBatchExport(manifest, dataProvider, (progress) => {
@@ -10112,7 +10241,7 @@ app.get("/api/mobile-chat/providers", async (c) => {
10112
10241
  });
10113
10242
  app.get("/api/setup/status", async (c) => {
10114
10243
  try {
10115
- const { setupStatusTool } = await import("./setup-F7MGEFIM.js");
10244
+ const { setupStatusTool } = await import("./setup-SMN7FJNZ.js");
10116
10245
  const result = await setupStatusTool.handler({});
10117
10246
  const data = JSON.parse(result.content[0].text);
10118
10247
  const idbInstalled = await isIdbInstalled().catch(() => false);