autokap 1.3.5 → 1.3.7

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
@@ -110,7 +110,9 @@ export declare class Browser {
110
110
  * Create a Browser dedicated to clip capture. Frames are pulled via CDP
111
111
  * `Page.captureScreenshot` in a tight loop by `ClipCaptureLoop` — NOT via
112
112
  * Playwright's built-in `recordVideo` (which plateaus at 27 FPS with ~12%
113
- * duplicates due to the CDP screencast throttler).
113
+ * duplicates due to the CDP screencast throttler, and on cloud Linux runs
114
+ * the WebM encoder on the compositor thread → frame drops to ~4 fps under
115
+ * software rasterization).
114
116
  *
115
117
  * Preserves the HiDPI rendering path (`--force-device-scale-factor` +
116
118
  * `--window-size`) so the captured frames match viewport × DSF pixels, and
package/dist/browser.js CHANGED
@@ -798,7 +798,9 @@ export class Browser {
798
798
  * Create a Browser dedicated to clip capture. Frames are pulled via CDP
799
799
  * `Page.captureScreenshot` in a tight loop by `ClipCaptureLoop` — NOT via
800
800
  * Playwright's built-in `recordVideo` (which plateaus at 27 FPS with ~12%
801
- * duplicates due to the CDP screencast throttler).
801
+ * duplicates due to the CDP screencast throttler, and on cloud Linux runs
802
+ * the WebM encoder on the compositor thread → frame drops to ~4 fps under
803
+ * software rasterization).
802
804
  *
803
805
  * Preserves the HiDPI rendering path (`--force-device-scale-factor` +
804
806
  * `--window-size`) so the captured frames match viewport × DSF pixels, and
@@ -832,13 +834,14 @@ export class Browser {
832
834
  headless: !options.headed,
833
835
  args: clipArgs,
834
836
  });
835
- instance.context = await instance.browser.newContext({
837
+ const contextOptions = {
836
838
  viewport: options.viewport,
837
839
  deviceScaleFactor,
838
840
  locale: langToLocale(options.lang ?? 'en'),
839
841
  colorScheme: options.colorScheme ?? 'light',
840
842
  storageState: options.storageState,
841
- });
843
+ };
844
+ instance.context = await instance.browser.newContext(contextOptions);
842
845
  // Inject cursor overlay at context level — survives all navigations in this session
843
846
  await instance.context.addInitScript(cursorScript);
844
847
  // Also hide dev/prototype chrome on every document, including navigations
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
  }
@@ -282,19 +316,31 @@ program
282
316
  return;
283
317
  lastProgressCheckpointAt = now;
284
318
  }
319
+ // Surface failures loudly to stderr (logger.error → fly.io machine logs).
320
+ // Cloud→cloud HTTP can fail silently for many reasons (DNS, firewall,
321
+ // expired token, dashboard cold-start) and dropping a single checkpoint
322
+ // produces a "stuck progress bar" symptom on the dashboard with no clue.
323
+ // The URL + status + error code are the minimum needed to debug from
324
+ // `flyctl logs` after the fact.
325
+ const checkpointType = typeof body.type === 'string' ? body.type : 'unknown';
285
326
  try {
286
327
  const response = await fetch(checkpointUrl, {
287
328
  method: 'POST',
288
329
  headers: { ...authHeaders(config), 'Content-Type': 'application/json' },
289
330
  body: JSON.stringify(body),
331
+ signal: AbortSignal.timeout(15_000),
290
332
  });
291
333
  if (!response.ok) {
292
334
  const bodyText = await response.text().catch(() => response.statusText);
293
- logger.warn(`[auto-recapture] Cloud checkpoint non-OK (${response.status}): ${bodyText}`);
335
+ logger.error(`[auto-recapture] Cloud checkpoint POST failed: status=${response.status} ` +
336
+ `type=${checkpointType} url=${checkpointUrl} body=${bodyText.slice(0, 200)}`);
294
337
  }
295
338
  }
296
339
  catch (err) {
297
- logger.warn(`[auto-recapture] Cloud checkpoint failed (best-effort): ${err.message}`);
340
+ const error = err;
341
+ const code = error.code ?? error.name ?? 'unknown';
342
+ logger.error(`[auto-recapture] Cloud checkpoint POST errored: code=${code} ` +
343
+ `type=${checkpointType} url=${checkpointUrl} message=${error.message}`);
298
344
  }
