@playdrop/playdrop-cli 0.13.17 → 0.14.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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.13.17",
2
+ "version": "0.14.0",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.13.17",
4
+ "runtimeSdkVersion": "0.14.0",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -44,6 +44,10 @@ type HostedGameMeasurement = {
44
44
  devicePixelRatio: number;
45
45
  iframeRect: CaptureRect;
46
46
  };
47
+ type BrowserWindowBounds = {
48
+ width: number;
49
+ height: number;
50
+ };
47
51
  type CropRect = {
48
52
  x: number;
49
53
  y: number;
@@ -141,8 +145,13 @@ export declare function resolveListingCaptureBrowserContextOptions(surface: AppS
141
145
  height: number;
142
146
  }): BrowserContextOptions;
143
147
  export declare function assertExportedListingAudio(value: unknown): ExportedListingAudio;
148
+ export declare function assertListingCaptureWindowCanContainViewport(measurement: HostedGameMeasurement, windowBounds: BrowserWindowBounds): void;
144
149
  export declare function computeRecordedCrop(measurement: HostedGameMeasurement, recorder: ListingRecorderMetadata, rawWidth: number, rawHeight: number): CropRect;
145
150
  export declare function runListingRecorder(recorderPath: string, pid: number, durationSeconds: number, rawOutputPath: string, metadataPath: string, audio: boolean, deadlineAt: number): Promise<ListingRecorderMetadata>;
151
+ export declare function formatCommandError(error: Error): {
152
+ message: string;
153
+ suggestions: string[];
154
+ };
146
155
  export declare function captureCommandMatchesExceptPoster(capture: ListingCaptureSurfaceReport, surface: AppSurface, parsedOptions: ParsedCaptureListingOptions): boolean;
147
156
  export declare function resolveCurrentCaptureReuseAction(captures: ListingCaptureSurfaceReport[], surfaces: AppSurface[], parsedOptions: ParsedCaptureListingOptions): 'reuse' | 'refresh_poster';
148
157
  export declare function captureListing(targetArg: string | undefined, options?: CaptureListingOptions): Promise<void>;
@@ -47,8 +47,10 @@ exports.resolveWorkerCaptureOutputDir = resolveWorkerCaptureOutputDir;
47
47
  exports.resolveCaptureDimensions = resolveCaptureDimensions;
48
48
  exports.resolveListingCaptureBrowserContextOptions = resolveListingCaptureBrowserContextOptions;
49
49
  exports.assertExportedListingAudio = assertExportedListingAudio;
50
+ exports.assertListingCaptureWindowCanContainViewport = assertListingCaptureWindowCanContainViewport;
50
51
  exports.computeRecordedCrop = computeRecordedCrop;
51
52
  exports.runListingRecorder = runListingRecorder;
53
+ exports.formatCommandError = formatCommandError;
52
54
  exports.captureCommandMatchesExceptPoster = captureCommandMatchesExceptPoster;
53
55
  exports.resolveCurrentCaptureReuseAction = resolveCurrentCaptureReuseAction;
54
56
  exports.captureListing = captureListing;
