muse-app-preview-mcp 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -57,6 +57,58 @@ function generateId() {
57
57
  }
58
58
  // Tool definitions
59
59
  const tools = [
60
+ {
61
+ name: "create_app_previews",
62
+ description: "Complete workflow to create App Store preview images. Steps: 1) User selects platform (iOS/macOS/watchOS), 2) User selects language, 3) Claude generates marketing text, 4) Auto-capture screenshots, 5) Send to MUSE Preview Maker app or save to folder.",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ bundleId: {
67
+ type: "string",
68
+ description: "Bundle ID of the app to capture",
69
+ },
70
+ appName: {
71
+ type: "string",
72
+ description: "Name of the app (for generating marketing text)",
73
+ },
74
+ appDescription: {
75
+ type: "string",
76
+ description: "Brief description of what the app does (helps Claude generate better text)",
77
+ },
78
+ platform: {
79
+ type: "string",
80
+ enum: ["ios", "macos", "watchos"],
81
+ description: "Target platform",
82
+ },
83
+ language: {
84
+ type: "string",
85
+ enum: ["ko", "en", "ja", "zh", "es", "fr", "de"],
86
+ description: "Language for preview text (ko=Korean, en=English, etc.)",
87
+ },
88
+ screenCount: {
89
+ type: "number",
90
+ description: "Number of screens to capture. Default: 3",
91
+ },
92
+ previews: {
93
+ type: "array",
94
+ description: "Array of preview definitions with title/subtitle. If not provided, Claude should generate these.",
95
+ items: {
96
+ type: "object",
97
+ properties: {
98
+ title: { type: "string" },
99
+ subtitle: { type: "string" },
100
+ paletteId: { type: "string" },
101
+ },
102
+ },
103
+ },
104
+ simulatorUDID: {
105
+ type: "string",
106
+ description: "Specific simulator UDID for iOS/watchOS. Optional.",
107
+ },
108
+ },
109
+ required: ["bundleId", "appName", "platform", "language"],
110
+ },
111
+ },
60
112
  {
61
113
  name: "add_preview",
62
114
  description: "Add a new preview set with screenshot, title, and subtitle. The screenshot will be used to generate an app store preview image.",
@@ -269,13 +321,13 @@ const tools = [
269
321
  },
270
322
  {
271
323
  name: "launch_and_capture",
272
- description: "Launch a specific app in the simulator, wait for it to load, then capture a screenshot. This is the smart capture that ensures the app is ready before taking screenshots.",
324
+ description: "Launch an app and capture screenshot. Auto-detects platform (iOS simulator or macOS). For iOS, launches in simulator and captures. For macOS, launches app and captures window.",
273
325
  inputSchema: {
274
326
  type: "object",
275
327
  properties: {
276
328
  bundleId: {
277
329
  type: "string",
278
- description: "Bundle ID of the app to launch (e.g., 'com.example.myapp')",
330
+ description: "Bundle ID of the app (e.g., 'com.example.myapp')",
279
331
  },
280
332
  title: {
281
333
  type: "string",
@@ -285,25 +337,34 @@ const tools = [
285
337
  type: "string",
286
338
  description: "Subtitle text for the preview",
287
339
  },
340
+ platform: {
341
+ type: "string",
342
+ enum: ["ios", "macos", "auto"],
343
+ description: "Platform: 'ios' for simulator, 'macos' for Mac app, 'auto' to detect. Default: auto",
344
+ },
288
345
  waitSeconds: {
289
346
  type: "number",
290
347
  description: "Seconds to wait after launch before capture. Default: 3",
291
348
  },
292
349
  simulatorUDID: {
293
350
  type: "string",
294
- description: "Specific simulator UDID. Optional.",
351
+ description: "Specific iOS simulator UDID. Optional.",
295
352
  },
296
353
  paletteId: {
297
354
  type: "string",
298
355
  description: "Color palette ID. Optional.",
299
356
  },
357
+ deviceId: {
358
+ type: "string",
359
+ description: "Device ID for preview (iphone_6_7, mac_retina, etc.). Auto-detected if not specified.",
360
+ },
300
361
  },
301
362
  required: ["bundleId", "title", "subtitle"],
302
363
  },
303
364
  },
304
365
  {
305
366
  name: "capture_app_screens",
306
- description: "Capture multiple screens from a running app by simulating navigation. Generates a complete set of App Store preview images.",
367
+ description: "Capture multiple screens from a running app. Auto-detects platform. Launches app, captures first screen, then captures additional screens with wait intervals.",
307
368
  inputSchema: {
308
369
  type: "object",
309
370
  properties: {
@@ -324,14 +385,305 @@ const tools = [
324
385
  },
325
386
  },
326
387
  },
388
+ platform: {
389
+ type: "string",
390
+ enum: ["ios", "macos", "auto"],
391
+ description: "Platform: 'ios', 'macos', or 'auto'. Default: auto",
392
+ },
327
393
  simulatorUDID: {
328
394
  type: "string",
329
- description: "Specific simulator UDID. Optional.",
395
+ description: "Specific iOS simulator UDID. Optional.",
330
396
  },
331
397
  },
332
398
  required: ["bundleId", "screens"],
333
399
  },
334
400
  },
