autokap 1.3.5 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.d.ts CHANGED
@@ -96,6 +96,7 @@ export declare class Browser {
96
96
  private browser;
97
97
  private context;
98
98
  private page;
99
+ private nativeVideoStartedAt;
99
100
  private elementMap;
100
101
  private akNodeIndex;
101
102
  private poolContext;
@@ -116,7 +117,9 @@ export declare class Browser {
116
117
  * `--window-size`) so the captured frames match viewport × DSF pixels, and
117
118
  * the cursor overlay script so clicks/hover moments are visible.
118
119
  */
119
- static forClipCapture(options: BrowserOptions, cursorScript: string): Promise<Browser>;
120
+ static forClipCapture(options: BrowserOptions, cursorScript: string, recording?: {
121
+ nativeVideoDir?: string;
122
+ }): Promise<Browser>;
120
123
  /**
121
124
  * Close only the browser context (not the browser process).
122
125
  * Used by clip capture to release the context promptly after the CDP loop
@@ -332,6 +335,7 @@ export declare class Browser {
332
335
  resizeViewport(width: number, height: number): Promise<void>;
333
336
  get currentPage(): Page;
334
337
  get browserContext(): BrowserContext;
338
+ get nativeVideoStartTime(): number | null;
335
339
  /**
336
340
  * Observation pass for mock data generation.
337
341
  * Navigates to the given URL, waits for network idle, and records all JSON API responses.
package/dist/browser.js CHANGED
@@ -772,6 +772,7 @@ export class Browser {
772
772
  browser = null;
773
773
  context = null;
774
774
  page = null;
775
+ nativeVideoStartedAt = null;
775
776
  elementMap = new Map();
776
777
  akNodeIndex = new Map();
777
778
  poolContext = false;
@@ -804,7 +805,7 @@ export class Browser {
804
805
  * `--window-size`) so the captured frames match viewport × DSF pixels, and
805
806
  * the cursor overlay script so clicks/hover moments are visible.
806
807
  */
807
- static async forClipCapture(options, cursorScript) {
808
+ static async forClipCapture(options, cursorScript, recording) {
808
809
  const instance = new Browser(options);
809
810
  const deviceScaleFactor = normalizeDeviceScaleFactor(options.deviceScaleFactor);
810
811
  // Enable GPU compositor on non-Linux platforms so Chrome can render
@@ -832,13 +833,23 @@ export class Browser {
832
833
  headless: !options.headed,
833
834
  args: clipArgs,
834
835
  });
835
- instance.context = await instance.browser.newContext({
836
+ const contextOptions = {
836
837
  viewport: options.viewport,
837
838
  deviceScaleFactor,
838
839
  locale: langToLocale(options.lang ?? 'en'),
839
840
  colorScheme: options.colorScheme ?? 'light',
840
841
  storageState: options.storageState,
841
- });
842
+ };
843
+ if (recording?.nativeVideoDir) {
844
+ contextOptions.recordVideo = {
845
+ dir: recording.nativeVideoDir,
846
+ size: {
847
+ width: Math.round(options.viewport.width),
848
+ height: Math.round(options.viewport.height),
849
+ },
850
+ };
851
+ }
852
+ instance.context = await instance.browser.newContext(contextOptions);
842
853
  // Inject cursor overlay at context level — survives all navigations in this session
843
854
  await instance.context.addInitScript(cursorScript);
844
855
  // Also hide dev/prototype chrome on every document, including navigations
@@ -863,7 +874,11 @@ export class Browser {
863
874
  document.addEventListener('DOMContentLoaded', install, { once: true });
864
875
  }
865
876
  }, { styleId: CAPTURE_HIDE_STYLE_ID, css: getCaptureHideCSS() });
877
+ const nativeVideoStartedAt = Date.now();
866
878
  instance.page = await instance.context.newPage();
879
+ if (recording?.nativeVideoDir) {
880
+ instance.nativeVideoStartedAt = nativeVideoStartedAt;
881
+ }
867
882
  return instance;
868
883
  }
