autokap 1.2.0 → 1.3.2

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/cli.js CHANGED
@@ -240,21 +240,58 @@ program
240
240
  .option('--local', `Use the local AutoKap dev server (${LOCAL_API_BASE_URL})`, false)
241
241
  .option('--allow-upload-failure', 'Keep a successful capture exit code even if artifact upload fails', false)
242
242
  .option('--debug', 'Verbose logging: per-substep timing, opcode dumps, recovery strategy traces', false)
243
+ .option('--cloud', 'Cloud runner mode: signals 4+ vCPU available, unblocks the conservative Linux FPS default (8 → 30)', false)
243
244
  .action(async (opts) => {
244
245
  if (opts.debug) {
245
246
  const { setDebugEnabled } = await import('./logger.js');
246
247
  setDebugEnabled(true);
247
248
  logger.info('[capture] Debug mode enabled — verbose logging on');
248
249
  }
250
+ if (opts.cloud) {
251
+ process.env.AUTOKAP_CLOUD_RUNNER = '1';
252
+ logger.info('[capture] Cloud runner mode — Linux FPS cap lifted (clips target 30 fps)');
253
+ }
249
254
  if (opts.local) {
250
255
  process.env[API_BASE_URL_ENV_VAR] = LOCAL_API_BASE_URL;
251
256
  process.env[WS_URL_ENV_VAR] = LOCAL_WS_URL;
252
257
  logger.info(`Using local AutoKap dev server: ${LOCAL_API_BASE_URL}`);
253
258
  }
254
259
  const config = await requireConfig();
260
+ // AUTOKAP_RUN_ID is set by the cloud-runner provider when the CLI is
261
+ // launched on Fly.io. When present, we POST a completion callback at exit
262
+ // so the `capture_runs` row flips out of `queued` — without this the
263
+ // backend rate-limiter blocks future cloud recaptures.
264
+ const cloudRunId = process.env.AUTOKAP_RUN_ID;
265
+ /**
266
+ * Best-effort: tell the AutoKap backend whether this cloud run finished
267
+ * cleanly. Failures here do NOT change the CLI exit code — the artifact
268
+ * uploads are already done at this point, so a missed callback only
269
+ * affects status reporting (a reconciliation cron heals it later).
270
+ */
271
+ const notifyCloudCallback = async (status, details) => {
272
+ if (!cloudRunId)
273
+ return;
274
+ try {
275
+ const response = await fetch(buildApiUrl(config, `/api/cli/cloud-recapture/${cloudRunId}/complete`), {
276
+ method: 'POST',
277
+ headers: { ...authHeaders(config), 'Content-Type': 'application/json' },
278
+ body: JSON.stringify({ status, ...details }),
279
+ });
280
+ if (!response.ok) {
281
+ const body = await response.text().catch(() => response.statusText);
282
+ logger.warn(`[auto-recapture] Cloud callback non-OK (${response.status}): ${body}`);
283
+ return;
284
+ }
285
+ logger.info(`[auto-recapture] Cloud run marked ${status} on backend`);
286
+ }
287
+ catch (err) {
288
+ logger.warn(`[auto-recapture] Cloud callback failed (best-effort): ${err.message}`);
289
+ }
290
+ };
255
291
  const data = await requestJson(config, `/api/cli/projects/${opts.project}/auto-recapture-presets`, { headers: authHeaders(config) }, 'Failed to list auto-recapture presets');
256
292
  if (data.presets.length === 0) {
257
293
  logger.info(`[auto-recapture] No presets enabled for project ${opts.project}`);
294
+ await notifyCloudCallback('completed', { totalPresets: 0, failedPresets: 0 });
258
295
  process.exit(0);
259
296
  }
260
297
  const { runCapture } = await import('./cli-runner.js');
@@ -275,12 +312,22 @@ program
275
312
  }
276
313
  }