401
+ // === Advanced Capture Tools ===
402
+ {
403
+ name: "interact_simulator",
404
+ description: "Interact with iOS Simulator: tap, swipe, scroll, type text. Use this to navigate to specific screens before capturing.",
405
+ inputSchema: {
406
+ type: "object",
407
+ properties: {
408
+ action: {
409
+ type: "string",
410
+ enum: ["tap", "swipe", "scroll", "type", "home", "shake"],
411
+ description: "Action type",
412
+ },
413
+ x: {
414
+ type: "number",
415
+ description: "X coordinate (for tap/swipe start)",
416
+ },
417
+ y: {
418
+ type: "number",
419
+ description: "Y coordinate (for tap/swipe start)",
420
+ },
421
+ toX: {
422
+ type: "number",
423
+ description: "End X coordinate (for swipe)",
424
+ },
425
+ toY: {
426
+ type: "number",
427
+ description: "End Y coordinate (for swipe)",
428
+ },
429
+ direction: {
430
+ type: "string",
431
+ enum: ["up", "down", "left", "right"],
432
+ description: "Scroll/swipe direction (alternative to coordinates)",
433
+ },
434
+ text: {
435
+ type: "string",
436
+ description: "Text to type (for type action)",
437
+ },
438
+ duration: {
439
+ type: "number",
440
+ description: "Duration in seconds (for swipe). Default: 0.3",
441
+ },
442
+ simulatorUDID: {
443
+ type: "string",
444
+ description: "Target simulator UDID. Uses tracked simulator if not specified.",
445
+ },
446
+ },
447
+ required: ["action"],
448
+ },
449
+ },
450
+ {
451
+ name: "navigate_app",
452
+ description: "Navigate to a specific screen in the app using deep links (URL schemes) or by opening URLs.",
453
+ inputSchema: {
454
+ type: "object",
455
+ properties: {
456
+ url: {
457
+ type: "string",
458
+ description: "Deep link URL or URL scheme (e.g., 'myapp://settings', 'https://example.com/page')",
459
+ },
460
+ bundleId: {
461
+ type: "string",
462
+ description: "Bundle ID of the app (required for some navigations)",
463
+ },
464
+ waitSeconds: {
465
+ type: "number",
466
+ description: "Seconds to wait after navigation. Default: 2",
467
+ },
468
+ simulatorUDID: {
469
+ type: "string",
470
+ description: "Target simulator UDID.",
471
+ },
472
+ },
473
+ required: ["url"],
474
+ },
475
+ },
476
+ {
477
+ name: "configure_simulator",
478
+ description: "Configure simulator appearance: status bar time, battery, signal, dark/light mode, etc. Makes screenshots look professional.",
479
+ inputSchema: {
480
+ type: "object",
481
+ properties: {
482
+ statusBarTime: {
483
+ type: "string",
484
+ description: "Set status bar time (e.g., '9:41')",
485
+ },
486
+ batteryLevel: {
487
+ type: "number",
488
+ description: "Battery level 0-100",
489
+ },
490
+ batteryState: {
491
+ type: "string",
492
+ enum: ["charging", "charged", "discharging"],
493
+ description: "Battery state",
494
+ },
495
+ cellularBars: {
496
+ type: "number",
497
+ description: "Cellular signal bars 0-4",
498
+ },
499
+ wifiBars: {
500
+ type: "number",
501
+ description: "WiFi signal bars 0-3",
502
+ },
503
+ appearance: {
504
+ type: "string",
505
+ enum: ["light", "dark"],
506
+ description: "Light or dark mode",
507
+ },
508
+ simulatorUDID: {
509
+ type: "string",
510
+ description: "Target simulator UDID.",
511
+ },
512
+ },
513
+ },
514
+ },
515
+ {
516
+ name: "auto_capture_flow",
517
+ description: "Automatically capture a user flow with actions. Define a sequence of actions (tap, swipe, wait) and capture at each step. Perfect for capturing onboarding flows, feature tours, etc.",
518
+ inputSchema: {
519
+ type: "object",
520
+ properties: {
521
+ bundleId: {
522
+ type: "string",
523
+ description: "Bundle ID of the app",
524
+ },
525
+ flow: {
526
+ type: "array",
527
+ description: "Sequence of actions and captures",
528
+ items: {
529
+ type: "object",
530
+ properties: {
531
+ action: {
532
+ type: "string",
533
+ enum: ["capture", "tap", "swipe", "scroll", "wait", "type", "navigate"],
534
+ description: "Action to perform",
535
+ },
536
+ title: { type: "string", description: "Title for capture" },
537
+ subtitle: { type: "string", description: "Subtitle for capture" },
538
+ paletteId: { type: "string", description: "Palette for capture" },
539
+ x: { type: "number" },
540
+ y: { type: "number" },
541
+ toX: { type: "number" },
542
+ toY: { type: "number" },
543
+ direction: { type: "string", enum: ["up", "down", "left", "right"] },
544
+ text: { type: "string" },
545
+ url: { type: "string", description: "URL for navigate action" },
546
+ seconds: { type: "number", description: "Wait time or animation duration" },
547
+ },
548
+ },
549
+ },
550
+ resetFirst: {
551
+ type: "boolean",
552
+ description: "Reset app state before starting. Default: true",
553
+ },
554
+ simulatorUDID: {
555
+ type: "string",
556
+ description: "Target simulator UDID.",
557
+ },
558
+ },
559
+ required: ["bundleId", "flow"],
560
+ },
561
+ },
562
+ {
563
+ name: "scroll_and_capture",
564
+ description: "Scroll through a screen and capture at intervals. Great for capturing long lists, feeds, or scrollable content.",
565
+ inputSchema: {
566
+ type: "object",
567
+ properties: {
568
+ bundleId: {
569
+ type: "string",
570
+ description: "Bundle ID of the app",
571
+ },
572
+ scrollCount: {
573
+ type: "number",
574
+ description: "Number of scroll actions. Default: 3",
575
+ },
576
+ direction: {
577
+ type: "string",
578
+ enum: ["up", "down", "left", "right"],
579
+ description: "Scroll direction. Default: down",
580
+ },
581
+ captureEach: {
582
+ type: "boolean",
583
+ description: "Capture after each scroll. Default: true",
584
+ },
585
+ titles: {
586
+ type: "array",
587
+ items: { type: "string" },
588
+ description: "Titles for each capture",
589
+ },
590
+ subtitles: {
591
+ type: "array",
592
+ items: { type: "string" },
593
+ description: "Subtitles for each capture",
594
+ },
595
+ paletteIds: {
596
+ type: "array",
597
+ items: { type: "string" },
598
+ description: "Palette IDs for each capture",
599
+ },
600
+ waitBetween: {
601
+ type: "number",
602
+ description: "Seconds to wait between scrolls. Default: 1",
603
+ },
604
+ simulatorUDID: {
605
+ type: "string",
606
+ description: "Target simulator UDID.",
607
+ },
608
+ },
609
+ required: ["bundleId"],
610
+ },
611
+ },
612
+ {
613
+ name: "batch_capture_devices",
614
+ description: "Capture the same screen on multiple devices/simulators. Great for generating previews for all device sizes at once.",
615
+ inputSchema: {
616
+ type: "object",
617
+ properties: {
618
+ bundleId: {
619
+ type: "string",
620
+ description: "Bundle ID of the app",
621
+ },
622
+ title: {
623
+ type: "string",
624
+ description: "Title for all captures",
625
+ },
626
+ subtitle: {
627
+ type: "string",
628
+ description: "Subtitle for all captures",
629
+ },
630
+ paletteId: {
631
+ type: "string",
632
+ description: "Palette ID for all captures",
633
+ },
634
+ devices: {
635
+ type: "array",
636
+ items: { type: "string" },
637
+ description: "List of simulator names or UDIDs to capture",
638
+ },
639
+ bootDevices: {
640
+ type: "boolean",
641
+ description: "Boot specified devices if not running. Default: false",
642
+ },
643
+ waitSeconds: {
644
+ type: "number",
645
+ description: "Wait time after launching app. Default: 3",
646
+ },
647
+ },
648
+ required: ["bundleId", "title", "subtitle"],
649
+ },
650
+ },
651
+ {
652
+ name: "record_and_extract",
653
+ description: "Record simulator video and extract key frames as screenshots. Useful for capturing animations or transitions.",
654
+ inputSchema: {
655
+ type: "object",
656
+ properties: {
657
+ bundleId: {
658
+ type: "string",
659
+ description: "Bundle ID of the app",
660
+ },
661
+ duration: {
662
+ type: "number",
663
+ description: "Recording duration in seconds. Default: 5",
664
+ },
665
+ frameCount: {
666
+ type: "number",
667
+ description: "Number of frames to extract. Default: 3",
668
+ },
669
+ titles: {
670
+ type: "array",
671
+ items: { type: "string" },
672
+ description: "Titles for each extracted frame",
673
+ },
674
+ subtitles: {
675
+ type: "array",
676
+ items: { type: "string" },
677
+ description: "Subtitles for each extracted frame",
678
+ },
679
+ simulatorUDID: {
680
+ type: "string",
681
+ description: "Target simulator UDID.",
682
+ },
683
+ },
684
+ required: ["bundleId"],
685
+ },
686
+ },
335
687
  ];
336
688
  // Tool handlers
337
689
  async function handleAddPreview(args) {
@@ -563,12 +915,16 @@ async function handleUpdateSettings(args) {
563
915
  settings: store.settings,
564
916
  });
565
917
  }