869
884
  /**
@@ -5043,6 +5058,9 @@ export class Browser {
5043
5058
  get browserContext() {
5044
5059
  return this.ensureContext();
5045
5060
  }
5061
+ get nativeVideoStartTime() {
5062
+ return this.nativeVideoStartedAt;
5063
+ }
5046
5064
  /**
5047
5065
  * Observation pass for mock data generation.
5048
5066
  * Navigates to the given URL, waits for network idle, and records all JSON API responses.
@@ -241,7 +241,13 @@ export async function runCapture(options) {
241
241
  }
242
242
  if (recordable) {
243
243
  recordingDir = await fs.mkdtemp(path.join(os.tmpdir(), `autokap-${program.mediaMode}-`));
244
- browser = await Browser.forClipCapture(browserOptions, buildCursorOverlayScript(program.artifactPlan.cursorTheme ?? 'minimal'));
244
+ const nativeCloudClipRecording = program.mediaMode === 'clip'
245
+ && process.env.AUTOKAP_CLOUD_RUNNER === '1'
246
+ && process.env.AUTOKAP_CLIP_RECORDER !== 'cdp';
247
+ if (nativeCloudClipRecording) {
248
+ logger.info('[capture] Cloud clip recorder: native Playwright video (CDP screenshot loop disabled)');
249
+ }
250
+ browser = await Browser.forClipCapture(browserOptions, buildCursorOverlayScript(program.artifactPlan.cursorTheme ?? 'minimal'), nativeCloudClipRecording ? { nativeVideoDir: recordingDir } : undefined);
245
251
  }
246
252
  else if (browserOptions.headed) {
247
253
  // Headed mode: standalone browser (pool is always headless)
package/dist/cli.js CHANGED
@@ -79,6 +79,40 @@ function ensureExportFormat(format) {
79
79
  function printJson(value) {
80
80
  console.log(JSON.stringify(value, null, 2));
81
81
  }
82
+ function displayPresetName(preset) {
83
+ const name = preset.name?.trim();
84
+ return name && name.length > 0 ? name : preset.id;
85
+ }
86
+ function cloudCaptureProgressCheckpoint(event) {
87
+ switch (event.type) {
88
+ case 'variant_start':
89
+ return {
90
+ type: 'preset_progress',
91
+ message: `Capture started: ${event.variantId}`,
92
+ };
93
+ case 'variant_end':
94
+ return {
95
+ type: 'preset_progress',
96
+ message: event.status === 'ok'
97
+ ? `Capture completed: ${event.variantId}`
98
+ : `Capture failed: ${event.variantId}`,
99
+ status: event.status === 'failed' ? 'running' : undefined,
100
+ };
101
+ case 'upload_start':
102
+ return {
103
+ type: 'upload_start',
104
+ message: 'Export started',
105
+ };
106
+ case 'upload_end':
107
+ return {
108
+ type: 'upload_end',
109
+ message: event.status === 'failed' ? 'Export failed' : 'Export completed',
110
+ status: event.status === 'failed' ? 'running' : undefined,
111
+ };
112
+ default:
113
+ return null;
114
+ }
115
+ }
82
116
  function buildEndpointAssetUrl(config, endpointId) {
83
117
  return `${config.apiBaseUrl}/api/v1/assets/${endpointId}`;
84
118
  }
@@ -329,7 +363,7 @@ program
329
363
  totalPresets: data.presets.length,
330
364
  completedPresets: 0,
331
365
  failedPresets: 0,
332
- message: `planned ${data.presets.length} preset(s)`,
366
+ message: `Run planned: ${data.presets.length} preset(s)`,
333
367
  });
334
368
  if (data.presets.length === 0) {
335
369
  logger.info(`[auto-recapture] No presets enabled for project ${opts.project}`);
@@ -343,9 +377,10 @@ program
343
377
  totalPresets: data.presets.length,
344
378
  completedPresets: 0,
345
379
  failedPresets: 0,
346
- message: `starting ${data.presets.length} preset(s)`,
380
+ message: `Runner started: ${data.presets.length} preset(s) to capture`,
347
381
  });
348
382
  for (const [index, preset] of data.presets.entries()) {
383
+ const presetDisplayName = displayPresetName(preset);
349
384
  const label = preset.name ? `${preset.name} (${preset.id})` : preset.id;
350
385
  logger.info(`[auto-recapture] Running ${label}`);
351
386
  await postCloudCheckpoint({
@@ -356,7 +391,7 @@ program
356
391
  totalPresets: data.presets.length,
357
392
  completedPresets: index,
358
393
  failedPresets: failures.length,
359
- message: `running ${preset.name ?? preset.id}`,
394
+ message: `Preset started: ${presetDisplayName}`,
360
395
  });
361
396
  const result = await runCapture({
362
397
  presetId: preset.id,
@@ -364,23 +399,21 @@ program
364
399
  headed: opts.headed,
365
400
  allowUploadFailure: opts.allowUploadFailure,
366
401
  onProgress: (event) => {
367
- const checkpointType = event.type === 'upload_start'
368
- ? 'upload_start'
369
- : event.type === 'upload_end'
370
- ? 'upload_end'
371
- : 'preset_progress';
402
+ const checkpoint = cloudCaptureProgressCheckpoint(event);
403
+ if (!checkpoint)
404
+ return;
372
405
  void postCloudCheckpoint({
373
- type: checkpointType,
406
+ type: checkpoint.type,
374
407
  presetId: preset.id,
375
408
  presetName: preset.name ?? null,
376
409
  presetIndex: index,
377
410
  totalPresets: data.presets.length,
378
411
  completedPresets: index,
379
412
  failedPresets: failures.length,
380
- message: event.message,
413
+ message: checkpoint.message,
381
414
  progressEvent: event,
382
- status: event.status === 'failed' ? 'running' : undefined,
383
- }, { throttle: checkpointType === 'preset_progress' });
415
+ status: checkpoint.status,
416
+ });
384
417
  },
385
418
  });
386
419
  const childRunId = result.runId;
@@ -399,7 +432,7 @@ program
399
432
  childRunId,
400
433
  status: 'failed',
401
434
  errorMessage: error,
402
- message: `failed ${preset.name ?? preset.id}`,
435
+ message: `Preset failed: ${presetDisplayName}`,
403
436
  });
404
437
  }
405
438
  else {
@@ -413,7 +446,7 @@ program
413
446
  failedPresets: failures.length,
414
447
  childRunId,
415
448
  status: 'completed',
416
- message: `completed ${preset.name ?? preset.id}`,
449
+ message: `Preset completed: ${presetDisplayName}`,
417
450
  });
418
451
  }
419
452
  }
@@ -445,7 +478,7 @@ program
445
478
  completedPresets: data.presets.length,
446
479
  failedPresets: 0,
447
480
  status: 'completed',
448
- message: 'cloud recapture completed',
481
+ message: 'Cloud recapture completed',
449
482
  });
450
483
  await notifyCloudCallback('completed', {
451
484
  totalPresets: data.presets.length,
@@ -352,21 +352,32 @@ export class WebPlaywrightLocal {
352
352
  const cloudClipFps = isCloudRunner ? 30 : defaultFps;
353
353
  const targetFps = options.captureFps
354
354
  ?? (options.mediaMode === 'video' ? 30 : cloudClipFps);
355
- const loop = new ClipCaptureLoop({
356
- page,
357
- framesDir,
358
- targetFps,
359
- // Cloud runners have CPU headroom — drop the Linux 50 ms idle cushion
360
- // (sized for tight CI runners) to let the loop stay close to its target.
361
- minRestMs: process.platform === 'linux' && !isCloudRunner ? 50 : 16,
362
- });
363
- await loop.start();
355
+ const nativeClipVideo = options.mediaMode === 'clip' && isCloudRunner
356
+ ? page.video()
357
+ : null;
358
+ let loop = null;
359
+ if (nativeClipVideo) {
360
+ logger.info('[capture] Native cloud clip recorder enabled using Playwright video stream instead of CDP screenshots');
361
+ }
362
+ else {
363
+ loop = new ClipCaptureLoop({
364
+ page,
365
+ framesDir,
366
+ targetFps,
367
+ // Cloud runners have CPU headroom — drop the Linux 50 ms idle cushion
368
+ // (sized for tight CI runners) to let the loop stay close to its target.
369
+ minRestMs: process.platform === 'linux' && !isCloudRunner ? 50 : 16,
370
+ });
371
+ await loop.start();
372
+ }
364
373
  this.recording = {
365
374
  mediaMode: options.mediaMode,
366
375
  startedAt: Date.now(),
367
376
  framesDir,
368
377
  mp4Path: path.join(baseDir, `${options.mediaMode}.mp4`),
369
378
  loop,
379
+ nativeVideo: nativeClipVideo,
380
+ nativeVideoStartedAt: this.browser.nativeVideoStartTime,
370
381
  finalized: false,
371
382
  };
372
383
  this.clipCursor = {
@@ -446,20 +457,47 @@ export class WebPlaywrightLocal {
446
457
  throw new Error('recording was not started');
447
458
  }
448
459
  if (this.recording.finalized) {
449
- const buffer = await fs.readFile(this.recording.mp4Path);
450
- const durationMs = this.recording.encodedDurationMs ?? await getMediaDurationMs(this.recording.mp4Path);
460
+ const sourcePath = this.recording.sourcePath ?? this.recording.mp4Path;
461
+ const buffer = await fs.readFile(sourcePath);
462
+ const durationMs = this.recording.encodedDurationMs ?? await getMediaDurationMs(sourcePath);
451
463
  this.recording.encodedDurationMs = durationMs;
452
464
  return {
453
465
  buffer,
454
466
  durationMs,
455
- mimeType: 'video/mp4',
456
- trimStartMs: this.recording.result?.trimStartMs ?? 0,
467
+ mimeType: this.recording.sourceMimeType ?? 'video/mp4',
468
+ trimStartMs: this.recording.trimStartMs ?? this.recording.result?.trimStartMs ?? 0,
457
469
  };
458
470
  }
459
471
  if (this.recordingNavWatcher) {
460
472
  this.recordingNavWatcher.detach();
461
473
  this.recordingNavWatcher = null;
462
474
  }
475
+ if (this.recording.nativeVideo) {
476
+ const video = this.recording.nativeVideo;
477
+ const trimStartMs = Math.max(0, this.recording.startedAt
478
+ - (this.recording.nativeVideoStartedAt ?? this.sessionStartedAt));
479
+ await this.browser.closeContext();
480
+ const videoPath = await video.path();
481
+ const durationMs = await getMediaDurationMs(videoPath);
482
+ logger.info(`[capture] Native clip recording finalized: source ${durationMs}ms, ` +
483
+ `trim start ${Math.round(trimStartMs)}ms`);
484
+ this.recording.finalized = true;
485
+ this.recording.sourcePath = videoPath;
486
+ this.recording.sourceMimeType = 'video/webm';
487
+ this.recording.trimStartMs = trimStartMs;
488
+ this.recording.encodedDurationMs = durationMs;
489
+ this.clipCursor = null;
490
+ const buffer = await fs.readFile(videoPath);
491
+ return {
492
+ buffer,
493
+ durationMs,
494
+ mimeType: 'video/webm',
495
+ trimStartMs,
496
+ };
497
+ }
498
+ if (!this.recording.loop) {
499
+ throw new Error('recording loop was not initialized');
500
+ }
463
501
  const result = await this.recording.loop.stop();
464
502
  logger.info(`[capture] Clip frame capture: ${result.frameCount} frame(s), ` +
465
503
  `${result.measuredFps.toFixed(1)} fps over ${(result.actualDurationMs / 1000).toFixed(2)}s ` +
@@ -475,6 +513,8 @@ export class WebPlaywrightLocal {
475
513
  });
476
514
  this.recording.finalized = true;
477
515
  this.recording.result = result;
516
+ this.recording.sourcePath = this.recording.mp4Path;
517
+ this.recording.sourceMimeType = 'video/mp4';
478
518
  this.recording.encodedDurationMs = await getMediaDurationMs(this.recording.mp4Path);
479
519
  this.clipCursor = null;
480
520
  const buffer = await fs.readFile(this.recording.mp4Path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autokap",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "description": "AI-powered CLI tool for capturing clean screenshots of websites",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",