@veolab/discoverylab 1.4.4 → 1.6.5

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 (33) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +70 -211
  4. package/assets/applab-bundle-icon.png +0 -0
  5. package/assets/icons/icons8-claude-150.png +0 -0
  6. package/assets/icons/icons8-claude-500.png +0 -0
  7. package/dist/{chunk-CUBQRT5L.js → chunk-JAA53ES7.js} +111 -2
  8. package/dist/{chunk-HB3YPWF3.js → chunk-Q7Q3A2ZI.js} +301 -10
  9. package/dist/{chunk-XKX6NBHF.js → chunk-TWRWARU4.js} +52 -2
  10. package/dist/{chunk-2UUMLAVR.js → chunk-V6RREMYD.js} +332 -38
  11. package/dist/cli.js +164 -28
  12. package/dist/export/infographic-template.html +254 -0
  13. package/dist/import-W2JEW254.js +180 -0
  14. package/dist/index.d.ts +30 -6
  15. package/dist/index.html +473 -11
  16. package/dist/index.js +5 -5
  17. package/dist/infographic-GQAHEOAA.js +183 -0
  18. package/dist/mcpb/node_modules/@anthropic-ai/sdk/src/lib/.keep +4 -0
  19. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/better_sqlite3.node.d +1 -0
  20. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/better_sqlite3/src/better_sqlite3.o.d +133 -0
  21. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/deps/locate_sqlite3.stamp.d +1 -0
  22. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/sqlite3/gen/sqlite3/sqlite3.o.d +4 -0
  23. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/obj.target/test_extension/deps/test_extension.o.d +7 -0
  24. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/sqlite3.a.d +1 -0
  25. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/Release/test_extension.node.d +1 -0
  26. package/dist/mcpb/node_modules/better-sqlite3/build/Release/.deps/ba23eeee118cd63e16015df367567cb043fed872.intermediate.d +1 -0
  27. package/dist/{server-QFNKZCOJ.js → server-C2NZM2RV.js} +1 -1
  28. package/dist/{server-OVOACIOJ.js → server-WN6DCCUA.js} +1 -1
  29. package/dist/{setup-6JJYKKBS.js → setup-SMN7FJNZ.js} +5 -2
  30. package/dist/{tools-Q7OZO732.js → tools-VXU3JEQP.js} +6 -4
  31. package/doc/esvp-protocol.md +116 -0
  32. package/package.json +9 -3
  33. package/skills/knowledge-brain/SKILL.md +44 -43
@@ -3,6 +3,7 @@ import {
3
3
  createNotionPage
4
4
  } from "./chunk-34GGYFXX.js";
5
5
  import {
6
+ getCachedRender,
6
7
  getRenderJob,
7
8
  startRender
8
9
  } from "./chunk-6GK5K6CS.js";