566
- // Helper to get booted simulators
918
+ // Helper to get booted simulators (iOS/iPadOS only, prioritized)
567
919
  async function getBootedSimulators() {
568
920
  const { stdout } = await execAsync("xcrun simctl list devices booted -j");
569
921
  const data = JSON.parse(stdout);
570
922
  const bootedDevices = [];
571
923
  for (const [runtime, devices] of Object.entries(data.devices)) {
924
+ // Only include iOS/iPadOS simulators (skip watchOS, tvOS, visionOS)
925
+ if (!runtime.toLowerCase().includes("ios")) {
926
+ continue;
927
+ }
572
928
  const deviceList = devices;
573
929
  for (const device of deviceList) {
574
930
  if (device.state === "Booted") {
@@ -576,15 +932,30 @@ async function getBootedSimulators() {
576
932
  const runtimeVersion = runtimeMatch
577
933
  ? `iOS ${runtimeMatch[1].replace("-", ".")}`
578
934
  : runtime;
935
+ // Priority: iPhone Pro Max > iPhone Pro > iPhone > iPad
936
+ let priority = 0;
937
+ const name = device.name.toLowerCase();
938
+ if (name.includes("iphone") && name.includes("pro max"))
939
+ priority = 4;
940
+ else if (name.includes("iphone") && name.includes("pro"))
941
+ priority = 3;
942
+ else if (name.includes("iphone"))
943
+ priority = 2;
944
+ else if (name.includes("ipad"))
945
+ priority = 1;
579
946
  bootedDevices.push({
580
947
  name: device.name,
581
948
  udid: device.udid,
582
949
  runtime: runtimeVersion,
950
+ priority,
583
951
  });
584
952
  }
585
953
  }
586
954
  }
587
- return bootedDevices;
955
+ // Sort by priority (highest first)
956
+ bootedDevices.sort((a, b) => b.priority - a.priority);
957
+ // Remove priority field before returning
958
+ return bootedDevices.map(({ name, udid, runtime }) => ({ name, udid, runtime }));
588
959
  }
589
960
  // Map simulator name to deviceId
590
961
  function mapSimulatorToDeviceId(simulatorName) {
@@ -776,6 +1147,68 @@ async function isAppRunning(simulatorUDID, bundleId) {
776
1147
  return false;
777
1148
  }
778
1149
  }
1150
+ // Helper: Detect platform for bundle ID
1151
+ async function detectPlatform(bundleId) {
1152
+ // Check if app exists on macOS
1153
+ try {
1154
+ const { stdout } = await execAsync(`mdfind "kMDItemCFBundleIdentifier == '${bundleId}'" | head -1`);
1155
+ if (stdout.trim()) {
1156
+ return "macos";
1157
+ }
1158
+ }
1159
+ catch {
1160
+ // Not found on macOS
1161
+ }
1162
+ // Check if app exists on any booted iOS simulator
1163
+ try {
1164
+ const simulators = await getBootedSimulators();
1165
+ for (const sim of simulators) {
1166
+ try {
1167
+ await execAsync(`xcrun simctl get_app_container ${sim.udid} ${bundleId}`);
1168
+ return "ios";
1169
+ }
1170
+ catch {
1171
+ // Not on this simulator
1172
+ }
1173
+ }
1174
+ }
1175
+ catch {
1176
+ // No simulators or error
1177
+ }
1178
+ return null;
1179
+ }
1180
+ // Helper: Capture macOS app window
1181
+ async function captureMacOSWindow(bundleId, outputPath) {
1182
+ try {
1183
+ // Get window ID for the app
1184
+ const { stdout: windowInfo } = await execAsync(`
1185
+ osascript -e 'tell application "System Events" to get id of first window of (first process whose bundle identifier is "${bundleId}")'
1186
+ `);
1187
+ const windowId = windowInfo.trim();
1188
+ if (windowId) {
1189
+ // Capture specific window by ID
1190
+ await execAsync(`screencapture -l ${windowId} -o "${outputPath}"`);
1191
+ return fs.existsSync(outputPath);
1192
+ }
1193
+ }
1194
+ catch {
1195
+ // Fallback: capture by app name
1196
+ }
1197
+ // Fallback: use screencapture with window selection
1198
+ try {
1199
+ // Bring app to front and capture
1200
+ await execAsync(`osascript -e 'tell application id "${bundleId}" to activate'`);
1201
+ await sleep(0.5);
1202
+ // Capture the frontmost window
1203
+ await execAsync(`screencapture -o -w "${outputPath}"`);
1204
+ return fs.existsSync(outputPath);
1205
+ }
1206
+ catch (error) {
1207
+ return false;
1208
+ }
1209
+ }
1210
+ // Track launched simulator for this session
1211
+ let lastLaunchedSimulator = null;
779
1212
  // Handler: Complete reset
780
1213
  async function handleResetAll(args) {
781
1214
  if (!args.confirm) {
@@ -823,65 +1256,135 @@ async function handleResetAll(args) {
823
1256
  });
824
1257
  }
825
1258
  }