277
314
  if (failures.length > 0) {
278
- logger.error(`[auto-recapture] ${failures.length}/${data.presets.length} preset(s) failed: ${failures
315
+ const errorMessage = `${failures.length}/${data.presets.length} preset(s) failed: ${failures
279
316
  .map((failure) => failure.name ?? failure.id)
280
- .join(', ')}`);
317
+ .join(', ')}`;
318
+ logger.error(`[auto-recapture] ${errorMessage}`);
319
+ await notifyCloudCallback('failed', {
320
+ totalPresets: data.presets.length,
321
+ failedPresets: failures.length,
322
+ errorMessage,
323
+ });
281
324
  process.exit(1);
282
325
  }
283
326
  logger.success(`[auto-recapture] ${data.presets.length} preset(s) recaptured successfully`);
327
+ await notifyCloudCallback('completed', {
328
+ totalPresets: data.presets.length,
329
+ failedPresets: 0,
330
+ });
284
331
  process.exit(0);
285
332
  });
286
333
  // ── project commands ───────────────────────────────────────────────
@@ -606,6 +653,81 @@ presetCmd
606
653
  console.log(JSON.stringify(info, null, 2));
607
654
  process.exit(0);
608
655
  });
656
+ // ── video commands ─────────────────────────────────────────────────
657
+ //
658
+ // Mirrors the preset commands but targets `/api/video-projects` (CLI-keyed
659
+ // route used by the IDE skill flow). The payload file contains the full
660
+ // body expected by the API (projectId, title, user_script, legacy
661
+ // narration_voice/narration_locale aliases, narration_by_app_locale,
662
+ // app_locale/app_locales, app_theme/app_themes, cursor_theme,
663
+ // credentials_account_id, mockDataInjection, program). On `update`, the same payload shape is sent
664
+ // via PATCH. TTS is generated later by `autokap run`.
665
+ const videoCmd = program
666
+ .command('video')
667
+ .description('Manage demo videos');
668
+ videoCmd
669
+ .command('create')
670
+ .description('Create a new demo video from a JSON payload file')
671
+ .requiredOption('--payload <file>', 'Path to payload JSON file (use "-" for stdin)')
672
+ .action(async (opts) => {
673
+ const cfg = await requireConfig();
674
+ let payload;
675
+ try {
676
+ payload = await readJsonInput(opts.payload);
677
+ }
678
+ catch (err) {
679
+ logger.error(`Failed to read payload: ${err.message}`);
680
+ process.exit(1);
681
+ }
682
+ const res = await fetch(`${cfg.apiBaseUrl}/api/video-projects`, {
683
+ method: 'POST',
684
+ headers: {
685
+ Authorization: `Bearer ${cfg.apiKey}`,
686
+ 'Content-Type': 'application/json',
687
+ },
688
+ body: JSON.stringify(payload),
689
+ });
690
+ if (!res.ok) {
691
+ const body = await res.json().catch(() => ({ error: res.statusText }));
692
+ logger.error(`Failed to create video: ${body.error || res.statusText}`);
693
+ process.exit(1);
694
+ }
695
+ const data = await res.json();
696
+ // Output only the video ID so callers can chain: autokap run $(autokap video create ...)
697
+ console.log(data.video.id);
698
+ process.exit(0);
699
+ });
700
+ videoCmd
701
+ .command('update <video-id>')
702
+ .description('Update an existing demo video from a JSON payload file')
703
+ .requiredOption('--payload <file>', 'Path to payload JSON file (use "-" for stdin)')
704
+ .action(async (videoId, opts) => {
705
+ const cfg = await requireConfig();
706
+ let payload;
707
+ try {
708
+ payload = await readJsonInput(opts.payload);
709
+ }
710
+ catch (err) {
711
+ logger.error(`Failed to read payload: ${err.message}`);
712
+ process.exit(1);
713
+ }
714
+ const res = await fetch(`${cfg.apiBaseUrl}/api/video-projects/${videoId}`, {
715
+ method: 'PATCH',
716
+ headers: {
717
+ Authorization: `Bearer ${cfg.apiKey}`,
718
+ 'Content-Type': 'application/json',
719
+ },
720
+ body: JSON.stringify(payload),
721
+ });
722
+ if (!res.ok) {
723
+ const body = await res.json().catch(() => ({ error: res.statusText }));
724
+ logger.error(`Failed to update video: ${body.error || res.statusText}`);
725
+ process.exit(1);
726
+ }
727
+ const data = await res.json();
728
+ console.log(data.video.id);
729
+ process.exit(0);
730
+ });
609
731
  // ── auth commands ──────────────────────────────────────────────────