299
345
  };
300
346
  /**
@@ -323,13 +369,57 @@ program
323
369
  logger.warn(`[auto-recapture] Cloud callback failed (best-effort): ${err.message}`);
324
370
  }
325
371
  };
326
- const data = await requestJson(config, `/api/cli/projects/${opts.project}/auto-recapture-presets`, { headers: authHeaders(config) }, 'Failed to list auto-recapture presets');
372
+ // Emit a CLI-side "booted" checkpoint before any other network call.
373
+ // The orchestrator already wrote a `run_start` "cloud runner machine
374
+ // started" event when it submitted the Fly machine — that event proves
375
+ // the machine was *scheduled*, not that the container actually booted.
376
+ // Emitting this from inside the container (and BEFORE the presets fetch)
377
+ // is the dashboard's only proof the CLI is alive. If this never arrives,
378
+ // the issue is container boot or fly→dashboard connectivity, not the
379
+ // capture pipeline.
380
+ const cliVersionForCheckpoint = process.env.AUTOKAP_CLI_VERSION ?? version;
381
+ await postCloudCheckpoint({
382
+ type: 'run_start',
383
+ status: 'running',
384
+ message: `CLI booted on cloud runner (autokap@${cliVersionForCheckpoint}) — fetching plan`,
385
+ });
386
+ // Fetch the presets list with a hard timeout. Without this, a slow or
387
+ // unreachable dashboard would leave the CLI hanging forever — the
388
+ // dashboard would stay stuck at "machine started" with no error surfaced.
389
+ const presetsPath = `/api/cli/projects/${opts.project}/auto-recapture-presets`;
390
+ let data;
391
+ try {
392
+ const response = await fetch(buildApiUrl(config, presetsPath), {
393
+ headers: authHeaders(config),
394
+ signal: AbortSignal.timeout(30_000),
395
+ });
396
+ if (!response.ok) {
397
+ const errorBody = await readApiError(response);
398
+ throw new Error(`HTTP ${response.status}: ${errorBody}`);
399
+ }
400
+ data = await response.json();
401
+ }
402
+ catch (error) {
403
+ const err = error;
404
+ const isTimeout = err.name === 'TimeoutError' || err.name === 'AbortError';
405
+ const reason = isTimeout
406
+ ? `presets fetch timed out after 30s — dashboard unreachable or unresponsive`
407
+ : `presets fetch failed: ${err.code ? `${err.code} ` : ''}${err.message}`;
408
+ await postCloudCheckpoint({
409
+ type: 'error',
410
+ status: 'failed',
411
+ message: reason,
412
+ errorMessage: reason,
413
+ });
414
+ await notifyCloudCallback('failed', { totalPresets: 0, failedPresets: 0, errorMessage: reason });
415
+ fatal(`Failed to list auto-recapture presets: ${reason}`);
416
+ }
327
417
  await postCloudCheckpoint({
328
418
  type: 'run_plan',
329
419
  totalPresets: data.presets.length,
330
420
  completedPresets: 0,
331
421
  failedPresets: 0,
332
- message: `planned ${data.presets.length} preset(s)`,
422
+ message: `Run planned: ${data.presets.length} preset(s)`,
333
423
  });
334
424
  if (data.presets.length === 0) {
335
425
  logger.info(`[auto-recapture] No presets enabled for project ${opts.project}`);
@@ -343,9 +433,10 @@ program
343
433
  totalPresets: data.presets.length,
344
434
  completedPresets: 0,
345
435
  failedPresets: 0,
346
- message: `starting ${data.presets.length} preset(s)`,
436
+ message: `Runner started: ${data.presets.length} preset(s) to capture`,
347
437
  });
348
438
  for (const [index, preset] of data.presets.entries()) {
439
+ const presetDisplayName = displayPresetName(preset);
349
440
  const label = preset.name ? `${preset.name} (${preset.id})` : preset.id;
350
441
  logger.info(`[auto-recapture] Running ${label}`);
351
442
  await postCloudCheckpoint({
@@ -356,7 +447,7 @@ program
356
447
  totalPresets: data.presets.length,
357
448
  completedPresets: index,
358
449
  failedPresets: failures.length,
359
- message: `running ${preset.name ?? preset.id}`,
450
+ message: `Preset started: ${presetDisplayName}`,
360
451
  });
361
452
  const result = await runCapture({
362
453
  presetId: preset.id,
@@ -364,23 +455,21 @@ program
364
455
  headed: opts.headed,
365
456
  allowUploadFailure: opts.allowUploadFailure,
366
457
  onProgress: (event) => {
367
- const checkpointType = event.type === 'upload_start'
368
- ? 'upload_start'
369
- : event.type === 'upload_end'
370
- ? 'upload_end'
371
- : 'preset_progress';
458
+ const checkpoint = cloudCaptureProgressCheckpoint(event);
459
+ if (!checkpoint)
460
+ return;
372
461
  void postCloudCheckpoint({
373
- type: checkpointType,
462
+ type: checkpoint.type,
374
463
  presetId: preset.id,
375
464
  presetName: preset.name ?? null,
376
465
  presetIndex: index,
377
466
  totalPresets: data.presets.length,
378
467
  completedPresets: index,
379
468
  failedPresets: failures.length,
380
- message: event.message,
469
+ message: checkpoint.message,
381
470
  progressEvent: event,
382
- status: event.status === 'failed' ? 'running' : undefined,
383
- }, { throttle: checkpointType === 'preset_progress' });
471
+ status: checkpoint.status,
472
+ });
384
473
  },
385
474
  });
386
475
  const childRunId = result.runId;
@@ -399,7 +488,7 @@ program
399
488
  childRunId,
400
489
  status: 'failed',
401
490
  errorMessage: error,
402
- message: `failed ${preset.name ?? preset.id}`,
491
+ message: `Preset failed: ${presetDisplayName}`,
403
492
  });
404
493
  }
405
494
  else {
@@ -413,7 +502,7 @@ program
413
502
  failedPresets: failures.length,
414
503
  childRunId,
415
504
  status: 'completed',
416
- message: `completed ${preset.name ?? preset.id}`,
505
+ message: `Preset completed: ${presetDisplayName}`,
417
506
  });
418
507
  }
419
508
  }
@@ -445,7 +534,7 @@ program
445
534
  completedPresets: data.presets.length,
446
535
  failedPresets: 0,
447
536
  status: 'completed',
448
- message: 'cloud recapture completed',
537
+ message: 'Cloud recapture completed',
449
538
  });
450
539
  await notifyCloudCallback('completed', {
451
540
  totalPresets: data.presets.length,
@@ -446,14 +446,15 @@ export class WebPlaywrightLocal {
446
446
  throw new Error('recording was not started');
447
447
  }
448
448
  if (this.recording.finalized) {
449
- const buffer = await fs.readFile(this.recording.mp4Path);
450
- const durationMs = this.recording.encodedDurationMs ?? await getMediaDurationMs(this.recording.mp4Path);
449
+ const sourcePath = this.recording.sourcePath ?? this.recording.mp4Path;
450
+ const buffer = await fs.readFile(sourcePath);
451
+ const durationMs = this.recording.encodedDurationMs ?? await getMediaDurationMs(sourcePath);
451
452
  this.recording.encodedDurationMs = durationMs;
452
453
  return {
453
454
  buffer,
454
455
  durationMs,
455
- mimeType: 'video/mp4',
456
- trimStartMs: this.recording.result?.trimStartMs ?? 0,
456
+ mimeType: this.recording.sourceMimeType ?? 'video/mp4',
457
+ trimStartMs: this.recording.trimStartMs ?? this.recording.result?.trimStartMs ?? 0,
457
458
  };
458
459
  }
459
460
  if (this.recordingNavWatcher) {
@@ -475,6 +476,8 @@ export class WebPlaywrightLocal {
475
476
  });
476
477
  this.recording.finalized = true;
477
478
  this.recording.result = result;
479
+ this.recording.sourcePath = this.recording.mp4Path;
480
+ this.recording.sourceMimeType = 'video/mp4';
478
481
  this.recording.encodedDurationMs = await getMediaDurationMs(this.recording.mp4Path);
479
482
  this.clipCursor = null;
480
483
  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.7",
4
4
  "description": "AI-powered CLI tool for capturing clean screenshots of websites",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",