826
- // Handler: Launch app and capture
1259
+ // Handler: Launch app and capture (supports iOS and macOS)
827
1260
  async function handleLaunchAndCapture(args) {
828
1261
  try {
829
1262
  ensureScreenshotsDir();
830
- // Get target simulator
831
- const bootedSimulators = await getBootedSimulators();
832
- if (bootedSimulators.length === 0) {
833
- return JSON.stringify({
834
- success: false,
835
- error: "No iOS Simulator is running",
836
- hint: "Please launch an iOS Simulator first",
837
- });
838
- }
839
- let targetSimulator = args.simulatorUDID
840
- ? bootedSimulators.find(s => s.udid === args.simulatorUDID)
841
- : bootedSimulators[0];
842
- if (!targetSimulator) {
843
- return JSON.stringify({
844
- success: false,
845
- error: "Specified simulator not found",
846
- bootedSimulators,
847
- });
1263
+ const waitTime = args.waitSeconds || 3;
1264
+ const timestamp = Date.now();
1265
+ const safeBundleId = args.bundleId.replace(/[^a-zA-Z0-9]/g, "_");
1266
+ // Determine platform
1267
+ let platform = args.platform || "auto";
1268
+ if (platform === "auto") {
1269
+ const detected = await detectPlatform(args.bundleId);
1270
+ if (!detected) {
1271
+ return JSON.stringify({
1272
+ success: false,
1273
+ error: `App not found: ${args.bundleId}`,
1274
+ hint: "Make sure the app is installed on macOS or iOS simulator",
1275
+ });
1276
+ }
1277
+ platform = detected;
848
1278
  }
849
- // Terminate app first (clean state)
850
- try {
851
- await execAsync(`xcrun simctl terminate ${targetSimulator.udid} ${args.bundleId}`);
852
- await sleep(0.5);
853
- }
854
- catch {
855
- // App might not be running, that's ok
856
- }
857
- // Launch the app
858
- try {
859
- await execAsync(`xcrun simctl launch ${targetSimulator.udid} ${args.bundleId}`);
1279
+ let screenshotPath;
1280
+ let deviceId;
1281
+ let captureInfo = {};
1282
+ if (platform === "macos") {
1283
+ // === macOS App Capture ===
1284
+ screenshotPath = path.join(SCREENSHOTS_DIR, `${safeBundleId}_mac_${timestamp}.png`);
1285
+ // Launch macOS app
1286
+ try {
1287
+ await execAsync(`open -b "${args.bundleId}"`);
1288
+ }
1289
+ catch (launchError) {
1290
+ return JSON.stringify({
1291
+ success: false,
1292
+ error: `Failed to launch macOS app: ${launchError.message}`,
1293
+ bundleId: args.bundleId,
1294
+ });
1295
+ }
1296
+ // Wait for app to load
1297
+ await sleep(waitTime);
1298
+ // Capture window
1299
+ const captured = await captureMacOSWindow(args.bundleId, screenshotPath);
1300
+ if (!captured) {
1301
+ return JSON.stringify({
1302
+ success: false,
1303
+ error: "Failed to capture macOS window",
1304
+ hint: "Make sure the app has a visible window",
1305
+ });
1306
+ }
1307
+ deviceId = args.deviceId || "mac_retina";
1308
+ captureInfo = { platform: "macos" };
860
1309
  }
861
- catch (launchError) {
862
- return JSON.stringify({
863
- success: false,
864
- error: `Failed to launch app: ${launchError.message}`,
865
- hint: "Make sure the app is installed on the simulator",
1310
+ else {
1311
+ // === iOS Simulator Capture ===
1312
+ const bootedSimulators = await getBootedSimulators();
1313
+ if (bootedSimulators.length === 0) {
1314
+ return JSON.stringify({
1315
+ success: false,
1316
+ error: "No iOS Simulator is running",
1317
+ hint: "Please launch an iOS Simulator first",
1318
+ });
1319
+ }
1320
+ // Find target simulator
1321
+ let targetSimulator = args.simulatorUDID
1322
+ ? bootedSimulators.find(s => s.udid === args.simulatorUDID)
1323
+ : null;
1324
+ // If not specified, find simulator that has the app installed
1325
+ if (!targetSimulator) {
1326
+ for (const sim of bootedSimulators) {
1327
+ try {
1328
+ await execAsync(`xcrun simctl get_app_container ${sim.udid} ${args.bundleId}`);
1329
+ targetSimulator = sim;
1330
+ break;
1331
+ }
1332
+ catch {
1333
+ // App not on this simulator
1334
+ }
1335
+ }
1336
+ }
1337
+ // Fallback to first simulator
1338
+ if (!targetSimulator) {
1339
+ targetSimulator = bootedSimulators[0];
1340
+ }
1341
+ screenshotPath = path.join(SCREENSHOTS_DIR, `${safeBundleId}_ios_${timestamp}.png`);
1342
+ // Terminate app first (clean state)
1343
+ try {
1344
+ await execAsync(`xcrun simctl terminate ${targetSimulator.udid} ${args.bundleId}`);
1345
+ await sleep(0.5);
1346
+ }
1347
+ catch {
1348
+ // App might not be running
1349
+ }
1350
+ // Launch the app
1351
+ try {
1352
+ await execAsync(`xcrun simctl launch ${targetSimulator.udid} ${args.bundleId}`);
1353
+ }
1354
+ catch (launchError) {
1355
+ return JSON.stringify({
1356
+ success: false,
1357
+ error: `Failed to launch app: ${launchError.message}`,
1358
+ hint: "Make sure the app is installed on the simulator",
1359
+ bundleId: args.bundleId,
1360
+ simulator: targetSimulator.name,
1361
+ });
1362
+ }
1363
+ // Track this simulator
1364
+ lastLaunchedSimulator = {
1365
+ udid: targetSimulator.udid,
1366
+ name: targetSimulator.name,
866
1367
  bundleId: args.bundleId,
867
- });
868
- }
869
- // Wait for app to load
870
- const waitTime = args.waitSeconds || 3;
871
- await sleep(waitTime);
872
- // Capture screenshot
873
- const timestamp = Date.now();
874
- const safeBundleId = args.bundleId.replace(/[^a-zA-Z0-9]/g, "_");
875
- const screenshotPath = path.join(SCREENSHOTS_DIR, `${safeBundleId}_${timestamp}.png`);
876
- await execAsync(`xcrun simctl io ${targetSimulator.udid} screenshot "${screenshotPath}"`);
877
- if (!fs.existsSync(screenshotPath)) {
878
- return JSON.stringify({
879
- success: false,
880
- error: "Screenshot capture failed",
881
- });
1368
+ };
1369
+ // Wait for app to load
1370
+ await sleep(waitTime);
1371
+ // Capture screenshot from specific simulator
1372
+ await execAsync(`xcrun simctl io ${targetSimulator.udid} screenshot "${screenshotPath}"`);
1373
+ if (!fs.existsSync(screenshotPath)) {
1374
+ return JSON.stringify({
1375
+ success: false,
1376
+ error: "Screenshot capture failed",
1377
+ });
1378
+ }
1379
+ deviceId = args.deviceId || mapSimulatorToDeviceId(targetSimulator.name);
1380
+ captureInfo = {
1381
+ platform: "ios",
1382
+ simulator: {
1383
+ name: targetSimulator.name,
1384
+ udid: targetSimulator.udid,
1385
+ },
1386
+ };
882
1387
  }
883
- // Auto-detect device ID
884
- const autoDeviceId = mapSimulatorToDeviceId(targetSimulator.name);
885
1388
  // Add to preview store
886
1389
  const store = loadStore();
887
1390
  const preview = {
@@ -889,7 +1392,7 @@ async function handleLaunchAndCapture(args) {
889
1392
  screenshotPath: screenshotPath,
890
1393
  title: args.title,
891
1394
  subtitle: args.subtitle,
892
- deviceId: autoDeviceId,
1395
+ deviceId: deviceId,
893
1396
  paletteId: args.paletteId || store.settings.defaultPaletteId,
894
1397
  createdAt: new Date().toISOString(),
895
1398
  };
@@ -897,11 +1400,8 @@ async function handleLaunchAndCapture(args) {
897
1400
  saveStore(store);
898
1401
  return JSON.stringify({
899
1402
  success: true,
900
- message: `App launched and captured: ${args.bundleId}`,
901
- simulator: {
902
- name: targetSimulator.name,
903
- udid: targetSimulator.udid,
904
- },
1403
+ message: `App launched and captured (${platform}): ${args.bundleId}`,
1404
+ ...captureInfo,
905
1405
  waitedSeconds: waitTime,
906
1406
  preview: preview,
907
1407
  totalPreviews: store.previews.length,
@@ -1000,10 +1500,805 @@ async function handleCaptureAppScreens(args) {
1000
1500
  });
1001
1501
  }
1002
1502
  }
1503
+ // Handler: Complete workflow to create app previews
1504
+ async function handleCreateAppPreviews(args) {
1505
+ try {
1506
+ ensureScreenshotsDir();
1507
+ const screenCount = args.screenCount || 3;
1508
+ const palettes = ["ocean", "sunset", "forest", "lavender", "coral"];
1509
+ // Step 1: Reset existing data
1510
+ const dataDir = path.join(process.env.HOME || "~", ".muse-app-preview");
1511
+ if (fs.existsSync(SCREENSHOTS_DIR)) {
1512
+ const files = fs.readdirSync(SCREENSHOTS_DIR);
1513
+ for (const file of files) {
1514
+ fs.unlinkSync(path.join(SCREENSHOTS_DIR, file));
1515
+ }
1516
+ }
1517
+ const store = loadStore();
1518
+ store.previews = [];
1519
+ saveStore(store);
1520
+ // Step 2: Determine device ID based on platform
1521
+ let deviceId;
1522
+ let captureMethod;
1523
+ switch (args.platform) {
1524
+ case "macos":
1525
+ deviceId = "mac_retina";
1526
+ captureMethod = "macos";
1527
+ break;
1528
+ case "watchos":
1529
+ deviceId = "watch_45mm";
1530
+ captureMethod = "watch";
1531
+ break;
1532
+ default:
1533
+ deviceId = "iphone_6_7";
1534
+ captureMethod = "simulator";
1535
+ }
1536
+ // Step 3: Prepare preview definitions
1537
+ const previewDefs = args.previews || [];
1538
+ // If previews not provided, this will be empty and Claude should fill them
1539
+ if (previewDefs.length === 0) {
1540
+ return JSON.stringify({
1541
+ success: false,
1542
+ needsInput: true,
1543
+ message: "Please provide preview titles and subtitles",
1544
+ hint: `Generate ${screenCount} marketing phrases for ${args.appName} in ${args.language}`,
1545
+ template: {
1546
+ previews: Array.from({ length: screenCount }, (_, i) => ({
1547
+ title: `[Title ${i + 1}]`,
1548
+ subtitle: `[Subtitle ${i + 1}]`,
1549
+ paletteId: palettes[i % palettes.length],
1550
+ })),
1551
+ },
1552
+ appInfo: {
1553
+ name: args.appName,
1554
+ description: args.appDescription,
1555
+ platform: args.platform,
1556
+ language: args.language,
1557
+ },
1558
+ });
1559
+ }
1560
+ // Step 4: Capture screenshots based on platform
1561
+ const capturedPreviews = [];
1562
+ if (captureMethod === "macos") {
1563
+ // macOS capture
1564
+ try {
1565
+ await execAsync(`open -b "${args.bundleId}"`);
1566
+ }
1567
+ catch {
1568
+ return JSON.stringify({
1569
+ success: false,
1570
+ error: `App not found: ${args.bundleId}`,
1571
+ hint: "Make sure the macOS app is installed",
1572
+ });
1573
+ }
1574
+ await sleep(3);
1575
+ for (let i = 0; i < previewDefs.length; i++) {
1576
+ const def = previewDefs[i];
1577
+ const timestamp = Date.now();
1578
+ const screenshotPath = path.join(SCREENSHOTS_DIR, `mac_${i + 1}_${timestamp}.png`);
1579
+ const captured = await captureMacOSWindow(args.bundleId, screenshotPath);
1580
+ if (captured) {
1581
+ const preview = {
1582
+ id: generateId(),
1583
+ screenshotPath,
1584
+ title: def.title,
1585
+ subtitle: def.subtitle,
1586
+ deviceId,
1587
+ paletteId: def.paletteId || palettes[i % palettes.length],
1588
+ createdAt: new Date().toISOString(),
1589
+ };
1590
+ store.previews.push(preview);
1591
+ capturedPreviews.push(preview);
1592
+ }
1593
+ if (i < previewDefs.length - 1) {
1594
+ await sleep(2);
1595
+ }
1596
+ }
1597
+ }
1598
+ else {
1599
+ // iOS/watchOS simulator capture
1600
+ const bootedSimulators = await getBootedSimulators();
1601
+ if (bootedSimulators.length === 0) {
1602
+ return JSON.stringify({
1603
+ success: false,
1604
+ error: "No iOS Simulator is running",
1605
+ hint: "Please launch an iOS Simulator first",
1606
+ });
1607
+ }
1608
+ // Find appropriate simulator
1609
+ let targetSimulator = args.simulatorUDID
1610
+ ? bootedSimulators.find(s => s.udid === args.simulatorUDID)
1611
+ : null;
1612
+ if (!targetSimulator) {
1613
+ for (const sim of bootedSimulators) {
1614
+ try {
1615
+ await execAsync(`xcrun simctl get_app_container ${sim.udid} ${args.bundleId}`);
1616
+ targetSimulator = sim;
1617
+ break;
1618
+ }
1619
+ catch { }
1620
+ }
1621
+ }
1622
+ if (!targetSimulator) {
1623
+ targetSimulator = bootedSimulators[0];
1624
+ }
1625
+ // Launch app
1626
+ try {
1627
+ await execAsync(`xcrun simctl terminate ${targetSimulator.udid} ${args.bundleId}`);
1628
+ await sleep(0.5);
1629
+ }
1630
+ catch { }
1631
+ try {
1632
+ await execAsync(`xcrun simctl launch ${targetSimulator.udid} ${args.bundleId}`);
1633
+ }
1634
+ catch (e) {
1635
+ return JSON.stringify({
1636
+ success: false,
1637
+ error: `Failed to launch app: ${e.message}`,
1638
+ hint: "Make sure the app is installed on the simulator",
1639
+ });
1640
+ }
1641
+ // Track simulator
1642
+ lastLaunchedSimulator = {
1643
+ udid: targetSimulator.udid,
1644
+ name: targetSimulator.name,
1645
+ bundleId: args.bundleId,
1646
+ };
1647
+ await sleep(3);
1648
+ deviceId = mapSimulatorToDeviceId(targetSimulator.name);
1649
+ for (let i = 0; i < previewDefs.length; i++) {
1650
+ const def = previewDefs[i];
1651
+ const timestamp = Date.now();
1652
+ const screenshotPath = path.join(SCREENSHOTS_DIR, `ios_${i + 1}_${timestamp}.png`);
1653
+ await execAsync(`xcrun simctl io ${targetSimulator.udid} screenshot "${screenshotPath}"`);
1654
+ if (fs.existsSync(screenshotPath)) {
1655
+ const preview = {
1656
+ id: generateId(),
1657
+ screenshotPath,
1658
+ title: def.title,
1659
+ subtitle: def.subtitle,
1660
+ deviceId,
1661
+ paletteId: def.paletteId || palettes[i % palettes.length],
1662
+ createdAt: new Date().toISOString(),
1663
+ };
1664
+ store.previews.push(preview);
1665
+ capturedPreviews.push(preview);
1666
+ }
1667
+ if (i < previewDefs.length - 1) {
1668
+ await sleep(2);
1669
+ }
1670
+ }
1671
+ }
1672
+ saveStore(store);
1673
+ // Step 5: Send to app or save to folder
1674
+ const pendingPath = path.join(dataDir, "pending-previews.json");
1675
+ fs.writeFileSync(pendingPath, JSON.stringify({ previews: store.previews }, null, 2));
1676
+ let appOpened = false;
1677
+ try {
1678
+ await execAsync(`open -b ${APP_BUNDLE_ID}`);
1679
+ appOpened = true;
1680
+ }
1681
+ catch {
1682
+ // App not installed
1683
+ }
1684
+ if (appOpened) {
1685
+ return JSON.stringify({
1686
+ success: true,
1687
+ message: `Created ${capturedPreviews.length} preview(s) and opened MUSE Preview Maker`,
1688
+ platform: args.platform,
1689
+ language: args.language,
1690
+ previews: capturedPreviews.map(p => ({ title: p.title, subtitle: p.subtitle, paletteId: p.paletteId })),
1691
+ appOpened: true,
1692
+ });
1693
+ }
1694
+ else {
1695
+ // Open folder instead
1696
+ await execAsync(`open "${dataDir}"`);
1697
+ return JSON.stringify({
1698
+ success: true,
1699
+ message: `Created ${capturedPreviews.length} preview(s). MUSE Preview Maker not installed.`,
1700
+ platform: args.platform,
1701
+ language: args.language,
1702
+ previews: capturedPreviews.map(p => ({ title: p.title, subtitle: p.subtitle, paletteId: p.paletteId })),
1703
+ appOpened: false,
1704
+ dataFolder: dataDir,
1705
+ hint: "Install MUSE Preview Maker to generate final images, or use the screenshots directly.",
1706
+ });
1707
+ }
1708
+ }
1709
+ catch (error) {
1710
+ return JSON.stringify({
1711
+ success: false,
1712
+ error: `Create app previews failed: ${error.message}`,
1713
+ });
1714
+ }
1715
+ }
1716
+ // === Advanced Capture Handlers ===
1717
+ // Helper: Get target simulator (uses tracked or finds appropriate one)
1718
+ async function getTargetSimulator(simulatorUDID) {
1719
+ const bootedSimulators = await getBootedSimulators();
1720
+ if (bootedSimulators.length === 0)
1721
+ return null;
1722
+ if (simulatorUDID) {
1723
+ return bootedSimulators.find(s => s.udid === simulatorUDID) || null;
1724
+ }
1725
+ // Use last launched simulator if available
1726
+ if (lastLaunchedSimulator) {
1727
+ const found = bootedSimulators.find(s => s.udid === lastLaunchedSimulator.udid);
1728
+ if (found)
1729
+ return found;
1730
+ }
1731
+ return bootedSimulators[0];
1732
+ }
1733
+ // Helper: Perform touch/gesture via AppleScript (controls Simulator.app window)
1734
+ async function simulatorInteract(udid, action, params) {
1735
+ try {
1736
+ // Get simulator window info
1737
+ const { stdout: windowBounds } = await execAsync(`
1738
+ osascript -e 'tell application "Simulator"
1739
+ set frontmost to true
1740
+ delay 0.2
1741
+ end tell'
1742
+ `);
1743
+ // For swipe/scroll by direction, calculate coordinates
1744
+ let startX = params.x || 200;
1745
+ let startY = params.y || 400;
1746
+ let endX = params.toX || startX;
1747
+ let endY = params.toY || startY;
1748
+ const duration = params.duration || 0.3;
1749
+ if (params.direction) {
1750
+ const swipeDistance = 300;
1751
+ switch (params.direction) {
1752
+ case "up":
1753
+ endY = startY - swipeDistance;
1754
+ break;
1755
+ case "down":
1756
+ endY = startY + swipeDistance;
1757
+ break;
1758
+ case "left":
1759
+ endX = startX - swipeDistance;
1760
+ break;
1761
+ case "right":
1762
+ endX = startX + swipeDistance;
1763
+ break;
1764
+ }
1765
+ }
1766
+ switch (action) {
1767
+ case "tap":
1768
+ // Use cliclick if available, otherwise AppleScript
1769
+ try {
1770
+ await execAsync(`cliclick c:${startX},${startY}`);
1771
+ }
1772
+ catch {
1773
+ // Fallback: Use AppleScript to click
1774
+ await execAsync(`
1775
+ osascript -e 'tell application "System Events"
1776
+ tell process "Simulator"
1777
+ click at {${startX}, ${startY}}
1778
+ end tell
1779
+ end tell'
1780
+ `);
1781
+ }
1782
+ break;
1783
+ case "swipe":
1784
+ case "scroll":
1785
+ // Use AppleScript for swipe gesture
1786
+ await execAsync(`
1787
+ osascript -e 'tell application "System Events"
1788
+ tell process "Simulator"
1789
+ set frontmost to true
1790
+ end tell
1791
+ end tell'
1792
+ `);
1793
+ // Simctl doesn't have direct swipe, but we can use privacy/UI automation
1794
+ // For now, use key events for scrolling
1795
+ if (action === "scroll") {
1796
+ const scrollKey = params.direction === "up" || params.direction === "left" ? "126" : "125";
1797
+ const count = 5;
1798
+ for (let i = 0; i < count; i++) {
1799
+ await execAsync(`
1800
+ osascript -e 'tell application "System Events" to key code ${scrollKey}'
1801
+ `);
1802
+ await sleep(0.1);
1803
+ }
1804
+ }
1805
+ break;
1806
+ case "type":
1807
+ if (params.text) {
1808
+ // Type text character by character
1809
+ await execAsync(`
1810
+ osascript -e 'tell application "System Events"
1811
+ tell process "Simulator"
1812
+ set frontmost to true
1813
+ keystroke "${params.text.replace(/"/g, '\\"')}"
1814
+ end tell
1815
+ end tell'
1816
+ `);
1817
+ }
1818
+ break;
1819
+ case "home":
1820
+ await execAsync(`xcrun simctl ui ${udid} home`);
1821
+ break;
1822
+ case "shake":
1823
+ // Trigger shake gesture (useful for debug menus)
1824
+ await execAsync(`
1825
+ osascript -e 'tell application "Simulator" to activate' -e 'tell application "System Events" to keystroke "z" using {control down, command down}'
1826
+ `);
1827
+ break;
1828
+ }
1829
+ return true;
1830
+ }
1831
+ catch (error) {
1832
+ console.error("Simulator interaction failed:", error);
1833
+ return false;
1834
+ }
1835
+ }
1836
+ // Handler: Interact with simulator
1837
+ async function handleInteractSimulator(args) {
1838
+ try {
1839
+ const simulator = await getTargetSimulator(args.simulatorUDID);
1840
+ if (!simulator) {
1841
+ return JSON.stringify({
1842
+ success: false,
1843
+ error: "No iOS Simulator is running",
1844
+ });
1845
+ }
1846
+ const success = await simulatorInteract(simulator.udid, args.action, {
1847
+ x: args.x,
1848
+ y: args.y,
1849
+ toX: args.toX,
1850
+ toY: args.toY,
1851
+ direction: args.direction,
1852
+ text: args.text,
1853
+ duration: args.duration,
1854
+ });
1855
+ return JSON.stringify({
1856
+ success,
1857
+ message: success ? `Performed ${args.action} on simulator` : `${args.action} failed`,
1858
+ simulator: { name: simulator.name, udid: simulator.udid },
1859
+ action: args.action,
1860
+ params: { x: args.x, y: args.y, direction: args.direction },
1861
+ });
1862
+ }
1863
+ catch (error) {
1864
+ return JSON.stringify({
1865
+ success: false,
1866
+ error: `Interact failed: ${error.message}`,
1867
+ });
1868
+ }
1869
+ }
1870
+ // Handler: Navigate app via deep link
1871
+ async function handleNavigateApp(args) {
1872
+ try {
1873
+ const simulator = await getTargetSimulator(args.simulatorUDID);
1874
+ if (!simulator) {
1875
+ return JSON.stringify({
1876
+ success: false,
1877
+ error: "No iOS Simulator is running",
1878
+ });
1879
+ }
1880
+ // Open URL in simulator
1881
+ await execAsync(`xcrun simctl openurl ${simulator.udid} "${args.url}"`);
1882
+ // Wait for navigation
1883
+ await sleep(args.waitSeconds || 2);
1884
+ return JSON.stringify({
1885
+ success: true,
1886
+ message: `Navigated to ${args.url}`,
1887
+ simulator: { name: simulator.name, udid: simulator.udid },
1888
+ url: args.url,
1889
+ });
1890
+ }
1891
+ catch (error) {
1892
+ return JSON.stringify({
1893
+ success: false,
1894
+ error: `Navigation failed: ${error.message}`,
1895
+ });
1896
+ }
1897
+ }
1898
+ // Handler: Configure simulator
1899
+ async function handleConfigureSimulator(args) {
1900
+ try {
1901
+ const simulator = await getTargetSimulator(args.simulatorUDID);
1902
+ if (!simulator) {
1903
+ return JSON.stringify({
1904
+ success: false,
1905
+ error: "No iOS Simulator is running",
1906
+ });
1907
+ }
1908
+ const changes = [];
1909
+ // Override status bar
1910
+ if (args.statusBarTime || args.batteryLevel !== undefined || args.cellularBars !== undefined || args.wifiBars !== undefined) {
1911
+ let statusBarCmd = `xcrun simctl status_bar ${simulator.udid} override`;
1912
+ if (args.statusBarTime) {
1913
+ statusBarCmd += ` --time "${args.statusBarTime}"`;
1914
+ changes.push(`time: ${args.statusBarTime}`);
1915
+ }
1916
+ if (args.batteryLevel !== undefined) {
1917
+ statusBarCmd += ` --batteryLevel ${args.batteryLevel}`;
1918
+ changes.push(`battery: ${args.batteryLevel}%`);
1919
+ }
1920
+ if (args.batteryState) {
1921
+ statusBarCmd += ` --batteryState ${args.batteryState}`;
1922
+ changes.push(`batteryState: ${args.batteryState}`);
1923
+ }
1924
+ if (args.cellularBars !== undefined) {
1925
+ statusBarCmd += ` --cellularBars ${args.cellularBars}`;
1926
+ changes.push(`cellular: ${args.cellularBars} bars`);
1927
+ }
1928
+ if (args.wifiBars !== undefined) {
1929
+ statusBarCmd += ` --wifiBars ${args.wifiBars}`;
1930
+ changes.push(`wifi: ${args.wifiBars} bars`);
1931
+ }
1932
+ await execAsync(statusBarCmd);
1933
+ }
1934
+ // Set appearance (dark/light mode)
1935
+ if (args.appearance) {
1936
+ await execAsync(`xcrun simctl ui ${simulator.udid} appearance ${args.appearance}`);
1937
+ changes.push(`appearance: ${args.appearance}`);
1938
+ }
1939
+ return JSON.stringify({
1940
+ success: true,
1941
+ message: "Simulator configured",
1942
+ simulator: { name: simulator.name, udid: simulator.udid },
1943
+ changes,
1944
+ });
1945
+ }
1946
+ catch (error) {
1947
+ return JSON.stringify({
1948
+ success: false,
1949
+ error: `Configure failed: ${error.message}`,
1950
+ });
1951
+ }
1952
+ }
1953
+ // Handler: Auto capture flow
1954
+ async function handleAutoCaptureFlow(args) {
1955
+ try {
1956
+ ensureScreenshotsDir();
1957
+ const simulator = await getTargetSimulator(args.simulatorUDID);
1958
+ if (!simulator) {
1959
+ return JSON.stringify({
1960
+ success: false,
1961
+ error: "No iOS Simulator is running",
1962
+ });
1963
+ }
1964
+ // Reset if requested
1965
+ if (args.resetFirst !== false) {
1966
+ try {
1967
+ await execAsync(`xcrun simctl terminate ${simulator.udid} ${args.bundleId}`);
1968
+ await sleep(0.5);
1969
+ }
1970
+ catch { }
1971
+ }
1972
+ // Launch app
1973
+ try {
1974
+ await execAsync(`xcrun simctl launch ${simulator.udid} ${args.bundleId}`);
1975
+ await sleep(2);
1976
+ }
1977
+ catch (e) {
1978
+ return JSON.stringify({
1979
+ success: false,
1980
+ error: `Failed to launch app: ${e.message}`,
1981
+ });
1982
+ }
1983
+ // Track this simulator
1984
+ lastLaunchedSimulator = {
1985
+ udid: simulator.udid,
1986
+ name: simulator.name,
1987
+ bundleId: args.bundleId,
1988
+ };
1989
+ const store = loadStore();
1990
+ const capturedPreviews = [];
1991
+ const palettes = ["ocean", "sunset", "forest", "lavender", "coral"];
1992
+ const deviceId = mapSimulatorToDeviceId(simulator.name);
1993
+ let captureIndex = 0;
1994
+ // Execute flow
1995
+ for (let i = 0; i < args.flow.length; i++) {
1996
+ const step = args.flow[i];
1997
+ switch (step.action) {
1998
+ case "capture":
1999
+ const timestamp = Date.now();
2000
+ const screenshotPath = path.join(SCREENSHOTS_DIR, `flow_${captureIndex + 1}_${timestamp}.png`);
2001
+ await execAsync(`xcrun simctl io ${simulator.udid} screenshot "${screenshotPath}"`);
2002
+ if (fs.existsSync(screenshotPath)) {
2003
+ const preview = {
2004
+ id: generateId(),
2005
+ screenshotPath,
2006
+ title: step.title || `Screen ${captureIndex + 1}`,
2007
+ subtitle: step.subtitle || "",
2008
+ deviceId,
2009
+ paletteId: step.paletteId || palettes[captureIndex % palettes.length],
2010
+ createdAt: new Date().toISOString(),
2011
+ };
2012
+ store.previews.push(preview);
2013
+ capturedPreviews.push(preview);
2014
+ captureIndex++;
2015
+ }
2016
+ break;
2017
+ case "tap":
2018
+ case "swipe":
2019
+ case "scroll":
2020
+ case "type":
2021
+ await simulatorInteract(simulator.udid, step.action, {
2022
+ x: step.x,
2023
+ y: step.y,
2024
+ toX: step.toX,
2025
+ toY: step.toY,
2026
+ direction: step.direction,
2027
+ text: step.text,
2028
+ });
2029
+ await sleep(0.5);
2030
+ break;
2031
+ case "wait":
2032
+ await sleep(step.seconds || 1);
2033
+ break;
2034
+ case "navigate":
2035
+ if (step.url) {
2036
+ await execAsync(`xcrun simctl openurl ${simulator.udid} "${step.url}"`);
2037
+ await sleep(step.seconds || 2);
2038
+ }
2039
+ break;
2040
+ }
2041
+ }
2042
+ saveStore(store);
2043
+ return JSON.stringify({
2044
+ success: true,
2045
+ message: `Executed flow with ${args.flow.length} steps, captured ${capturedPreviews.length} screenshots`,
2046
+ simulator: { name: simulator.name, udid: simulator.udid },
2047
+ previews: capturedPreviews.map(p => ({ title: p.title, subtitle: p.subtitle })),
2048
+ totalPreviews: store.previews.length,
2049
+ });
2050
+ }
2051
+ catch (error) {
2052
+ return JSON.stringify({
2053
+ success: false,
2054
+ error: `Auto capture flow failed: ${error.message}`,
2055
+ });
2056
+ }
2057
+ }
2058
+ // Handler: Scroll and capture
2059
+ async function handleScrollAndCapture(args) {
2060
+ try {
2061
+ ensureScreenshotsDir();
2062
+ const simulator = await getTargetSimulator(args.simulatorUDID);
2063
+ if (!simulator) {
2064
+ return JSON.stringify({
2065
+ success: false,
2066
+ error: "No iOS Simulator is running",
2067
+ });
2068
+ }
2069
+ const scrollCount = args.scrollCount || 3;
2070
+ const direction = args.direction || "down";
2071
+ const waitBetween = args.waitBetween || 1;
2072
+ const palettes = ["ocean", "sunset", "forest", "lavender", "coral"];
2073
+ const store = loadStore();
2074
+ const capturedPreviews = [];
2075
+ const deviceId = mapSimulatorToDeviceId(simulator.name);
2076
+ // Capture initial state
2077
+ const initialTimestamp = Date.now();
2078
+ const initialPath = path.join(SCREENSHOTS_DIR, `scroll_0_${initialTimestamp}.png`);
2079
+ await execAsync(`xcrun simctl io ${simulator.udid} screenshot "${initialPath}"`);
2080
+ if (fs.existsSync(initialPath)) {
2081
+ const preview = {
2082
+ id: generateId(),
2083
+ screenshotPath: initialPath,
2084
+ title: args.titles?.[0] || "Overview",
2085
+ subtitle: args.subtitles?.[0] || "",
2086
+ deviceId,
2087
+ paletteId: args.paletteIds?.[0] || palettes[0],
2088
+ createdAt: new Date().toISOString(),
2089
+ };
2090
+ store.previews.push(preview);
2091
+ capturedPreviews.push(preview);
2092
+ }
2093
+ // Scroll and capture
2094
+ for (let i = 0; i < scrollCount; i++) {
2095
+ await simulatorInteract(simulator.udid, "scroll", { direction });
2096
+ await sleep(waitBetween);
2097
+ if (args.captureEach !== false) {
2098
+ const timestamp = Date.now();
2099
+ const screenshotPath = path.join(SCREENSHOTS_DIR, `scroll_${i + 1}_${timestamp}.png`);
2100
+ await execAsync(`xcrun simctl io ${simulator.udid} screenshot "${screenshotPath}"`);
2101
+ if (fs.existsSync(screenshotPath)) {
2102
+ const preview = {
2103
+ id: generateId(),
2104
+ screenshotPath,
2105
+ title: args.titles?.[i + 1] || `Section ${i + 2}`,
2106
+ subtitle: args.subtitles?.[i + 1] || "",
2107
+ deviceId,
2108
+ paletteId: args.paletteIds?.[i + 1] || palettes[(i + 1) % palettes.length],
2109
+ createdAt: new Date().toISOString(),
2110
+ };
2111
+ store.previews.push(preview);
2112
+ capturedPreviews.push(preview);
2113
+ }
2114
+ }
2115
+ }
2116
+ saveStore(store);
2117
+ return JSON.stringify({
2118
+ success: true,
2119
+ message: `Scrolled ${scrollCount} times and captured ${capturedPreviews.length} screenshots`,
2120
+ simulator: { name: simulator.name, udid: simulator.udid },
2121
+ previews: capturedPreviews.map(p => ({ title: p.title, subtitle: p.subtitle })),
2122
+ totalPreviews: store.previews.length,
2123
+ });
2124
+ }
2125
+ catch (error) {
2126
+ return JSON.stringify({
2127
+ success: false,
2128
+ error: `Scroll and capture failed: ${error.message}`,
2129
+ });
2130
+ }
2131
+ }
2132
+ // Handler: Batch capture on multiple devices
2133
+ async function handleBatchCaptureDevices(args) {
2134
+ try {
2135
+ ensureScreenshotsDir();
2136
+ const bootedSimulators = await getBootedSimulators();
2137
+ if (bootedSimulators.length === 0) {
2138
+ return JSON.stringify({
2139
+ success: false,
2140
+ error: "No iOS Simulators are running",
2141
+ });
2142
+ }
2143
+ const waitSeconds = args.waitSeconds || 3;
2144
+ const store = loadStore();
2145
+ const capturedPreviews = [];
2146
+ // If specific devices requested, filter
2147
+ let targetSimulators = bootedSimulators;
2148
+ if (args.devices && args.devices.length > 0) {
2149
+ targetSimulators = bootedSimulators.filter(s => args.devices.some(d => s.name.toLowerCase().includes(d.toLowerCase()) || s.udid === d));
2150
+ }
2151
+ if (targetSimulators.length === 0) {
2152
+ return JSON.stringify({
2153
+ success: false,
2154
+ error: "No matching simulators found",
2155
+ availableSimulators: bootedSimulators.map(s => s.name),
2156
+ });
2157
+ }
2158
+ // Capture on each device
2159
+ for (const simulator of targetSimulators) {
2160
+ // Launch app
2161
+ try {
2162
+ await execAsync(`xcrun simctl terminate ${simulator.udid} ${args.bundleId}`);
2163
+ await sleep(0.3);
2164
+ }
2165
+ catch { }
2166
+ try {
2167
+ await execAsync(`xcrun simctl launch ${simulator.udid} ${args.bundleId}`);
2168
+ }
2169
+ catch (e) {
2170
+ continue; // Skip if app not installed on this device
2171
+ }
2172
+ await sleep(waitSeconds);
2173
+ // Capture
2174
+ const timestamp = Date.now();
2175
+ const safeName = simulator.name.replace(/[^a-zA-Z0-9]/g, "_");
2176
+ const screenshotPath = path.join(SCREENSHOTS_DIR, `batch_${safeName}_${timestamp}.png`);
2177
+ await execAsync(`xcrun simctl io ${simulator.udid} screenshot "${screenshotPath}"`);
2178
+ if (fs.existsSync(screenshotPath)) {
2179
+ const preview = {
2180
+ id: generateId(),
2181
+ screenshotPath,
2182
+ title: args.title,
2183
+ subtitle: args.subtitle,
2184
+ deviceId: mapSimulatorToDeviceId(simulator.name),
2185
+ paletteId: args.paletteId || store.settings.defaultPaletteId,
2186
+ createdAt: new Date().toISOString(),
2187
+ };
2188
+ store.previews.push(preview);
2189
+ capturedPreviews.push(preview);
2190
+ }
2191
+ }
2192
+ saveStore(store);
2193
+ return JSON.stringify({
2194
+ success: true,
2195
+ message: `Captured on ${capturedPreviews.length} device(s)`,
2196
+ previews: capturedPreviews.map(p => ({
2197
+ title: p.title,
2198
+ deviceId: p.deviceId,
2199
+ })),
2200
+ totalPreviews: store.previews.length,
2201
+ });
2202
+ }
2203
+ catch (error) {
2204
+ return JSON.stringify({
2205
+ success: false,
2206
+ error: `Batch capture failed: ${error.message}`,
2207
+ });
2208
+ }
2209
+ }
2210
+ // Handler: Record and extract frames
2211
+ async function handleRecordAndExtract(args) {
2212
+ try {
2213
+ ensureScreenshotsDir();
2214
+ const simulator = await getTargetSimulator(args.simulatorUDID);
2215
+ if (!simulator) {
2216
+ return JSON.stringify({
2217
+ success: false,
2218
+ error: "No iOS Simulator is running",
2219
+ });
2220
+ }
2221
+ const duration = args.duration || 5;
2222
+ const frameCount = args.frameCount || 3;
2223
+ const palettes = ["ocean", "sunset", "forest", "lavender", "coral"];
2224
+ // Record video
2225
+ const timestamp = Date.now();
2226
+ const videoPath = path.join(SCREENSHOTS_DIR, `recording_${timestamp}.mp4`);
2227
+ // Start recording in background
2228
+ const recordProcess = exec(`xcrun simctl io ${simulator.udid} recordVideo --codec=h264 "${videoPath}"`);
2229
+ // Wait for duration
2230
+ await sleep(duration);
2231
+ // Stop recording
2232
+ recordProcess.kill("SIGINT");
2233
+ await sleep(1);
2234
+ // Check if video was created
2235
+ if (!fs.existsSync(videoPath)) {
2236
+ return JSON.stringify({
2237
+ success: false,
2238
+ error: "Failed to record video",
2239
+ });
2240
+ }
2241
+ // Extract frames using ffmpeg
2242
+ const store = loadStore();
2243
+ const capturedPreviews = [];
2244
+ const deviceId = mapSimulatorToDeviceId(simulator.name);
2245
+ try {
2246
+ // Get video duration
2247
+ const { stdout: durationInfo } = await execAsync(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}"`);
2248
+ const videoDuration = parseFloat(durationInfo.trim());
2249
+ const interval = videoDuration / (frameCount + 1);
2250
+ for (let i = 0; i < frameCount; i++) {
2251
+ const timeOffset = interval * (i + 1);
2252
+ const framePath = path.join(SCREENSHOTS_DIR, `frame_${i + 1}_${timestamp}.png`);
2253
+ await execAsync(`ffmpeg -ss ${timeOffset} -i "${videoPath}" -vframes 1 -q:v 2 "${framePath}" -y`);
2254
+ if (fs.existsSync(framePath)) {
2255
+ const preview = {
2256
+ id: generateId(),
2257
+ screenshotPath: framePath,
2258
+ title: args.titles?.[i] || `Frame ${i + 1}`,
2259
+ subtitle: args.subtitles?.[i] || "",
2260
+ deviceId,
2261
+ paletteId: palettes[i % palettes.length],
2262
+ createdAt: new Date().toISOString(),
2263
+ };
2264
+ store.previews.push(preview);
2265
+ capturedPreviews.push(preview);
2266
+ }
2267
+ }
2268
+ }
2269
+ catch (ffmpegError) {
2270
+ // ffmpeg not available, fall back to regular captures at intervals
2271
+ return JSON.stringify({
2272
+ success: false,
2273
+ error: "ffmpeg not found. Install it with: brew install ffmpeg",
2274
+ hint: "Use scroll_and_capture or auto_capture_flow as alternatives",
2275
+ });
2276
+ }
2277
+ // Clean up video file
2278
+ try {
2279
+ fs.unlinkSync(videoPath);
2280
+ }
2281
+ catch { }
2282
+ saveStore(store);
2283
+ return JSON.stringify({
2284
+ success: true,
2285
+ message: `Recorded ${duration}s video and extracted ${capturedPreviews.length} frames`,
2286
+ simulator: { name: simulator.name, udid: simulator.udid },
2287
+ previews: capturedPreviews.map(p => ({ title: p.title, subtitle: p.subtitle })),
2288
+ totalPreviews: store.previews.length,
2289
+ });
2290
+ }
2291
+ catch (error) {
2292
+ return JSON.stringify({
2293
+ success: false,
2294
+ error: `Record and extract failed: ${error.message}`,
2295
+ });
2296
+ }
2297
+ }
1003
2298
  // Main server setup
1004
2299
  const server = new Server({
1005
2300
  name: "muse-app-preview-mcp",
1006
- version: "1.2.0",
2301
+ version: "1.5.0",
1007
2302
  }, {
1008
2303
  capabilities: {
1009
2304
  tools: {},
@@ -1060,6 +2355,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1060
2355
  case "capture_app_screens":
1061
2356
  result = await handleCaptureAppScreens(args);
1062
2357
  break;
2358
+ case "create_app_previews":
2359
+ result = await handleCreateAppPreviews(args);
2360
+ break;
2361
+ // Advanced Capture Tools
2362
+ case "interact_simulator":
2363
+ result = await handleInteractSimulator(args);
2364
+ break;
2365
+ case "navigate_app":
2366
+ result = await handleNavigateApp(args);
2367
+ break;
2368
+ case "configure_simulator":
2369
+ result = await handleConfigureSimulator(args);
2370
+ break;
2371
+ case "auto_capture_flow":
2372
+ result = await handleAutoCaptureFlow(args);
2373
+ break;
2374
+ case "scroll_and_capture":
2375
+ result = await handleScrollAndCapture(args);
2376
+ break;
2377
+ case "batch_capture_devices":
2378
+ result = await handleBatchCaptureDevices(args);
2379
+ break;
2380
+ case "record_and_extract":
2381
+ result = await handleRecordAndExtract(args);
2382
+ break;
1063
2383
  default:
1064
2384
  result = JSON.stringify({ error: `Unknown tool: ${name}` });
1065
2385
  }