610
732
  const authCmd = program
611
733
  .command('auth')
@@ -34,10 +34,19 @@ export class ClipCaptureLoop {
34
34
  this.page = opts.page;
35
35
  this.framesDir = opts.framesDir;
36
36
  this.jpegQuality = opts.jpegQuality ?? 80;
37
- const targetFps = Math.max(1, Math.min(30, opts.targetFps ?? (process.platform === 'linux' ? 8 : 15)));
37
+ // Linux default is 8 fps to stay safe on 2 vCPU CI runners. Cloud runners
38
+ // (AUTOKAP_CLOUD_RUNNER=1, set by the Fly.io image and the `--cloud` CLI
39
+ // flag) get the same 15 fps default as macOS/Windows since they have
40
+ // ≥ 4 vCPU. Callers can still override via opts.targetFps.
41
+ const isCloudRunner = process.env.AUTOKAP_CLOUD_RUNNER === '1';
42
+ const linuxDefault = isCloudRunner ? 15 : 8;
43
+ const platformDefault = process.platform === 'linux' ? linuxDefault : 15;
44
+ const targetFps = Math.max(1, Math.min(30, opts.targetFps ?? platformDefault));
38
45
  this.targetFps = targetFps;
39
46
  this.targetFrameIntervalMs = 1000 / targetFps;
40
- this.minRestMs = Math.max(0, Math.min(250, opts.minRestMs ?? (process.platform === 'linux' ? 50 : 16)));
47
+ const linuxMinRest = isCloudRunner ? 16 : 50;
48
+ const platformMinRest = process.platform === 'linux' ? linuxMinRest : 16;
49
+ this.minRestMs = Math.max(0, Math.min(250, opts.minRestMs ?? platformMinRest));
41
50
  }
42
51
  async start() {
43
52
  this.cdp = await this.page.context().newCDPSession(this.page);
@@ -1,4 +1,5 @@
1
1
  import type { Page } from 'playwright';
2
+ export declare const CAPTURE_HIDE_STYLE_ID = "autokap-capture-hide-style";
2
3
  export declare function getCaptureHideCSS(): string;
3
4
  export declare function ensureCaptureHideStyles(page: Page): Promise<void>;
4
5
  export declare function dismissCookiesAndWidgets(page: Page): Promise<{
@@ -98,8 +98,20 @@ const DEV_TOOL_SELECTORS = [
98
98
  'nuxt-devtools-frame',
99
99
  '#nuxt-devtools-anchor',
100
100
  '#nuxt-devtools-container',
101
+ // Common coding-agent / prototype builder chrome
102
+ '[data-testid="v0-toolbar"]',
103
+ '#v0-toolbar',
104
+ '[data-v0-devtools]',
105
+ '[data-lovable-badge]',
106
+ '#lovable-badge',
107
+ '[class*="lovable-badge"]',
108
+ '[data-bolt-toolbar]',
109
+ '#bolt-toolbar',
110
+ '[class*="bolt-toolbar"]',
111
+ '[data-replit-devtools]',
112
+ '#replit-devtools',
101
113
  ];
102
- const CAPTURE_HIDE_STYLE_ID = 'autokap-capture-hide-style';
114
+ export const CAPTURE_HIDE_STYLE_ID = 'autokap-capture-hide-style';
103
115
  export function getCaptureHideCSS() {
104
116
  return [...HIDE_SELECTORS, ...DEV_TOOL_SELECTORS].map(selector => `${selector} { display: none !important; visibility: hidden !important; pointer-events: none !important; }`).join('\n');
105
117
  }