@@ -501,11 +503,6 @@ async function fitWindowToRequestedGameplay(page, requestedWidth, requestedHeigh
501
503
  const cdpSession = await page.context().newCDPSession(page);
502
504
  let measurement = await waitForHostedGameMeasurement(page, 15000);
503
505
  for (let attempt = 0; attempt < 5; attempt += 1) {
504
- const deltaWidth = requestedWidth - Math.round(measurement.iframeRect.width);
505
- const deltaHeight = requestedHeight - Math.round(measurement.iframeRect.height);
506
- if (Math.abs(deltaWidth) <= 1 && Math.abs(deltaHeight) <= 1) {
507
- return measurement;
508
- }
509
506
  const windowState = await cdpSession.send('Browser.getWindowForTarget');
510
507
  const currentWidth = typeof windowState.bounds.width === 'number'
511
508
  ? windowState.bounds.width
@@ -513,6 +510,15 @@ async function fitWindowToRequestedGameplay(page, requestedWidth, requestedHeigh
513
510
  const currentHeight = typeof windowState.bounds.height === 'number'
514
511
  ? windowState.bounds.height
515
512
  : Math.round(measurement.outerHeight);
513
+ const deltaWidth = requestedWidth - Math.round(measurement.iframeRect.width);
514
+ const deltaHeight = requestedHeight - Math.round(measurement.iframeRect.height);
515
+ if (Math.abs(deltaWidth) <= 1 && Math.abs(deltaHeight) <= 1) {
516
+ assertListingCaptureWindowCanContainViewport(measurement, {
517
+ width: currentWidth,
518
+ height: currentHeight,
519
+ });
520
+ return measurement;
521
+ }
516
522
  const nextWidth = Math.max(400, currentWidth + deltaWidth);
517
523
  const nextHeight = Math.max(300, currentHeight + deltaHeight);
518
524
  await cdpSession.send('Browser.setWindowBounds', {
@@ -527,6 +533,15 @@ async function fitWindowToRequestedGameplay(page, requestedWidth, requestedHeigh
527
533
  }
528
534
  throw new Error('hosted_game_resize_failed');
529
535
  }
536
+ function assertListingCaptureWindowCanContainViewport(measurement, windowBounds) {
537
+ const requiredWidth = Math.ceil(measurement.innerWidth);
538
+ const requiredHeight = Math.ceil(measurement.innerHeight);
539
+ const availableWidth = Math.floor(windowBounds.width);
540
+ const availableHeight = Math.floor(windowBounds.height);
541
+ if (availableWidth + 1 < requiredWidth || availableHeight + 1 < requiredHeight) {
542
+ throw new Error(`listing_capture_dimensions_exceed_display:${requiredWidth}x${requiredHeight}:${availableWidth}x${availableHeight}`);
543
+ }
544
+ }
530
545
  function roundCropValue(value) {
531
546
  return Math.round(value);
532
547
  }
@@ -726,6 +741,13 @@ function formatCommandError(error) {
726
741
  suggestions: ['Fix or remove the incomplete canonical capture directory, then record the runtime once.'],
727
742
  };
728
743
  }
744
+ if (error.message.startsWith('listing_capture_dimensions_exceed_display:')) {
745
+ const [, requested = 'unknown', available = 'unknown'] = error.message.split(':');
746
+ return {
747
+ message: `Requested capture size ${requested} cannot fit in this worker's ${available} browser window.`,
748
+ suggestions: ['Omit --width and --height to use the safe surface defaults, or request dimensions that fit this display.'],
749
+ };
750
+ }
729
751
  if (error.message.startsWith('listing_recorder_failed:')) {
730
752
  return {
731
753
  message: error.message.slice('listing_recorder_failed:'.length),
@@ -1195,14 +1217,14 @@ async function captureListing(targetArg, options = {}) {
1195
1217
  await prepareHostedListingScene(browserHandle.page, captureSceneId);
1196
1218
  await browserHandle.page.waitForTimeout(1000);
1197
1219
  }
1220
+ const measurement = await fitWindowToRequestedGameplay(browserHandle.page, dimensions.width, dimensions.height);
1221
+ console.log(`[listing] Gameplay frame ${Math.round(measurement.iframeRect.width)}x${Math.round(measurement.iframeRect.height)} in window ${Math.round(measurement.outerWidth)}x${Math.round(measurement.outerHeight)}.`);
1198
1222
  if (shouldExportPreviewAudio) {
1199
1223
  console.log('[listing] Checking the in-app preview audio export contract.');
1200
1224
  await assertHostedListingAudioCaptureContract(browserHandle.page, outputPaths.outputDir);
1201
1225
  console.log('[listing] Starting in-app preview audio capture.');
1202
1226
  await startHostedListingAudioCapture(browserHandle.page);
1203
1227
  }
1204
- const measurement = await fitWindowToRequestedGameplay(browserHandle.page, dimensions.width, dimensions.height);
1205
- console.log(`[listing] Gameplay frame ${Math.round(measurement.iframeRect.width)}x${Math.round(measurement.iframeRect.height)} in window ${Math.round(measurement.outerWidth)}x${Math.round(measurement.outerHeight)}.`);
1206
1228
  await browserHandle.page.waitForTimeout(750);
1207
1229
  console.log('[listing] Waiting for the machine capture lease.');
1208
1230
  const captureLease = await (0, listingCaptureLease_1.acquireListingCaptureLease)({
@@ -2584,7 +2584,8 @@ function buildAgentFailureCode(agent, result, request) {
2584
2584
  if (providerStatus === 529 || (/\b529\b/.test(combinedOutput) && /\boverloaded?\b/i.test(combinedOutput))) {
2585
2585
  return `agent_provider_overloaded:${provider}:529`;
2586
2586
  }
2587
- if (/\boverloaded?\b/i.test(combinedOutput)) {
2587
+ if (/\boverloaded?\b/i.test(combinedOutput)
2588
+ || /\b(?:selected\s+)?model\s+is\s+at\s+capacity\b/i.test(combinedOutput)) {
2588
2589
  return `agent_provider_overloaded:${provider}`;
2589
2590
  }
2590
2591
  if (providerStatus === 429
@@ -5427,7 +5428,10 @@ async function materialTask(options) {
5427
5428
  }
5428
5429
  catch (error) {
5429
5430
  const message = error instanceof Error ? error.message : String(error);
5430
- console.warn(`Warning: task material was not shared: ${message}`);
5431
+ const suggestion = message.startsWith('task_material_asset_background_not_removed:')
5432
+ ? ' Remove the background and retry with --asset, or omit --asset if the image is honestly useful as a full frame.'
5433
+ : '';
5434
+ console.warn(`Warning: task material was not shared: ${message}.${suggestion}`);
5431
5435
  node_process_1.default.exitCode = 0;
5432
5436
  }
5433
5437
  }
@@ -117,13 +117,13 @@ function assertListingScreenshots(task) {
117
117
  const portraitPaths = task.listing?.screenshotPortraitPaths ?? [];
118
118
  const landscapePaths = task.listing?.screenshotLandscapePaths ?? [];
119
119
  if (portraitPaths.length === 0 && landscapePaths.length === 0) {
120
- throw new Error('agent_task_listing_screenshots_missing: new game tasks must include listing.screenshotsPortrait and/or listing.screenshotsLandscape PNG captures.');
120
+ throw new Error('agent_task_listing_screenshots_missing: new game tasks must include listing.screenshotsPortrait and/or listing.screenshotsLandscape PNG marketing images.');
121
121
  }
122
122
  if (task.surfaceTargets.includes('MOBILE_PORTRAIT') && portraitPaths.length === 0) {
123
- throw new Error('agent_task_listing_screenshots_missing: mobile portrait games must include at least one listing.screenshotsPortrait PNG capture.');
123
+ throw new Error('agent_task_listing_screenshots_missing: mobile portrait games must include at least one listing.screenshotsPortrait PNG marketing image.');
124
124
  }
125
125
  if ((task.surfaceTargets.includes('DESKTOP') || task.surfaceTargets.includes('MOBILE_LANDSCAPE')) && landscapePaths.length === 0) {
126
- throw new Error('agent_task_listing_screenshots_missing: desktop or landscape games must include at least one listing.screenshotsLandscape PNG capture.');
126
+ throw new Error('agent_task_listing_screenshots_missing: desktop or landscape games must include at least one listing.screenshotsLandscape PNG marketing image.');
127
127
  }
128
128
  for (const filePath of portraitPaths) {
129
129
  assertScreenshotPath(task, filePath, 'listing.screenshotsPortrait');
@@ -137,10 +137,8 @@ function assertListingScreenshots(task) {
137
137
  function isSha256Hex(value) {
138
138
  return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value.trim());
139
139
  }
140
- function isCaptureMediaFileKey(fileKey) {
141
- return fileKey.startsWith('screenshotsPortrait:')
142
- || fileKey.startsWith('screenshotsLandscape:')
143
- || fileKey.startsWith('videosPortrait:')
140
+ function isCaptureVideoFileKey(fileKey) {
141
+ return fileKey.startsWith('videosPortrait:')
144
142
  || fileKey.startsWith('videosLandscape:');
145
143
  }
146
144
  function readMediaCaptureProof(input) {
@@ -175,20 +173,21 @@ function readMediaCaptureProof(input) {
175
173
  if (!rawArtifactHashes || typeof rawArtifactHashes !== 'object' || Array.isArray(rawArtifactHashes)) {
176
174
  throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes must be an object.`);
177
175
  }
178
- const captureArtifactHashEntries = Object.values(rawArtifactHashes)
179
- .map((value) => typeof value === 'string' ? value.trim().toLowerCase() : '')
176
+ const captureArtifactHashEntries = Object.entries(rawArtifactHashes)
177
+ .filter(([artifactKey]) => (0, node_path_1.extname)(artifactKey).toLowerCase() === '.mp4')
178
+ .map(([, value]) => typeof value === 'string' ? value.trim().toLowerCase() : '')
180
179
  .filter((value) => isSha256Hex(value));
181
180
  const captureArtifactHashes = new Set(captureArtifactHashEntries);
182
181
  if (captureArtifactHashEntries.length === 0) {
183
- throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes is empty.`);
182
+ throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes must include at least one MP4 gameplay video.`);
184
183
  }
185
184
  const captureArtifactHashCounts = new Map();
186
185
  for (const hash of captureArtifactHashEntries) {
187
186
  captureArtifactHashCounts.set(hash, (captureArtifactHashCounts.get(hash) ?? 0) + 1);
188
187
  }
189
- const mediaFiles = input.preparedSessionFiles.filter((file) => isCaptureMediaFileKey(file.fileKey));
188
+ const mediaFiles = input.preparedSessionFiles.filter((file) => isCaptureVideoFileKey(file.fileKey));
190
189
  if (mediaFiles.length === 0) {
191
- throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload recorder stills and video in listing.screenshots* and listing.videos*.`);
190
+ throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload recorder gameplay video in listing.videos*.`);
192
191
  }
193
192
  const uploadedMediaHashCounts = new Map();
194
193
  for (const file of mediaFiles) {
@@ -197,21 +196,19 @@ function readMediaCaptureProof(input) {
197
196
  const missingUploadedCaptureArtifacts = [...captureArtifactHashCounts.entries()]
198
197
  .filter(([hash, expectedCount]) => (uploadedMediaHashCounts.get(hash) ?? 0) < expectedCount);
199
198
  if (missingUploadedCaptureArtifacts.length > 0) {
200
- throw new Error(`agent_task_media_capture_artifact_missing_from_listing: ${input.task.name} must list every recorder poster and video from listing.captureReport in catalogue.json.`);
199
+ throw new Error(`agent_task_media_capture_artifact_missing_from_listing: ${input.task.name} must list every recorder gameplay video from listing.captureReport in catalogue.json.`);
201
200
  }
202
- let hasStill = false;
203
201
  let hasVideo = false;
204
202
  const artifactHashes = {};
205
203
  for (const file of mediaFiles) {
206
204
  if (!captureArtifactHashes.has(file.sha256)) {
207
205
  throw new Error(`agent_task_media_capture_artifact_mismatch: ${file.fileKey} was not produced by listing.captureReport for ${input.task.name}.`);
208
206
  }
209
- hasStill = hasStill || file.fileKey.startsWith('screenshots');
210
207
  hasVideo = hasVideo || file.fileKey.startsWith('videos');
211
208
  artifactHashes[file.fileKey] = file.sha256;
212
209
  }
213
- if (!hasStill || !hasVideo) {
214
- throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload at least one recorder still and one recorder video.`);
210
+ if (!hasVideo) {
211
+ throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload at least one recorder gameplay video.`);
215
212
  }
216
213
  const capturedSurfaces = Array.isArray(binding.capturedSurfaces)
217
214
  ? Array.from(new Set(binding.capturedSurfaces
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.13.17",
2
+ "version": "0.14.0",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.13.17",
4
+ "runtimeSdkVersion": "0.14.0",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playdrop/playdrop-cli",
3
- "version": "0.13.17",
3
+ "version": "0.14.0",
4
4
  "description": "Official Playdrop CLI for publishing browser games, creator apps, and AI-generated game assets on playdrop.ai",
5
5
  "homepage": "https://www.playdrop.ai",
6
6
  "repository": {