autokap 1.3.4 → 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
  }
@@ -249,7 +283,8 @@ program
249
283
  }
250
284
  if (opts.cloud) {
251
285
  process.env.AUTOKAP_CLOUD_RUNNER = '1';
252
- logger.info('[capture] Cloud runner mode Linux FPS cap lifted (clips target 30 fps)');
286
+ const runnerVersion = process.env.AUTOKAP_CLI_VERSION ?? version;
287
+ logger.info(`[capture] Cloud runner mode — CLI ${runnerVersion}; Linux FPS cap lifted (clips target 30 fps)`);
253
288
  }
254
289
  if (opts.local) {
255
290
  process.env[API_BASE_URL_ENV_VAR] = LOCAL_API_BASE_URL;
@@ -265,6 +300,12 @@ program
265
300
  const checkpointUrl = cloudRunId
266
301
  ? buildApiUrl(config, `/api/cli/cloud-recapture/${cloudRunId}/checkpoint`)
267
302
  : null;
303
+ if (opts.cloud && cloudRunId) {
304
+ logger.info(`[auto-recapture] Cloud progress callbacks enabled for run ${cloudRunId}`);
305
+ }
306
+ else if (opts.cloud) {
307
+ logger.warn('[auto-recapture] Cloud progress disabled: AUTOKAP_RUN_ID is not set');
308
+ }
268
309
  let lastProgressCheckpointAt = 0;
269
310
  const postCloudCheckpoint = async (body, options = {}) => {
270
311
  if (!checkpointUrl)
@@ -322,7 +363,7 @@ program
322
363
  totalPresets: data.presets.length,
323
364
  completedPresets: 0,
324
365
  failedPresets: 0,
325
- message: `planned ${data.presets.length} preset(s)`,
366
+ message: `Run planned: ${data.presets.length} preset(s)`,
326
367
  });
327
368
  if (data.presets.length === 0) {
328
369
  logger.info(`[auto-recapture] No presets enabled for project ${opts.project}`);
@@ -336,9 +377,10 @@ program
336
377
  totalPresets: data.presets.length,
337
378
  completedPresets: 0,
338
379
  failedPresets: 0,
339
- message: `starting ${data.presets.length} preset(s)`,
380
+ message: `Runner started: ${data.presets.length} preset(s) to capture`,
340
381
  });
341
382
  for (const [index, preset] of data.presets.entries()) {
383
+ const presetDisplayName = displayPresetName(preset);
342
384
  const label = preset.name ? `${preset.name} (${preset.id})` : preset.id;
343
385
  logger.info(`[auto-recapture] Running ${label}`);
344
386
  await postCloudCheckpoint({
@@ -349,7 +391,7 @@ program
349
391
  totalPresets: data.presets.length,
350
392
  completedPresets: index,
351
393
  failedPresets: failures.length,
352
- message: `running ${preset.name ?? preset.id}`,
394
+ message: `Preset started: ${presetDisplayName}`,
353
395
  });
354
396
  const result = await runCapture({
355
397
  presetId: preset.id,
@@ -357,23 +399,21 @@ program
357
399
  headed: opts.headed,
358
400
  allowUploadFailure: opts.allowUploadFailure,
359
401
  onProgress: (event) => {
360
- const checkpointType = event.type === 'upload_start'
361
- ? 'upload_start'
362
- : event.type === 'upload_end'
363
- ? 'upload_end'
364
- : 'preset_progress';
402
+ const checkpoint = cloudCaptureProgressCheckpoint(event);
403
+ if (!checkpoint)
404
+ return;
365
405
  void postCloudCheckpoint({
366
- type: checkpointType,
406
+ type: checkpoint.type,
367
407
  presetId: preset.id,
368
408
  presetName: preset.name ?? null,
369
409
  presetIndex: index,
370
410
  totalPresets: data.presets.length,
371
411
  completedPresets: index,
372
412
  failedPresets: failures.length,
373
- message: event.message,
413
+ message: checkpoint.message,
374
414
  progressEvent: event,
375
- status: event.status === 'failed' ? 'running' : undefined,
376
- }, { throttle: checkpointType === 'preset_progress' });
415
+ status: checkpoint.status,
416
+ });
377
417
  },
378
418
  });
379
419
  const childRunId = result.runId;
@@ -392,7 +432,7 @@ program
392
432
  childRunId,
393
433
  status: 'failed',
394
434
  errorMessage: error,
395
- message: `failed ${preset.name ?? preset.id}`,
435
+ message: `Preset failed: ${presetDisplayName}`,
396
436
  });
397
437
  }
398
438
  else {
@@ -406,7 +446,7 @@ program
406
446
  failedPresets: failures.length,
407
447
  childRunId,
408
448
  status: 'completed',
409
- message: `completed ${preset.name ?? preset.id}`,
449
+ message: `Preset completed: ${presetDisplayName}`,
410
450
  });
411
451
  }
412
452
  }
@@ -438,7 +478,7 @@ program
438
478
  completedPresets: data.presets.length,
439
479
  failedPresets: 0,
440
480
  status: 'completed',
441
- message: 'cloud recapture completed',
481
+ message: 'Cloud recapture completed',
442
482
  });
443
483
  await notifyCloudCallback('completed', {
444
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.4",
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",