@@ -66,6 +67,7 @@ import {
66
67
  import {
67
68
  DATA_DIR,
68
69
  EXPORTS_DIR,
70
+ FRAMES_DIR,
69
71
  PROJECTS_DIR,
70
72
  frames,
71
73
  getDatabase,
@@ -3706,7 +3708,7 @@ app.post("/api/upload", async (c) => {
3706
3708
  const { exec: exec2 } = await import("child_process");
3707
3709
  const { promisify } = await import("util");
3708
3710
  const execAsync = promisify(exec2);
3709
- 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");
3710
3712
  const id = crypto.randomUUID();
3711
3713
  const now = /* @__PURE__ */ new Date();
3712
3714
  const projectDir = join6(PROJECTS_DIR2, id);
@@ -3735,7 +3737,7 @@ app.post("/api/upload", async (c) => {
3735
3737
  let framePaths = [];
3736
3738
  try {
3737
3739
  if (isVideo) {
3738
- const projectFramesDir = join6(FRAMES_DIR, id);
3740
+ const projectFramesDir = join6(FRAMES_DIR2, id);
3739
3741
  if (!fsExists(projectFramesDir)) {
3740
3742
  mkdirSync5(projectFramesDir, { recursive: true });
3741
3743
  }
@@ -4502,8 +4504,8 @@ app.post("/api/analyze/:id", async (c) => {
4502
4504
  const { promisify } = await import("util");
4503
4505
  const { mkdirSync: mkdirSync5, readdirSync: readdirSync5 } = await import("fs");
4504
4506
  const execAsync = promisify(exec2);
4505
- const { FRAMES_DIR } = await import("./db-5ECN3O7F.js");
4506
- const projectFramesDir = join6(FRAMES_DIR, id);
4507
+ const { FRAMES_DIR: FRAMES_DIR2 } = await import("./db-5ECN3O7F.js");
4508
+ const projectFramesDir = join6(FRAMES_DIR2, id);
4507
4509
  if (!existsSync5(projectFramesDir)) {
4508
4510
  mkdirSync5(projectFramesDir, { recursive: true });
4509
4511
  }
@@ -5006,11 +5008,11 @@ app.get("/api/grid/project-frames/:id", async (c) => {
5006
5008
  return c.json({ error: "Project not found" }, 404);
5007
5009
  }
5008
5010
  const project = result[0];
5009
- const { FRAMES_DIR } = await import("./db-5ECN3O7F.js");
5011
+ const { FRAMES_DIR: FRAMES_DIR2 } = await import("./db-5ECN3O7F.js");
5010
5012
  const { readdirSync: readdirSync5 } = await import("fs");
5011
5013
  const availableFrames = [];
5012
5014
  const { isBlankFrame } = await import("./frames-2NFCSKXQ.js");
5013
- const projectFramesDir = join6(FRAMES_DIR, id);
5015
+ const projectFramesDir = join6(FRAMES_DIR2, id);
5014
5016
  if (existsSync5(projectFramesDir)) {
5015
5017
  const frameFiles = readdirSync5(projectFramesDir).filter((f) => /\.(png|jpg|jpeg)$/i.test(f)).sort();
5016
5018
  for (const f of frameFiles) {
@@ -5137,18 +5139,43 @@ function resolveProjectBundleRecordingDir(originalVideoPath, resolvedVideoPath)
5137
5139
  }
5138
5140
  return null;
5139
5141
  }
5140
- function copyProjectExportArtifacts(sourceDir, destinationDir) {
5141
- if (!existsSync5(sourceDir) || !statSync3(sourceDir).isDirectory()) {
5142
- 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
+ }
5143
5164
  }
5144
- let copiedCount = 0;
5145
- for (const entry of readdirSync4(sourceDir)) {
5146
- if (/\.(applab|esvp)$/i.test(entry)) continue;
5147
- if (copyPathIntoExportBundle(join6(sourceDir, entry), join6(destinationDir, entry))) {
5148
- 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;
5149
5176
  }
5150
5177
  }
5151
- return copiedCount;
5178
+ return null;
5152
5179
  }
5153
5180
  async function runExportCommand(command, args, cwd) {
5154
5181
  await new Promise((resolve, reject) => {
@@ -5281,7 +5308,6 @@ app.post("/api/export", async (c) => {
5281
5308
  const projectFrames = await db.select().from(frames).where(eq(frames.projectId, projectId)).orderBy(frames.frameNumber);
5282
5309
  const exportRecords = await db.select().from(projectExports).where(eq(projectExports.projectId, projectId)).orderBy(desc(projectExports.createdAt));
5283
5310
  const recordingBaseDir = resolveProjectBundleRecordingDir(rawProject.videoPath, resolvedVideoPath);
5284
- const exportArtifactsDir = join6(EXPORTS_DIR, projectId);
5285
5311
  const sessionPath = recordingBaseDir ? join6(recordingBaseDir, "session.json") : null;
5286
5312
  let sessionData = null;
5287
5313
  let networkEntries = [];
@@ -5306,8 +5332,7 @@ app.post("/api/export", async (c) => {
5306
5332
  const summaryPath = rawProject.aiSummary ? "analysis/app-intelligence.md" : null;
5307
5333
  const ocrPath = rawProject.ocrText ? "analysis/ocr.txt" : null;
5308
5334
  const thumbnailName = rawProject.thumbnailPath ? basename3(rawProject.thumbnailPath) : null;
5309
- const resolvedMediaName = resolvedVideoPath ? basename3(resolvedVideoPath) : null;
5310
- const bundledFrames = projectFrames.map((frame) => {
5335
+ let bundledFrames = projectFrames.map((frame) => {
5311
5336
  const extensionMatch = basename3(frame.imagePath).match(/(\.[^.]+)$/);
5312
5337
  const extension = extensionMatch ? extensionMatch[1] : ".png";
5313
5338
  const relativeImagePath = `frames/frame-${String(frame.frameNumber).padStart(4, "0")}${extension}`;
@@ -5317,19 +5342,39 @@ app.post("/api/export", async (c) => {
5317
5342
  imagePath: relativeImagePath
5318
5343
  };
5319
5344
  });
5320
- const mediaFiles = [];
5321
- if (resolvedVideoPath && existsSync5(resolvedVideoPath) && !statSync3(resolvedVideoPath).isDirectory()) {
5322
- const relativePath = `media/${resolvedMediaName}`;
5323
- copyPathIntoExportBundle(resolvedVideoPath, join6(bundleRoot, relativePath));
5324
- mediaFiles.push({ role: "primary-media", path: relativePath });
5345
+ if (bundledFrames.length === 0) {
5346
+ const fallbackFramePaths = collectProjectExportFramePaths(projectId, recordingBaseDir);
5347
+ bundledFrames = fallbackFramePaths.map((framePath, index) => {
5348
+ const extensionMatch = basename3(framePath).match(/(\.[^.]+)$/);
5349
+ const extension = extensionMatch ? extensionMatch[1] : ".png";
5350
+ const relativeImagePath = `frames/frame-${String(index + 1).padStart(4, "0")}${extension}`;
5351
+ copyPathIntoExportBundle(framePath, join6(bundleRoot, relativeImagePath));
5352
+ return {
5353
+ id: `${projectId}-fallback-frame-${index + 1}`,
5354
+ projectId,
5355
+ frameNumber: index + 1,
5356
+ imagePath: relativeImagePath,
5357
+ ocrText: null,
5358
+ timestamp,
5359
+ createdAt: new Date(timestamp),
5360
+ isKeyFrame: null
5361
+ };
5362
+ });
5325
5363
  }
5364
+ const mediaFiles = [];
5326
5365
  if (rawProject.thumbnailPath && existsSync5(rawProject.thumbnailPath) && thumbnailName) {
5327
5366
  const relativePath = `media/${thumbnailName}`;
5328
5367
  copyPathIntoExportBundle(rawProject.thumbnailPath, join6(bundleRoot, relativePath));
5329
5368
  mediaFiles.push({ role: "thumbnail", path: relativePath });
5330
5369
  }
5331
- const recordingIncluded = recordingBaseDir ? copyPathIntoExportBundle(recordingBaseDir, join6(bundleRoot, "recording")) : false;
5332
- const exportArtifactCount = copyProjectExportArtifacts(exportArtifactsDir, join6(bundleRoot, "exports"));
5370
+ const canonicalBundleIconPath = resolveAppLabBundleIconPath();
5371
+ const bundleIconRelativePath = canonicalBundleIconPath ? "media/icon.png" : null;
5372
+ if (canonicalBundleIconPath && bundleIconRelativePath) {
5373
+ copyPathIntoExportBundle(canonicalBundleIconPath, join6(bundleRoot, bundleIconRelativePath));
5374
+ mediaFiles.push({ role: "icon", path: bundleIconRelativePath });
5375
+ }
5376
+ const recordingIncluded = false;
5377
+ const exportArtifactCount = 0;
5333
5378
  const esvpSessionId = resolveProjectESVPSessionId(esvp);
5334
5379
  const esvpServerUrl = resolveProjectESVPServerUrl(esvp);
5335
5380
  let esvpSnapshot = null;
@@ -5351,9 +5396,13 @@ app.post("/api/export", async (c) => {
5351
5396
  }
5352
5397
  const packagedProject = {
5353
5398
  ...normalizedProject,
5354
- videoPath: resolvedMediaName ? `media/${resolvedMediaName}` : normalizedProject.videoPath,
5399
+ videoPath: null,
5355
5400
  thumbnailPath: thumbnailName ? `media/${thumbnailName}` : normalizedProject.thumbnailPath,
5356
- frames: bundledFrames
5401
+ frames: bundledFrames,
5402
+ icon: bundleIconRelativePath ? {
5403
+ path: bundleIconRelativePath,
5404
+ kind: "app-icon"
5405
+ } : null
5357
5406
  };
5358
5407
  writeExportJson(join6(bundleRoot, "manifest.json"), {
5359
5408
  bundleVersion: 1,
@@ -5367,6 +5416,10 @@ app.post("/api/export", async (c) => {
5367
5416
  id: rawProject.id,
5368
5417
  name: rawProject.name,
5369
5418
  platform: rawProject.platform || null,
5419
+ icon: bundleIconRelativePath ? {
5420
+ path: bundleIconRelativePath,
5421
+ kind: "app-icon"
5422
+ } : null,
5370
5423
  frameCount: bundledFrames.length,
5371
5424
  hasRecordingFolder: recordingIncluded,
5372
5425
  hasNetworkTrace: networkEntries.length > 0 || !!networkCapture,
@@ -5418,6 +5471,11 @@ app.post("/api/export", async (c) => {
5418
5471
  if (esvpSnapshot) {
5419
5472
  writeExportJson(join6(bundleRoot, "esvp", "snapshot.json"), esvpSnapshot);
5420
5473
  }
5474
+ const templateContentPath = join6(PROJECTS_DIR, projectId, "template-content.json");
5475
+ if (existsSync5(templateContentPath)) {
5476
+ mkdirSync4(join6(bundleRoot, "templates"), { recursive: true });
5477
+ cpSync(templateContentPath, join6(bundleRoot, "templates", "content.json"));
5478
+ }
5421
5479
  writeExportText(join6(bundleRoot, "README.txt"), [
5422
5480
  `${rawProject.name}`,
5423
5481
  `Exported from DiscoveryLab ${APP_VERSION} on ${new Date(timestamp).toISOString()}.`,
@@ -5425,12 +5483,16 @@ app.post("/api/export", async (c) => {
5425
5483
  "This package bundles the local project context for sharing or re-analysis.",
5426
5484
  "",
5427
5485
  "Included when available:",
5428
- "- original media and thumbnail",
5429
- "- recording folder with session data, screenshots, and test script",
5486
+ "- selected thumbnail and analyzed frames",
5487
+ "- lightweight project/session metadata",
5430
5488
  "- OCR text and app intelligence summary",
5431
5489
  "- network trace, capture metadata, and ESVP snapshot",
5432
- "- previously generated export assets such as grids and renders",
5433
- "- Task Hub links, requirements, and test map"
5490
+ "- Task Hub links, requirements, and test map",
5491
+ "",
5492
+ "Excluded by default to keep the bundle Claude-friendly:",
5493
+ "- original long-form media",
5494
+ "- recording folder",
5495
+ "- generated export assets and renders"
5434
5496
  ].join("\n"));
5435
5497
  outputPath = join6(exportDir, `export-${timestamp}.${format}`);
5436
5498
  mimeType = "application/zip";
@@ -5591,8 +5653,8 @@ app.get("/api/visualization/:projectId/:templateId", async (c) => {
5591
5653
  const db = getDatabase();
5592
5654
  const [project] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
5593
5655
  if (!project) return c.json({ error: "Project not found" }, 404);
5594
- const { FRAMES_DIR } = await import("./db-5ECN3O7F.js");
5595
- const projectFramesDir = join6(FRAMES_DIR, projectId);
5656
+ const { FRAMES_DIR: FRAMES_DIR2 } = await import("./db-5ECN3O7F.js");
5657
+ const projectFramesDir = join6(FRAMES_DIR2, projectId);
5596
5658
  const dbFrames = await db.select().from(frames).where(eq(frames.projectId, projectId)).orderBy(frames.frameNumber).limit(10);
5597
5659
  let frameImages = [];
5598
5660
  if (dbFrames.length > 0) {
@@ -5913,6 +5975,72 @@ app.post("/api/visualization/screenshot", async (c) => {
5913
5975
  return c.json({ error: message }, 500);
5914
5976
  }
5915
5977
  });
5978
+ app.post("/api/export/infographic", async (c) => {
5979
+ try {
5980
+ const body = await c.req.json();
5981
+ const { projectId, open } = body;
5982
+ if (!projectId) return c.json({ error: "projectId required" }, 400);
5983
+ const db = getDatabase();
5984
+ const [project] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
5985
+ if (!project) return c.json({ error: "Project not found" }, 404);
5986
+ const { FRAMES_DIR: fDir, EXPORTS_DIR: eDir, PROJECTS_DIR: pDir } = await import("./db-5ECN3O7F.js");
5987
+ const { collectFrameImages, buildInfographicData, generateInfographicHtml } = await import("./infographic-GQAHEOAA.js");
5988
+ const dbFrames = await db.select().from(frames).where(eq(frames.projectId, projectId)).orderBy(frames.frameNumber).limit(20);
5989
+ let frameFiles;
5990
+ let frameOcr;
5991
+ if (dbFrames.length > 0) {
5992
+ frameFiles = dbFrames.map((f) => f.imagePath);
5993
+ frameOcr = dbFrames;
5994
+ } else {
5995
+ frameFiles = collectFrameImages(join6(fDir, projectId), project.videoPath, pDir, projectId);
5996
+ frameOcr = frameFiles.map(() => ({ ocrText: null }));
5997
+ }
5998
+ if (frameFiles.length === 0) {
5999
+ return c.json({ error: "No frames found. Run analyzer first." }, 400);
6000
+ }
6001
+ const cached = annotationCache.get(projectId);
6002
+ const annotations = cached?.steps?.map((s) => ({ label: s }));
6003
+ const data = buildInfographicData(project, frameFiles, frameOcr, annotations);
6004
+ const slug = (project.marketingTitle || project.name || projectId).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
6005
+ const outputPath = join6(eDir, `${slug}-infographic.html`);
6006
+ const result = generateInfographicHtml(data, outputPath);
6007
+ if (!result.success) return c.json({ error: result.error }, 500);
6008
+ if (open) {
6009
+ const { exec: exec2 } = await import("child_process");
6010
+ exec2(`open "${result.outputPath}"`);
6011
+ }
6012
+ return c.json({
6013
+ success: true,
6014
+ path: result.outputPath,
6015
+ downloadUrl: `/api/file?path=${encodeURIComponent(result.outputPath)}&download=true`,
6016
+ size: result.size,
6017
+ frameCount: result.frameCount
6018
+ });
6019
+ } catch (error) {
6020
+ const message = error instanceof Error ? error.message : "Unknown error";
6021
+ return c.json({ error: message }, 500);
6022
+ }
6023
+ });
6024
+ app.post("/api/import", async (c) => {
6025
+ try {
6026
+ const body = await c.req.json();
6027
+ const { filePath } = body;
6028
+ if (!filePath) return c.json({ error: "filePath required" }, 400);
6029
+ const { importApplabBundle } = await import("./import-W2JEW254.js");
6030
+ const { FRAMES_DIR: fDir, PROJECTS_DIR: pDir } = await import("./db-5ECN3O7F.js");
6031
+ const db = getDatabase();
6032
+ const result = await importApplabBundle(filePath, db, { projects, frames }, {
6033
+ dataDir: DATA_DIR,
6034
+ framesDir: fDir,
6035
+ projectsDir: pDir
6036
+ });
6037
+ if (!result.success) return c.json({ error: result.error }, 400);
6038
+ return c.json(result);
6039
+ } catch (error) {
6040
+ const message = error instanceof Error ? error.message : "Unknown error";
6041
+ return c.json({ error: message }, 500);
6042
+ }
6043
+ });
5916
6044
  app.get("/api/export/document/:projectId", async (c) => {
5917
6045
  try {
5918
6046
  const projectId = c.req.param("projectId");
@@ -5922,9 +6050,9 @@ app.get("/api/export/document/:projectId", async (c) => {
5922
6050
  const projectFrames = await db.select().from(frames).where(eq(frames.projectId, projectId)).orderBy(frames.frameNumber).limit(20);
5923
6051
  let frameData = projectFrames.map((f) => ({ imagePath: f.imagePath, ocrText: f.ocrText }));
5924
6052
  if (frameData.length === 0 && project.videoPath) {
5925
- const { FRAMES_DIR } = await import("./db-5ECN3O7F.js");
6053
+ const { FRAMES_DIR: FRAMES_DIR2 } = await import("./db-5ECN3O7F.js");
5926
6054
  const dirs = [
5927
- join6(FRAMES_DIR, projectId),
6055
+ join6(FRAMES_DIR2, projectId),
5928
6056
  join6(project.videoPath, "screenshots"),
5929
6057
  join6(PROJECTS_DIR, "maestro-recordings", projectId, "screenshots"),
5930
6058
  join6(PROJECTS_DIR, "web-recordings", projectId, "screenshots")
@@ -6001,7 +6129,7 @@ app.post("/api/export/batch", async (c) => {
6001
6129
  if (!manifest.destination?.type) {
6002
6130
  return c.json({ error: "Destination type required" }, 400);
6003
6131
  }
6004
- const { FRAMES_DIR } = await import("./db-5ECN3O7F.js");
6132
+ const { FRAMES_DIR: FRAMES_DIR2 } = await import("./db-5ECN3O7F.js");
6005
6133
  const dataProvider = {
6006
6134
  async getProject(projectId) {
6007
6135
  const db2 = getDatabase();
@@ -6009,7 +6137,7 @@ app.post("/api/export/batch", async (c) => {
6009
6137
  return p || null;
6010
6138
  },
6011
6139
  getFramesDir(projectId) {
6012
- return join6(FRAMES_DIR, projectId);
6140
+ return join6(FRAMES_DIR2, projectId);
6013
6141
  }
6014
6142
  };
6015
6143
  const result = await executeBatchExport(manifest, dataProvider, (progress) => {
@@ -6253,6 +6381,158 @@ app.get("/api/testing/status", async (c) => {
6253
6381
  return c.json({ error: message }, 500);
6254
6382
  }
6255
6383
  });
6384
+ function getClaudeDesktopConfigPath() {
6385
+ const home = homedir3();
6386
+ return process.platform === "win32" ? join6(process.env.APPDATA || join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json") : join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
6387
+ }
6388
+ function getClaudeDesktopAppCandidates() {
6389
+ const home = homedir3();
6390
+ if (process.platform === "darwin") {
6391
+ return [
6392
+ "/Applications/Claude.app",
6393
+ join6(home, "Applications", "Claude.app")
6394
+ ];
6395
+ }
6396
+ if (process.platform === "win32") {
6397
+ return [
6398
+ process.env.LOCALAPPDATA ? join6(process.env.LOCALAPPDATA, "Programs", "Claude", "Claude.exe") : "",
6399
+ process.env.PROGRAMFILES ? join6(process.env.PROGRAMFILES, "Claude", "Claude.exe") : "",
6400
+ process.env["PROGRAMFILES(X86)"] ? join6(process.env["PROGRAMFILES(X86)"], "Claude", "Claude.exe") : "",
6401
+ process.env.APPDATA ? join6(process.env.APPDATA, "Claude", "Claude.exe") : ""
6402
+ ].filter(Boolean);
6403
+ }
6404
+ return [];
6405
+ }
6406
+ function findClaudeDesktopApp() {
6407
+ if (process.platform === "darwin") {
6408
+ try {
6409
+ execSync2('open -Ra "Claude"', { stdio: "pipe", timeout: 2e3 });
6410
+ return {
6411
+ detected: true,
6412
+ launchTarget: "Claude",
6413
+ installPath: getClaudeDesktopAppCandidates().find((candidate2) => existsSync5(candidate2)) || null
6414
+ };
6415
+ } catch {
6416
+ const candidate2 = getClaudeDesktopAppCandidates().find((path) => existsSync5(path));
6417
+ return {
6418
+ detected: Boolean(candidate2),
6419
+ launchTarget: candidate2 ? "Claude" : null,
6420
+ installPath: candidate2 || null
6421
+ };
6422
+ }
6423
+ }
6424
+ const candidate = getClaudeDesktopAppCandidates().find((path) => existsSync5(path));
6425
+ return {
6426
+ detected: Boolean(candidate),
6427
+ launchTarget: candidate || null,
6428
+ installPath: candidate || null
6429
+ };
6430
+ }
6431
+ function detectDiscoveryLabClaudeDesktopMcp() {
6432
+ const configPath = getClaudeDesktopConfigPath();
6433
+ if (!existsSync5(configPath)) {
6434
+ return {
6435
+ configured: false,
6436
+ serverName: null,
6437
+ source: "none",
6438
+ configPath
6439
+ };
6440
+ }
6441
+ try {
6442
+ const raw = readFileSync2(configPath, "utf8");
6443
+ const parsed = JSON.parse(raw);
6444
+ const servers = parsed?.mcpServers;
6445
+ if (!servers || typeof servers !== "object") {
6446
+ return {
6447
+ configured: false,
6448
+ serverName: null,
6449
+ source: "none",
6450
+ configPath
6451
+ };
6452
+ }
6453
+ for (const [name, config] of Object.entries(servers)) {
6454
+ const configString = [
6455
+ name,
6456
+ config?.command || "",
6457
+ ...Array.isArray(config?.args) ? config.args : [],
6458
+ config?.url || ""
6459
+ ].join(" ").toLowerCase();
6460
+ if (name === "discoverylab" || configString.includes("@veolab/discoverylab") || configString.includes("discoverylab") || configString.includes("applab-discovery")) {
6461
+ return {
6462
+ configured: true,
6463
+ serverName: name || "discoverylab",
6464
+ source: "settings",
6465
+ configPath
6466
+ };
6467
+ }
6468
+ }
6469
+ } catch {
6470
+ }
6471
+ return {
6472
+ configured: false,
6473
+ serverName: null,
6474
+ source: "none",
6475
+ configPath
6476
+ };
6477
+ }
6478
+ app.get("/api/integrations/claude-desktop/status", async (c) => {
6479
+ try {
6480
+ const { platform } = await import("os");
6481
+ const app2 = findClaudeDesktopApp();
6482
+ const mcp = detectDiscoveryLabClaudeDesktopMcp();
6483
+ const launcherSupported = platform() === "darwin" || platform() === "win32";
6484
+ const ready = app2.detected && launcherSupported && mcp.configured;
6485
+ let message = "Claude Desktop launcher unavailable on this platform.";
6486
+ if (platform() === "darwin" || platform() === "win32") {
6487
+ if (!app2.detected) {
6488
+ message = "Claude Desktop was not detected on this machine.";
6489
+ } else if (!mcp.configured) {
6490
+ message = "Claude Desktop is installed, but the DiscoveryLab local MCP is not configured yet.";
6491
+ } else {
6492
+ message = "Claude Desktop is ready to open this project with the local DiscoveryLab MCP.";
6493
+ }
6494
+ }
6495
+ return c.json({
6496
+ ready,
6497
+ appDetected: app2.detected,
6498
+ launcherSupported,
6499
+ launchTarget: app2.launchTarget,
6500
+ installPath: app2.installPath,
6501
+ mcpConfigured: mcp.configured,
6502
+ serverName: mcp.serverName,
6503
+ source: mcp.source,
6504
+ configPath: mcp.configPath,
6505
+ installCommand: "npx -y @veolab/discoverylab@latest install --target desktop",
6506
+ message
6507
+ });
6508
+ } catch (error) {
6509
+ const message = error instanceof Error ? error.message : "Unknown error";
6510
+ return c.json({ error: message }, 500);
6511
+ }
6512
+ });
6513
+ app.post("/api/integrations/claude-desktop/launch", async (c) => {
6514
+ try {
6515
+ const { platform } = await import("os");
6516
+ const { exec: exec2 } = await import("child_process");
6517
+ const { promisify } = await import("util");
6518
+ const app2 = findClaudeDesktopApp();
6519
+ if (!app2.detected || !app2.launchTarget) {
6520
+ return c.json({ success: false, error: "Claude Desktop was not detected on this machine." }, 404);
6521
+ }
6522
+ const execAsync = promisify(exec2);
6523
+ if (platform() === "darwin") {
6524
+ await execAsync(`open -a "${app2.launchTarget}"`);
6525
+ } else if (platform() === "win32") {
6526
+ await execAsync(`cmd /c start "" "${app2.launchTarget}"`);
6527
+ } else {
6528
+ return c.json({ success: false, error: "Claude Desktop launcher is not supported on this platform." }, 400);
6529
+ }
6530
+ return c.json({ success: true });
6531
+ } catch (error) {
6532
+ const message = error instanceof Error ? error.message : "Unknown error";
6533
+ return c.json({ success: false, error: message }, 500);
6534
+ }
6535
+ });
6256
6536
  app.get("/api/integrations/jira-mcp/status", async (c) => {
6257
6537
  try {
6258
6538
  const { execSync: execSync3 } = await import("child_process");
@@ -9876,7 +10156,7 @@ app.get("/api/mobile-chat/providers", async (c) => {
9876
10156
  });
9877
10157
  app.get("/api/setup/status", async (c) => {
9878
10158
  try {
9879
- const { setupStatusTool } = await import("./setup-6JJYKKBS.js");
10159
+ const { setupStatusTool } = await import("./setup-SMN7FJNZ.js");
9880
10160
  const result = await setupStatusTool.handler({});
9881
10161
  const data = JSON.parse(result.content[0].text);
9882
10162
  const idbInstalled = await isIdbInstalled().catch(() => false);
@@ -11286,6 +11566,20 @@ app.post("/api/templates/render", async (c) => {
11286
11566
  return c.json({ error: templateState.eligibility.reason || `Templates are limited to videos up to ${TEMPLATE_MAX_DURATION_SECONDS} seconds.` }, 400);
11287
11567
  }
11288
11568
  const { props } = templateState;
11569
+ const forceRender = body.force === true;
11570
+ if (!forceRender) {
11571
+ const cached = getCachedRender(projectId, templateId);
11572
+ if (cached && existsSync5(cached)) {
11573
+ return c.json({
11574
+ jobId: "cached",
11575
+ status: "completed",
11576
+ outputPath: cached,
11577
+ downloadUrl: `/api/file?path=${encodeURIComponent(cached)}&download=true`,
11578
+ previewUrl: `/api/file?path=${encodeURIComponent(cached)}`,
11579
+ cached: true
11580
+ });
11581
+ }
11582
+ }
11289
11583
  const job = await startRender(projectId, templateId, props, (progress) => {
11290
11584
  broadcastToClients({
11291
11585
  type: "templateRenderProgress",