@veolab/discoverylab 1.4.3 → 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.
@@ -2,6 +2,11 @@ import {
2
2
  createJsonResult,
3
3
  createTextResult
4
4
  } from "./chunk-XKX6NBHF.js";
5
+ import {
6
+ LOCAL_ESVP_SERVER_URL,
7
+ getESVPHealth,
8
+ listESVPDevices
9
+ } from "./chunk-GRU332L4.js";
5
10
  import {
6
11
  DATA_DIR,
7
12
  DB_PATH
@@ -144,6 +149,24 @@ function checkMaestro(dep) {
144
149
  error: "Maestro not found in PATH or common installation directories"
145
150
  };
146
151
  }
152
+ function buildReplayExecutorStatus(input) {
153
+ return {
154
+ available: input.dependencyReady && input.devices.length > 0,
155
+ dependencyReady: input.dependencyReady,
156
+ deviceCount: input.devices.length,
157
+ devices: input.devices,
158
+ missing: input.missing
159
+ };
160
+ }
161
+ function createLocalReplayMessage(input) {
162
+ if (input.ready && input.recommendedExecutor) {
163
+ return `Local replay is ready. Recommended executor: ${input.recommendedExecutor}.`;
164
+ }
165
+ if (input.androidReady || input.iosReady) {
166
+ return "A mobile runtime is partially available, but the full local replay path still needs one or more dependencies.";
167
+ }
168
+ return "Local replay is not ready yet. Install the missing mobile dependencies and boot at least one iOS Simulator or Android device/emulator.";
169
+ }
147
170
  var setupStatusTool = {
148
171
  name: "dlab.setup.status",
149
172
  description: "Check the status of DiscoveryLab setup and all dependencies.",
@@ -218,6 +241,91 @@ Install with: ${dep.installHint}`);
218
241
  }
219
242
  }
220
243
  };
244
+ var setupReplayStatusTool = {
245
+ name: "dlab.setup.replay.status",
246
+ description: "Check whether this machine can run local ESVP replay for Claude Desktop using iOS Simulator or Android.",
247
+ inputSchema: z.object({}),
248
+ handler: async () => {
249
+ const adbStatus = checkDependency(dependencies[4]);
250
+ const xcodeStatus = platform() === "darwin" ? checkDependency(dependencies[3]) : { installed: false, version: null, error: "Xcode CLI Tools are only available on macOS" };
251
+ const maestroStatus = checkDependency(dependencies[1]);
252
+ const deviceEnvelope = await listESVPDevices("all").catch((error) => ({
253
+ adb: { devices: [], error: error instanceof Error ? error.message : String(error) },
254
+ iosSim: { devices: [], error: error instanceof Error ? error.message : String(error) },
255
+ maestroIos: { devices: [], error: error instanceof Error ? error.message : String(error) }
256
+ }));
257
+ const localEntropyHealth = await getESVPHealth(LOCAL_ESVP_SERVER_URL).catch((error) => ({
258
+ ok: false,
259
+ error: error instanceof Error ? error.message : String(error)
260
+ }));
261
+ const androidDevices = Array.isArray(deviceEnvelope?.adb?.devices) ? deviceEnvelope.adb.devices : [];
262
+ const iosSimDevices = Array.isArray(deviceEnvelope?.iosSim?.devices) ? deviceEnvelope.iosSim.devices : [];
263
+ const maestroIosDevices = Array.isArray(deviceEnvelope?.maestroIos?.devices) ? deviceEnvelope.maestroIos.devices : [];
264
+ const android = buildReplayExecutorStatus({
265
+ devices: androidDevices,
266
+ dependencyReady: adbStatus.installed,
267
+ missing: adbStatus.installed ? [] : ["adb"]
268
+ });
269
+ const iosSimulator = buildReplayExecutorStatus({
270
+ devices: iosSimDevices,
271
+ dependencyReady: xcodeStatus.installed,
272
+ missing: xcodeStatus.installed ? [] : ["xcode"]
273
+ });
274
+ const iosMaestro = buildReplayExecutorStatus({
275
+ devices: maestroIosDevices,
276
+ dependencyReady: maestroStatus.installed && xcodeStatus.installed,
277
+ missing: [
278
+ ...maestroStatus.installed ? [] : ["maestro"],
279
+ ...xcodeStatus.installed ? [] : ["xcode"]
280
+ ]
281
+ });
282
+ const recommendedExecutor = android.available ? "adb" : iosSimulator.available ? "ios-sim" : iosMaestro.available ? "maestro-ios" : null;
283
+ const ready = Boolean(localEntropyHealth?.ok) && Boolean(recommendedExecutor);
284
+ return createJsonResult({
285
+ ready,
286
+ minimumMobileReady: android.available || iosSimulator.available || iosMaestro.available,
287
+ recommendedExecutor,
288
+ message: createLocalReplayMessage({
289
+ ready,
290
+ recommendedExecutor,
291
+ androidReady: android.available,
292
+ iosReady: iosSimulator.available || iosMaestro.available
293
+ }),
294
+ entropyLocal: {
295
+ available: localEntropyHealth?.ok === true,
296
+ kind: "embedded-app-lab-runtime",
297
+ serverUrl: LOCAL_ESVP_SERVER_URL,
298
+ service: typeof localEntropyHealth?.service === "string" ? localEntropyHealth.service : "applab-esvp-local",
299
+ version: typeof localEntropyHealth?.version === "string" ? localEntropyHealth.version : null,
300
+ note: "Entropy local is the in-process AppLab ESVP runtime. It runs on this machine, stores runs under the local data directory, and uses local emulators/devices instead of a remote control-plane.",
301
+ error: localEntropyHealth?.ok === true ? null : typeof localEntropyHealth?.error === "string" ? localEntropyHealth.error : null
302
+ },
303
+ executors: {
304
+ android,
305
+ iosSimulator,
306
+ iosMaestro
307
+ },
308
+ dependencies: {
309
+ adb: {
310
+ installed: adbStatus.installed,
311
+ version: adbStatus.version,
312
+ installHint: adbStatus.installed ? null : dependencies[4].installHint
313
+ },
314
+ xcode: {
315
+ installed: xcodeStatus.installed,
316
+ version: xcodeStatus.version,
317
+ installHint: xcodeStatus.installed ? null : dependencies[3].installHint
318
+ },
319
+ maestro: {
320
+ installed: maestroStatus.installed,
321
+ version: maestroStatus.version,
322
+ installHint: maestroStatus.installed ? null : dependencies[1].installHint
323
+ }
324
+ },
325
+ dataDirectory: DATA_DIR
326
+ });
327
+ }
328
+ };
221
329
  var setupInitTool = {
222
330
  name: "dlab.setup.init",
223
331
  description: "Initialize DiscoveryLab data directories and database.",
@@ -323,11 +431,12 @@ Please run the following commands to install missing dependencies:
323
431
  );
324
432
  }
325
433
  };
326
- var setupTools = [setupStatusTool, setupCheckTool, setupInitTool, setupInstallTool];
434
+ var setupTools = [setupStatusTool, setupCheckTool, setupReplayStatusTool, setupInitTool, setupInstallTool];
327
435
 
328
436
  export {
329
437
  setupStatusTool,
330
438
  setupCheckTool,
439
+ setupReplayStatusTool,
331
440
  setupInitTool,
332
441
  setupInstallTool,
333
442
  setupTools
@@ -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";
@@ -5418,6 +5419,23 @@ app.post("/api/export", async (c) => {
5418
5419
  if (esvpSnapshot) {
5419
5420
  writeExportJson(join6(bundleRoot, "esvp", "snapshot.json"), esvpSnapshot);
5420
5421
  }
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
+ const templateContentPath = join6(PROJECTS_DIR, projectId, "template-content.json");
5435
+ if (existsSync5(templateContentPath)) {
5436
+ mkdirSync4(join6(bundleRoot, "templates"), { recursive: true });
5437
+ cpSync(templateContentPath, join6(bundleRoot, "templates", "content.json"));
5438
+ }
5421
5439
  writeExportText(join6(bundleRoot, "README.txt"), [
5422
5440
  `${rawProject.name}`,
5423
5441
  `Exported from DiscoveryLab ${APP_VERSION} on ${new Date(timestamp).toISOString()}.`,
@@ -5804,11 +5822,17 @@ Rules:
5804
5822
  default:
5805
5823
  return c.json({ error: `Unknown template: ${templateId}` }, 400);
5806
5824
  }
5807
- const templatePath = join6(__dirname, "..", "core", "visualizations", "templates", `${templateId}.html`);
5808
5825
  const possiblePaths = [
5809
- templatePath,
5810
- join6(__dirname, "..", "..", "src", "core", "visualizations", "templates", `${templateId}.html`),
5826
+ join6(__dirname, "..", "visualizations", `${templateId}.html`),
5827
+ // dist/visualizations/
5828
+ join6(__dirname, "visualizations", `${templateId}.html`),
5829
+ // dist/visualizations/ (alt)
5830
+ join6(__dirname, "..", "core", "visualizations", "templates", `${templateId}.html`),
5831
+ // dist/core/...
5832
+ join6(process.cwd(), "dist", "visualizations", `${templateId}.html`),
5833
+ // cwd/dist/visualizations/
5811
5834
  join6(process.cwd(), "src", "core", "visualizations", "templates", `${templateId}.html`)
5835
+ // dev: src/
5812
5836
  ];
5813
5837
  let htmlContent = "";
5814
5838
  for (const p of possiblePaths) {
@@ -5907,6 +5931,72 @@ app.post("/api/visualization/screenshot", async (c) => {
5907
5931
  return c.json({ error: message }, 500);
5908
5932
  }
5909
5933
  });
5934
+ app.post("/api/export/infographic", async (c) => {
5935
+ try {
5936
+ const body = await c.req.json();
5937
+ const { projectId, open } = body;
5938
+ if (!projectId) return c.json({ error: "projectId required" }, 400);
5939
+ const db = getDatabase();
5940
+ const [project] = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
5941
+ if (!project) return c.json({ error: "Project not found" }, 404);
5942
+ 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");
5944
+ 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);
5956
+ }
5957
+ const cached = annotationCache.get(projectId);
5958
+ const annotations = cached?.steps?.map((s) => ({ label: s }));
5959
+ const data = buildInfographicData(project, frameFiles, frameOcr, annotations);
5960
+ const slug = (project.marketingTitle || project.name || projectId).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
5961
+ const outputPath = join6(eDir, `${slug}-infographic.html`);
5962
+ const result = generateInfographicHtml(data, outputPath);
5963
+ if (!result.success) return c.json({ error: result.error }, 500);
5964
+ if (open) {
5965
+ const { exec: exec2 } = await import("child_process");
5966
+ exec2(`open "${result.outputPath}"`);
5967
+ }
5968
+ return c.json({
5969
+ success: true,
5970
+ path: result.outputPath,
5971
+ downloadUrl: `/api/file?path=${encodeURIComponent(result.outputPath)}&download=true`,
5972
+ size: result.size,
5973
+ frameCount: result.frameCount
5974
+ });
5975
+ } catch (error) {
5976
+ const message = error instanceof Error ? error.message : "Unknown error";
5977
+ return c.json({ error: message }, 500);
5978
+ }
5979
+ });
5980
+ app.post("/api/import", async (c) => {
5981
+ try {
5982
+ const body = await c.req.json();
5983
+ const { filePath } = body;
5984
+ if (!filePath) return c.json({ error: "filePath required" }, 400);
5985
+ const { importApplabBundle } = await import("./import-W2JEW254.js");
5986
+ const { FRAMES_DIR: fDir, PROJECTS_DIR: pDir } = await import("./db-5ECN3O7F.js");
5987
+ const db = getDatabase();
5988
+ const result = await importApplabBundle(filePath, db, { projects, frames }, {
5989
+ dataDir: DATA_DIR,
5990
+ framesDir: fDir,
5991
+ projectsDir: pDir
5992
+ });
5993
+ if (!result.success) return c.json({ error: result.error }, 400);
5994
+ return c.json(result);
5995
+ } catch (error) {
5996
+ const message = error instanceof Error ? error.message : "Unknown error";
5997
+ return c.json({ error: message }, 500);
5998
+ }
5999
+ });
5910
6000
  app.get("/api/export/document/:projectId", async (c) => {
5911
6001
  try {
5912
6002
  const projectId = c.req.param("projectId");
@@ -6247,6 +6337,158 @@ app.get("/api/testing/status", async (c) => {
6247
6337
  return c.json({ error: message }, 500);
6248
6338
  }
6249
6339
  });
6340
+ function getClaudeDesktopConfigPath() {
6341
+ const home = homedir3();
6342
+ 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");
6343
+ }
6344
+ function getClaudeDesktopAppCandidates() {
6345
+ const home = homedir3();
6346
+ if (process.platform === "darwin") {
6347
+ return [
6348
+ "/Applications/Claude.app",
6349
+ join6(home, "Applications", "Claude.app")
6350
+ ];
6351
+ }
6352
+ if (process.platform === "win32") {
6353
+ return [
6354
+ process.env.LOCALAPPDATA ? join6(process.env.LOCALAPPDATA, "Programs", "Claude", "Claude.exe") : "",
6355
+ process.env.PROGRAMFILES ? join6(process.env.PROGRAMFILES, "Claude", "Claude.exe") : "",
6356
+ process.env["PROGRAMFILES(X86)"] ? join6(process.env["PROGRAMFILES(X86)"], "Claude", "Claude.exe") : "",
6357
+ process.env.APPDATA ? join6(process.env.APPDATA, "Claude", "Claude.exe") : ""
6358
+ ].filter(Boolean);
6359
+ }
6360
+ return [];
6361
+ }
6362
+ function findClaudeDesktopApp() {
6363
+ if (process.platform === "darwin") {
6364
+ try {
6365
+ execSync2('open -Ra "Claude"', { stdio: "pipe", timeout: 2e3 });
6366
+ return {
6367
+ detected: true,
6368
+ launchTarget: "Claude",
6369
+ installPath: getClaudeDesktopAppCandidates().find((candidate2) => existsSync5(candidate2)) || null
6370
+ };
6371
+ } catch {
6372
+ const candidate2 = getClaudeDesktopAppCandidates().find((path) => existsSync5(path));
6373
+ return {
6374
+ detected: Boolean(candidate2),
6375
+ launchTarget: candidate2 ? "Claude" : null,
6376
+ installPath: candidate2 || null
6377
+ };
6378
+ }
6379
+ }
6380
+ const candidate = getClaudeDesktopAppCandidates().find((path) => existsSync5(path));
6381
+ return {
6382
+ detected: Boolean(candidate),
6383
+ launchTarget: candidate || null,
6384
+ installPath: candidate || null
6385
+ };
6386
+ }
6387
+ function detectDiscoveryLabClaudeDesktopMcp() {
6388
+ const configPath = getClaudeDesktopConfigPath();
6389
+ if (!existsSync5(configPath)) {
6390
+ return {
6391
+ configured: false,
6392
+ serverName: null,
6393
+ source: "none",
6394
+ configPath
6395
+ };
6396
+ }
6397
+ try {
6398
+ const raw = readFileSync2(configPath, "utf8");
6399
+ const parsed = JSON.parse(raw);
6400
+ const servers = parsed?.mcpServers;
6401
+ if (!servers || typeof servers !== "object") {
6402
+ return {
6403
+ configured: false,
6404
+ serverName: null,
6405
+ source: "none",
6406
+ configPath
6407
+ };
6408
+ }
6409
+ for (const [name, config] of Object.entries(servers)) {
6410
+ const configString = [
6411
+ name,
6412
+ config?.command || "",
6413
+ ...Array.isArray(config?.args) ? config.args : [],
6414
+ config?.url || ""
6415
+ ].join(" ").toLowerCase();
6416
+ if (name === "discoverylab" || configString.includes("@veolab/discoverylab") || configString.includes("discoverylab") || configString.includes("applab-discovery")) {
6417
+ return {
6418
+ configured: true,
6419
+ serverName: name || "discoverylab",
6420
+ source: "settings",
6421
+ configPath
6422
+ };
6423
+ }
6424
+ }
6425
+ } catch {
6426
+ }
6427
+ return {
6428
+ configured: false,
6429
+ serverName: null,
6430
+ source: "none",
6431
+ configPath
6432
+ };
6433
+ }
6434
+ app.get("/api/integrations/claude-desktop/status", async (c) => {
6435
+ try {
6436
+ const { platform } = await import("os");
6437
+ const app2 = findClaudeDesktopApp();
6438
+ const mcp = detectDiscoveryLabClaudeDesktopMcp();
6439
+ const launcherSupported = platform() === "darwin" || platform() === "win32";
6440
+ const ready = app2.detected && launcherSupported && mcp.configured;
6441
+ let message = "Claude Desktop launcher unavailable on this platform.";
6442
+ if (platform() === "darwin" || platform() === "win32") {
6443
+ if (!app2.detected) {
6444
+ message = "Claude Desktop was not detected on this machine.";
6445
+ } else if (!mcp.configured) {
6446
+ message = "Claude Desktop is installed, but the DiscoveryLab local MCP is not configured yet.";
6447
+ } else {
6448
+ message = "Claude Desktop is ready to open this project with the local DiscoveryLab MCP.";
6449
+ }
6450
+ }
6451
+ return c.json({
6452
+ ready,
6453
+ appDetected: app2.detected,
6454
+ launcherSupported,
6455
+ launchTarget: app2.launchTarget,
6456
+ installPath: app2.installPath,
6457
+ mcpConfigured: mcp.configured,
6458
+ serverName: mcp.serverName,
6459
+ source: mcp.source,
6460
+ configPath: mcp.configPath,
6461
+ installCommand: "npx -y @veolab/discoverylab@latest install --target desktop",
6462
+ message
6463
+ });
6464
+ } catch (error) {
6465
+ const message = error instanceof Error ? error.message : "Unknown error";
6466
+ return c.json({ error: message }, 500);
6467
+ }
6468
+ });
6469
+ app.post("/api/integrations/claude-desktop/launch", async (c) => {
6470
+ try {
6471
+ const { platform } = await import("os");
6472
+ const { exec: exec2 } = await import("child_process");
6473
+ const { promisify } = await import("util");
6474
+ const app2 = findClaudeDesktopApp();
6475
+ if (!app2.detected || !app2.launchTarget) {
6476
+ return c.json({ success: false, error: "Claude Desktop was not detected on this machine." }, 404);
6477
+ }
6478
+ const execAsync = promisify(exec2);
6479
+ if (platform() === "darwin") {
6480
+ await execAsync(`open -a "${app2.launchTarget}"`);
6481
+ } else if (platform() === "win32") {
6482
+ await execAsync(`cmd /c start "" "${app2.launchTarget}"`);
6483
+ } else {
6484
+ return c.json({ success: false, error: "Claude Desktop launcher is not supported on this platform." }, 400);
6485
+ }
6486
+ return c.json({ success: true });
6487
+ } catch (error) {
6488
+ const message = error instanceof Error ? error.message : "Unknown error";
6489
+ return c.json({ success: false, error: message }, 500);
6490
+ }
6491
+ });
6250
6492
  app.get("/api/integrations/jira-mcp/status", async (c) => {
6251
6493
  try {
6252
6494
  const { execSync: execSync3 } = await import("child_process");
@@ -9870,7 +10112,7 @@ app.get("/api/mobile-chat/providers", async (c) => {
9870
10112
  });
9871
10113
  app.get("/api/setup/status", async (c) => {
9872
10114
  try {
9873
- const { setupStatusTool } = await import("./setup-6JJYKKBS.js");
10115
+ const { setupStatusTool } = await import("./setup-F7MGEFIM.js");
9874
10116
  const result = await setupStatusTool.handler({});
9875
10117
  const data = JSON.parse(result.content[0].text);
9876
10118
  const idbInstalled = await isIdbInstalled().catch(() => false);
@@ -11280,6 +11522,20 @@ app.post("/api/templates/render", async (c) => {
11280
11522
  return c.json({ error: templateState.eligibility.reason || `Templates are limited to videos up to ${TEMPLATE_MAX_DURATION_SECONDS} seconds.` }, 400);
11281
11523
  }
11282
11524
  const { props } = templateState;
11525
+ const forceRender = body.force === true;
11526
+ if (!forceRender) {
11527
+ const cached = getCachedRender(projectId, templateId);
11528
+ if (cached && existsSync5(cached)) {
11529
+ return c.json({
11530
+ jobId: "cached",
11531
+ status: "completed",
11532
+ outputPath: cached,
11533
+ downloadUrl: `/api/file?path=${encodeURIComponent(cached)}&download=true`,
11534
+ previewUrl: `/api/file?path=${encodeURIComponent(cached)}`,
11535
+ cached: true
11536
+ });
11537
+ }
11538
+ }
11283
11539
  const job = await startRender(projectId, templateId, props, (progress) => {
11284
11540
  broadcastToClients({
11285
11541
  type: "templateRenderProgress",
package/dist/cli.js CHANGED
@@ -389,7 +389,7 @@ program.command("serve").alias("server").description("Start the DiscoveryLab web
389
389
  console.log(chalk.cyan("\n DiscoveryLab"));
390
390
  console.log(chalk.gray(" AI-powered app testing & evidence generator\n"));
391
391
  try {
392
- const { startServer } = await import("./server-CZPWQYOI.js");
392
+ const { startServer } = await import("./server-W3JQ5RG7.js");
393
393
  await startServer(port);
394
394
  console.log(chalk.green(` Server running at http://localhost:${port}`));
395
395
  console.log(chalk.gray(" Press Ctrl+C to stop\n"));
@@ -404,7 +404,7 @@ program.command("serve").alias("server").description("Start the DiscoveryLab web
404
404
  program.command("setup").description("Check and configure DiscoveryLab dependencies").action(async () => {
405
405
  console.log(chalk.cyan("\n DiscoveryLab Setup\n"));
406
406
  try {
407
- const { setupStatusTool } = await import("./setup-6JJYKKBS.js");
407
+ const { setupStatusTool } = await import("./setup-F7MGEFIM.js");
408
408
  const result = await setupStatusTool.handler({});
409
409
  if (result.isError) {
410
410
  console.error(chalk.red(" Setup check failed"));
@@ -454,35 +454,78 @@ program.command("init").description("Initialize DiscoveryLab data directories").
454
454
  process.exit(1);
455
455
  }
456
456
  });
457
- program.command("install").description("Install DiscoveryLab as Claude Code MCP server").action(async () => {
458
- const { homedir } = await import("os");
459
- const { existsSync, readFileSync, writeFileSync } = await import("fs");
460
- const { join } = await import("path");
461
- const claudeConfigPath = join(homedir(), ".claude.json");
457
+ program.command("install").description("Install DiscoveryLab as MCP server for Claude Code and/or Claude Desktop").option("--target <target>", "Installation target: code, desktop, all (default: auto-detect)", "").action(async (opts) => {
458
+ const { homedir, platform } = await import("os");
459
+ const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import("fs");
460
+ const { join, dirname } = await import("path");
461
+ const home = homedir();
462
+ const mcpEntry = {
463
+ command: "npx",
464
+ args: ["-y", "@veolab/discoverylab@latest", "mcp"]
465
+ };
466
+ const targets = {
467
+ code: {
468
+ name: "Claude Code",
469
+ path: join(home, ".claude.json"),
470
+ restart: "Restart Claude Code to activate."
471
+ },
472
+ desktop: {
473
+ name: "Claude Desktop",
474
+ path: platform() === "win32" ? join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json") : join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
475
+ restart: "Restart Claude Desktop to activate."
476
+ }
477
+ };
478
+ let selectedTargets = [];
479
+ const target = opts.target?.toLowerCase() || "";
480
+ if (target === "code") {
481
+ selectedTargets = ["code"];
482
+ } else if (target === "desktop") {
483
+ selectedTargets = ["desktop"];
484
+ } else if (target === "all") {
485
+ selectedTargets = ["code", "desktop"];
486
+ } else {
487
+ selectedTargets = ["code"];
488
+ const desktopDir = dirname(targets.desktop.path);
489
+ if (existsSync(desktopDir)) {
490
+ selectedTargets.push("desktop");
491
+ }
492
+ }
462
493
  console.log(chalk.cyan("\n Installing DiscoveryLab MCP...\n"));
463
- try {
464
- let config = {};
465
- if (existsSync(claudeConfigPath)) {
466
- const content = readFileSync(claudeConfigPath, "utf-8");
467
- config = JSON.parse(content);
494
+ let installed = 0;
495
+ for (const key of selectedTargets) {
496
+ const t = targets[key];
497
+ try {
498
+ const dir = dirname(t.path);
499
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
500
+ let config = {};
501
+ if (existsSync(t.path)) {
502
+ const content = readFileSync(t.path, "utf-8");
503
+ try {
504
+ config = JSON.parse(content);
505
+ } catch {
506
+ config = {};
507
+ }
508
+ }
509
+ if (!config.mcpServers) config.mcpServers = {};
510
+ config.mcpServers.discoverylab = mcpEntry;
511
+ writeFileSync(t.path, JSON.stringify(config, null, 2));
512
+ console.log(chalk.green(` \u2713 ${t.name} configured`));
513
+ console.log(chalk.gray(` ${t.path}`));
514
+ installed++;
515
+ } catch (error) {
516
+ console.log(chalk.yellow(` \u2717 ${t.name} skipped: ${error instanceof Error ? error.message : String(error)}`));
468
517
  }
469
- if (!config.mcpServers) {
470
- config.mcpServers = {};
518
+ }
519
+ console.log();
520
+ if (installed > 0) {
521
+ for (const key of selectedTargets) {
522
+ console.log(chalk.white(` ${targets[key].restart}`));
471
523
  }
472
- config.mcpServers.discoverylab = {
473
- command: "npx",
474
- args: ["-y", "@veolab/discoverylab@latest", "mcp"]
475
- };
476
- writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2));
477
- console.log(chalk.green(" \u2713 Added to ~/.claude.json"));
478
- console.log();
479
- console.log(chalk.white(" Restart Claude Code to activate."));
480
524
  console.log(chalk.gray(" Or run: discoverylab serve"));
481
- console.log();
482
- } catch (error) {
483
- console.error(chalk.red(` Failed to install: ${error}`));
484
- process.exit(1);
525
+ } else {
526
+ console.log(chalk.red(" No targets configured."));
485
527
  }
528
+ console.log();
486
529
  });
487
530
  program.command("mcp").description("Run as MCP server (for Claude Code integration)").action(async () => {
488
531
  try {
@@ -502,7 +545,7 @@ program.command("mcp").description("Run as MCP server (for Claude Code integrati
502
545
  taskHubTools,
503
546
  esvpTools,
504
547
  knowledgeTools
505
- } = await import("./tools-Q7OZO732.js");
548
+ } = await import("./tools-VYFNRUS4.js");
506
549
  mcpServer.registerTools([
507
550
  ...uiTools,
508
551
  ...projectTools,
@@ -540,6 +583,94 @@ program.command("info").description("Show version and configuration info").actio
540
583
  console.log();
541
584
  }
542
585
  });
586
+ program.command("export").description("Export project in various formats").argument("<project-id>", "Project ID or slug").option("--format <format>", "Export format (infographic, applab, esvp)", "infographic").option("--output <path>", "Custom output path").option("--open", "Open file after generation").option("--compress", "Force image compression").option("--no-baseline", "Omit baseline info").action(async (projectId, opts) => {
587
+ try {
588
+ if (opts.format === "infographic") {
589
+ console.log(chalk.cyan(`
590
+ Exporting infographic for: ${projectId}
591
+ `));
592
+ const { join: pathJoin } = await import("path");
593
+ const { getDatabase, projects, frames: framesTable, FRAMES_DIR, EXPORTS_DIR, PROJECTS_DIR } = await import("./db-5ECN3O7F.js");
594
+ const { eq } = await import("drizzle-orm");
595
+ const { collectFrameImages, buildInfographicData, generateInfographicHtml } = await import("./infographic-GQAHEOAA.js");
596
+ const db = getDatabase();
597
+ const allProjects = await db.select().from(projects);
598
+ const project = allProjects.find((p) => p.id === projectId || p.id.startsWith(projectId) || p.name.toLowerCase().includes(projectId.toLowerCase()));
599
+ if (!project) {
600
+ console.log(chalk.red(` Project not found: ${projectId}`));
601
+ console.log(chalk.gray(" Available projects:"));
602
+ for (const p of allProjects.slice(0, 10)) {
603
+ console.log(chalk.gray(` ${p.id.slice(0, 12)} - ${p.marketingTitle || p.name}`));
604
+ }
605
+ return;
606
+ }
607
+ console.log(chalk.green(` \u2714 Found project: ${project.marketingTitle || project.name}`));
608
+ const dbFrames = await db.select().from(framesTable).where(eq(framesTable.projectId, project.id)).orderBy(framesTable.frameNumber).limit(20);
609
+ let frameFiles;
610
+ let frameOcr;
611
+ if (dbFrames.length > 0) {
612
+ frameFiles = dbFrames.map((f) => f.imagePath);
613
+ frameOcr = dbFrames;
614
+ } else {
615
+ frameFiles = collectFrameImages(pathJoin(FRAMES_DIR, project.id), project.videoPath, PROJECTS_DIR, project.id);
616
+ frameOcr = frameFiles.map(() => ({ ocrText: null }));
617
+ }
618
+ console.log(chalk.green(` \u2714 ${frameFiles.length} frames found`));
619
+ if (frameFiles.length === 0) {
620
+ console.log(chalk.red(" No frames found. Run analyzer first."));
621
+ return;
622
+ }
623
+ const data = buildInfographicData(project, frameFiles, frameOcr);
624
+ console.log(chalk.green(` \u2714 ${project.aiSummary ? "AI analysis loaded" : "No analysis (basic labels)"}`));
625
+ const slug = (project.marketingTitle || project.name || project.id).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
626
+ const outputPath = opts.output ? pathJoin(opts.output, `${slug}-infographic.html`) : pathJoin(EXPORTS_DIR, `${slug}-infographic.html`);
627
+ const result = generateInfographicHtml(data, outputPath);
628
+ if (result.success) {
629
+ const sizeKb = ((result.size || 0) / 1024).toFixed(1);
630
+ console.log(chalk.green(` \u2714 Exported: ${result.outputPath} (${sizeKb}KB, ${result.frameCount} frames)`));
631
+ if (opts.open) {
632
+ const { exec } = await import("child_process");
633
+ exec(`open "${result.outputPath}"`);
634
+ }
635
+ } else {
636
+ console.log(chalk.red(` Export failed: ${result.error}`));
637
+ }
638
+ } else {
639
+ console.log(chalk.yellow(` Format "${opts.format}" - use the web UI for applab/esvp exports.`));
640
+ }
641
+ } catch (error) {
642
+ console.log(chalk.red(` Export failed: ${error instanceof Error ? error.message : String(error)}`));
643
+ }
644
+ });
645
+ program.command("import").description("Import a shared .applab project bundle").argument("<file>", "Path to .applab file").action(async (file) => {
646
+ try {
647
+ const { resolve } = await import("path");
648
+ const filePath = resolve(file);
649
+ console.log(chalk.cyan(`
650
+ Importing: ${filePath}
651
+ `));
652
+ const { getDatabase, projects, frames: framesTable, DATA_DIR, FRAMES_DIR, PROJECTS_DIR } = await import("./db-5ECN3O7F.js");
653
+ const { importApplabBundle } = await import("./import-W2JEW254.js");
654
+ const db = getDatabase();
655
+ const result = await importApplabBundle(filePath, db, { projects, frames: framesTable }, {
656
+ dataDir: DATA_DIR,
657
+ framesDir: FRAMES_DIR,
658
+ projectsDir: PROJECTS_DIR
659
+ });
660
+ if (result.success) {
661
+ console.log(chalk.green(` \u2714 Imported: ${result.projectName}`));
662
+ console.log(chalk.green(` \u2714 ${result.frameCount} frames`));
663
+ console.log(chalk.gray(` ID: ${result.projectId}
664
+ `));
665
+ } else {
666
+ console.log(chalk.red(` Import failed: ${result.error}
667
+ `));
668
+ }
669
+ } catch (error) {
670
+ console.log(chalk.red(` Import failed: ${error instanceof Error ? error.message : String(error)}
671
+ `));
672
+ }
673
+ });
543
674
  program.parse();
544
675
  if (!process.argv.slice(2).length) {
545
676
  program.outputHelp();