autokap 1.3.2 → 1.3.4

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.
@@ -8,6 +8,7 @@ declare const DEFAULT_WS_URL = "wss://autokap.app/ws";
8
8
  declare const LOCAL_API_BASE_URL = "http://localhost:3000";
9
9
  declare const LOCAL_WS_URL = "ws://localhost:3000/ws";
10
10
  declare const API_KEY_ENV_VAR = "AUTOKAP_API_KEY";
11
+ declare const RUN_TOKEN_ENV_VAR = "AUTOKAP_RUN_TOKEN";
11
12
  declare const API_BASE_URL_ENV_VAR = "AUTOKAP_API_BASE_URL";
12
13
  declare const WS_URL_ENV_VAR = "AUTOKAP_WS_URL";
13
14
  declare const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = "AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN";
@@ -19,4 +20,4 @@ export declare function readConfig(): Promise<AutokapConfig | null>;
19
20
  export declare function writeConfig(config: AutokapConfig): Promise<void>;
20
21
  export declare function deleteConfig(): Promise<void>;
21
22
  export declare function requireConfig(): Promise<AutokapConfig>;
22
- export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
23
+ export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
@@ -7,6 +7,7 @@ const DEFAULT_WS_URL = 'wss://autokap.app/ws';
7
7
  const LOCAL_API_BASE_URL = 'http://localhost:3000';
8
8
  const LOCAL_WS_URL = 'ws://localhost:3000/ws';
9
9
  const API_KEY_ENV_VAR = 'AUTOKAP_API_KEY';
10
+ const RUN_TOKEN_ENV_VAR = 'AUTOKAP_RUN_TOKEN';
10
11
  const API_BASE_URL_ENV_VAR = 'AUTOKAP_API_BASE_URL';
11
12
  const WS_URL_ENV_VAR = 'AUTOKAP_WS_URL';
12
13
  const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = 'AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN';
@@ -23,6 +24,17 @@ export function getDefaultWsUrl(apiBaseUrl = getDefaultApiBaseUrl()) {
23
24
  return normalizeUrl(process.env[WS_URL_ENV_VAR]) ?? deriveWsUrl(apiBaseUrl);
24
25
  }
25
26
  export async function readConfig() {
27
+ const envRunToken = normalizeApiKey(process.env[RUN_TOKEN_ENV_VAR]);
28
+ if (envRunToken) {
29
+ const apiBaseUrl = getDefaultApiBaseUrl();
30
+ const wsUrl = getDefaultWsUrl(apiBaseUrl);
31
+ assertAllowedApiOrigin(apiBaseUrl, DEFAULT_API_BASE_URL, API_BASE_URL_ENV_VAR);
32
+ return {
33
+ apiKey: envRunToken,
34
+ apiBaseUrl,
35
+ wsUrl,
36
+ };
37
+ }
26
38
  const envApiKey = normalizeApiKey(process.env[API_KEY_ENV_VAR]);
27
39
  if (envApiKey) {
28
40
  const apiBaseUrl = getDefaultApiBaseUrl();
@@ -159,5 +171,5 @@ function assertAllowedApiOrigin(candidateUrl, baselineUrl, envVar) {
159
171
  }
160
172
  throw new Error(`Refusing unsafe server override to ${candidateOrigin}. Set ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1 to allow ${envVar ?? 'this override'} explicitly.`);
161
173
  }
162
- export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
174
+ export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
163
175
  //# sourceMappingURL=cli-config.js.map
@@ -52,7 +52,7 @@ export const CLI_PUBLIC_COMMANDS = [
52
52
  {
53
53
  id: "auto-recapture",
54
54
  command: "autokap auto-recapture --project <project-id> --env local",
55
- summary: "Run every preset enabled for CI auto-recapture in a project",
55
+ summary: "Run every preset enabled for Recapture Cloud in a project",
56
56
  docsDescriptionKey: "cliCmdAutoRecapture",
57
57
  },
58
58
  {
@@ -57,6 +57,17 @@ export async function runLocal(presetId, opts) {
57
57
  case 'breaker_trip':
58
58
  logger.error(`${prefix} CIRCUIT BREAKER: ${event.message}`);
59
59
  break;
60
+ case 'upload_start':
61
+ logger.info('[capture] Uploading artifacts and telemetry');
62
+ break;
63
+ case 'upload_end':
64
+ if (event.status === 'failed') {
65
+ logger.error(`[capture] Upload failed: ${event.message}`);
66
+ }
67
+ else {
68
+ logger.info('[capture] Upload complete');
69
+ }
70
+ break;
60
71
  }
61
72
  },
62
73
  });
@@ -40,6 +40,7 @@ export interface CLIRunnerOptions {
40
40
  }
41
41
  export interface CLIRunResult {
42
42
  success: boolean;
43
+ runId?: string;
43
44
  runResult?: RunResult;
44
45
  error?: string;
45
46
  }
@@ -156,12 +156,12 @@ export async function runCapture(options) {
156
156
  assertProgramNavigationScope(program, resolvedProgram.security);
157
157
  }
158
158
  catch (error) {
159
- return { success: false, error: error instanceof Error ? error.message : String(error) };
159
+ return { success: false, runId, error: error instanceof Error ? error.message : String(error) };
160
160
  }
161
161
  if (!options.program && program.mediaMode === 'video') {
162
162
  const prepareResult = await prepareVideoSpeechForRun(config, options.presetId, runId);
163
163
  if (!prepareResult.success) {
164
- return { success: false, error: prepareResult.error };
164
+ return { success: false, runId, error: prepareResult.error };
165
165
  }
166
166
  program = applyVideoSpeechDurations(program, prepareResult.durationsByStepId);
167
167
  videoAudioAssets = prepareResult.audioAssets;
@@ -170,7 +170,7 @@ export async function runCapture(options) {
170
170
  program = normalizeVideoCaptureProgram(parseProgram(program));
171
171
  }
172
172
  catch (err) {
173
- return { success: false, error: `prepared video program validation failed: ${err instanceof Error ? err.message : String(err)}` };
173
+ return { success: false, runId, error: `prepared video program validation failed: ${err instanceof Error ? err.message : String(err)}` };
174
174
  }
175
175
  }
176
176
  logger.info(`[capture] Running preset "${options.presetId}" — ${program.steps.length} opcodes, ${program.variants.length} variant(s)`);
@@ -264,19 +264,37 @@ export async function runCapture(options) {
264
264
  }
265
265
  try {
266
266
  logger.info('[capture] Saving captures, might take a few seconds...');
267
+ options.onProgress?.({
268
+ type: 'upload_start',
269
+ variantId: 'run',
270
+ message: 'saving captures',
271
+ });
267
272
  const uploadOutcome = await uploadResults(config, program, runResult, runId);
268
273
  if (program.mediaMode === 'video' && runResult.success) {
269
274
  await signalVideoComplete(config, program, runResult, uploadOutcome.runId, videoAudioAssets, videoAudioAssetsByLocale);
270
275
  }
271
276
  const totalDurationSec = ((Date.now() - captureStart) / 1000).toFixed(1);
272
277
  logger.info(`[capture] Captures saved successfully — total ${totalDurationSec}s`);
278
+ options.onProgress?.({
279
+ type: 'upload_end',
280
+ variantId: 'run',
281
+ status: 'ok',
282
+ message: 'captures saved',
283
+ });
273
284
  }
274
285
  catch (err) {
275
286
  const message = err instanceof Error ? err.message : String(err);
276
287
  logger.error(`[capture] Failed to upload results: ${message}`);
288
+ options.onProgress?.({
289
+ type: 'upload_end',
290
+ variantId: 'run',
291
+ status: 'failed',
292
+ message,
293
+ });
277
294
  if (!options.allowUploadFailure) {
278
295
  return {
279
296
  success: false,
297
+ runId,
280
298
  runResult,
281
299
  error: runResult.success
282
300
  ? `upload failed: ${message}`
@@ -285,7 +303,7 @@ export async function runCapture(options) {
285
303
  }
286
304
  logger.warn('[capture] Continuing after upload failure because --allow-upload-failure was set');
287
305
  }
288
- return { success: runResult.success, runResult };
306
+ return { success: runResult.success, runId, runResult };
289
307
  }
290
308
  // ── Server communication ────────────────────────────────────────────
291
309
  async function fetchProgram(config, presetId, environmentName) {
@@ -1067,6 +1085,17 @@ function logProgress(event) {
1067
1085
  case 'breaker_trip':
1068
1086
  logger.error(`${prefix} Circuit breaker tripped: ${event.message}`);
1069
1087
  break;
1088
+ case 'upload_start':
1089
+ logger.info('[capture] Uploading artifacts and telemetry');
1090
+ break;
1091
+ case 'upload_end':
1092
+ if (event.status === 'failed') {
1093
+ logger.error(`[capture] Upload failed: ${event.message}`);
1094
+ }
1095
+ else {
1096
+ logger.info('[capture] Upload complete');
1097
+ }
1098
+ break;
1070
1099
  }
1071
1100
  }
1072
1101
  //# sourceMappingURL=cli-runner.js.map
package/dist/cli.js CHANGED
@@ -233,7 +233,7 @@ program
233
233
  // ── auto-recapture command ─────────────────────────────────────────
234
234
  program
235
235
  .command('auto-recapture')
236
- .description('Run every preset enabled for CI auto-recapture in a project')
236
+ .description('Run every preset enabled for Recapture Cloud in a project')
237
237
  .requiredOption('--project <id>', 'Project ID')
238
238
  .option('--env <name>', "Project environment to capture against. Falls back to the project's default environment when omitted.")
239
239
  .option('--headed', 'Show browser window for debugging', false)
@@ -262,6 +262,34 @@ program
262
262
  // so the `capture_runs` row flips out of `queued` — without this the
263
263
  // backend rate-limiter blocks future cloud recaptures.
264
264
  const cloudRunId = process.env.AUTOKAP_RUN_ID;
265
+ const checkpointUrl = cloudRunId
266
+ ? buildApiUrl(config, `/api/cli/cloud-recapture/${cloudRunId}/checkpoint`)
267
+ : null;
268
+ let lastProgressCheckpointAt = 0;
269
+ const postCloudCheckpoint = async (body, options = {}) => {
270
+ if (!checkpointUrl)
271
+ return;
272
+ if (options.throttle) {
273
+ const now = Date.now();
274
+ if (now - lastProgressCheckpointAt < 750)
275
+ return;
276
+ lastProgressCheckpointAt = now;
277
+ }
278
+ try {
279
+ const response = await fetch(checkpointUrl, {
280
+ method: 'POST',
281
+ headers: { ...authHeaders(config), 'Content-Type': 'application/json' },
282
+ body: JSON.stringify(body),
283
+ });
284
+ if (!response.ok) {
285
+ const bodyText = await response.text().catch(() => response.statusText);
286
+ logger.warn(`[auto-recapture] Cloud checkpoint non-OK (${response.status}): ${bodyText}`);
287
+ }
288
+ }
289
+ catch (err) {
290
+ logger.warn(`[auto-recapture] Cloud checkpoint failed (best-effort): ${err.message}`);
291
+ }
292
+ };
265
293
  /**
266
294
  * Best-effort: tell the AutoKap backend whether this cloud run finished
267
295
  * cleanly. Failures here do NOT change the CLI exit code — the artifact
@@ -289,6 +317,13 @@ program
289
317
  }
290
318
  };
291
319
  const data = await requestJson(config, `/api/cli/projects/${opts.project}/auto-recapture-presets`, { headers: authHeaders(config) }, 'Failed to list auto-recapture presets');
320
+ await postCloudCheckpoint({
321
+ type: 'run_plan',
322
+ totalPresets: data.presets.length,
323
+ completedPresets: 0,
324
+ failedPresets: 0,
325
+ message: `planned ${data.presets.length} preset(s)`,
326
+ });
292
327
  if (data.presets.length === 0) {
293
328
  logger.info(`[auto-recapture] No presets enabled for project ${opts.project}`);
294
329
  await notifyCloudCallback('completed', { totalPresets: 0, failedPresets: 0 });
@@ -296,19 +331,83 @@ program
296
331
  }
297
332
  const { runCapture } = await import('./cli-runner.js');
298
333
  const failures = [];
299
- for (const preset of data.presets) {
334
+ await postCloudCheckpoint({
335
+ type: 'run_start',
336
+ totalPresets: data.presets.length,
337
+ completedPresets: 0,
338
+ failedPresets: 0,
339
+ message: `starting ${data.presets.length} preset(s)`,
340
+ });
341
+ for (const [index, preset] of data.presets.entries()) {
300
342
  const label = preset.name ? `${preset.name} (${preset.id})` : preset.id;
301
343
  logger.info(`[auto-recapture] Running ${label}`);
344
+ await postCloudCheckpoint({
345
+ type: 'preset_start',
346
+ presetId: preset.id,
347
+ presetName: preset.name ?? null,
348
+ presetIndex: index,
349
+ totalPresets: data.presets.length,
350
+ completedPresets: index,
351
+ failedPresets: failures.length,
352
+ message: `running ${preset.name ?? preset.id}`,
353
+ });
302
354
  const result = await runCapture({
303
355
  presetId: preset.id,
304
356
  env: opts.env,
305
357
  headed: opts.headed,
306
358
  allowUploadFailure: opts.allowUploadFailure,
359
+ onProgress: (event) => {
360
+ const checkpointType = event.type === 'upload_start'
361
+ ? 'upload_start'
362
+ : event.type === 'upload_end'
363
+ ? 'upload_end'
364
+ : 'preset_progress';
365
+ void postCloudCheckpoint({
366
+ type: checkpointType,
367
+ presetId: preset.id,
368
+ presetName: preset.name ?? null,
369
+ presetIndex: index,
370
+ totalPresets: data.presets.length,
371
+ completedPresets: index,
372
+ failedPresets: failures.length,
373
+ message: event.message,
374
+ progressEvent: event,
375
+ status: event.status === 'failed' ? 'running' : undefined,
376
+ }, { throttle: checkpointType === 'preset_progress' });
377
+ },
307
378
  });
379
+ const childRunId = result.runId;
308
380
  if (!result.success) {
309
381
  const error = result.error ?? result.runResult?.error ?? 'capture failed';
310
382
  failures.push({ id: preset.id, name: preset.name, error });
311
383
  logger.error(`[auto-recapture] Failed ${label}: ${error}`);
384
+ await postCloudCheckpoint({
385
+ type: 'preset_end',
386
+ presetId: preset.id,
387
+ presetName: preset.name ?? null,
388
+ presetIndex: index,
389
+ totalPresets: data.presets.length,
390
+ completedPresets: index + 1,
391
+ failedPresets: failures.length,
392
+ childRunId,
393
+ status: 'failed',
394
+ errorMessage: error,
395
+ message: `failed ${preset.name ?? preset.id}`,
396
+ });
397
+ }
398
+ else {
399
+ await postCloudCheckpoint({
400
+ type: 'preset_end',
401
+ presetId: preset.id,
402
+ presetName: preset.name ?? null,
403
+ presetIndex: index,
404
+ totalPresets: data.presets.length,
405
+ completedPresets: index + 1,
406
+ failedPresets: failures.length,
407
+ childRunId,
408
+ status: 'completed',
409
+ message: `completed ${preset.name ?? preset.id}`,
410
+ });
312
411
  }
313
412
  }
314
413
  if (failures.length > 0) {
@@ -316,6 +415,15 @@ program
316
415
  .map((failure) => failure.name ?? failure.id)
317
416
  .join(', ')}`;
318
417
  logger.error(`[auto-recapture] ${errorMessage}`);
418
+ await postCloudCheckpoint({
419
+ type: 'error',
420
+ totalPresets: data.presets.length,
421
+ completedPresets: data.presets.length,
422
+ failedPresets: failures.length,
423
+ status: 'failed',
424
+ errorMessage,
425
+ message: errorMessage,
426
+ });
319
427
  await notifyCloudCallback('failed', {
320
428
  totalPresets: data.presets.length,
321
429
  failedPresets: failures.length,
@@ -324,6 +432,14 @@ program
324
432
  process.exit(1);
325
433
  }
326
434
  logger.success(`[auto-recapture] ${data.presets.length} preset(s) recaptured successfully`);
435
+ await postCloudCheckpoint({
436
+ type: 'run_end',
437
+ totalPresets: data.presets.length,
438
+ completedPresets: data.presets.length,
439
+ failedPresets: 0,
440
+ status: 'completed',
441
+ message: 'cloud recapture completed',
442
+ });
327
443
  await notifyCloudCallback('completed', {
328
444
  totalPresets: data.presets.length,
329
445
  failedPresets: 0,
@@ -37,7 +37,7 @@ export class ClipCaptureLoop {
37
37
  // Linux default is 8 fps to stay safe on 2 vCPU CI runners. Cloud runners
38
38
  // (AUTOKAP_CLOUD_RUNNER=1, set by the Fly.io image and the `--cloud` CLI
39
39
  // flag) get the same 15 fps default as macOS/Windows since they have
40
- // 4 vCPU. Callers can still override via opts.targetFps.
40
+ // dedicated 8 vCPU capacity. Callers can still override via opts.targetFps.
41
41
  const isCloudRunner = process.env.AUTOKAP_CLOUD_RUNNER === '1';
42
42
  const linuxDefault = isCloudRunner ? 15 : 8;
43
43
  const platformDefault = process.platform === 'linux' ? linuxDefault : 15;
@@ -43,7 +43,7 @@ export interface RunOptions {
43
43
  presetName?: string;
44
44
  }
45
45
  export interface ProgressEvent {
46
- type: 'variant_start' | 'variant_end' | 'opcode_start' | 'opcode_end' | 'recovery' | 'breaker_trip';
46
+ type: 'variant_start' | 'variant_end' | 'opcode_start' | 'opcode_end' | 'recovery' | 'breaker_trip' | 'upload_start' | 'upload_end';
47
47
  variantId: string;
48
48
  opcodeIndex?: number;
49
49
  opcodeKind?: string;
@@ -1,5 +1,5 @@
1
1
  import type { SupabaseClient } from '@supabase/supabase-js';
2
- export type CreditUsageType = 'screenshot' | 'clip' | 'video' | 'preset_analysis' | 'ai_chat' | 'studio_creation' | 'studio_iteration';
2
+ export type CreditUsageType = 'screenshot' | 'clip' | 'video' | 'preset_analysis' | 'cloud_recapture' | 'ai_chat' | 'studio_creation' | 'studio_iteration';
3
3
  export declare function recordCreditUsage(supabase: SupabaseClient, params: {
4
4
  userId: string;
5
5
  projectId: string | null;
@@ -344,7 +344,7 @@ export class WebPlaywrightLocal {
344
344
  await fs.mkdir(framesDir, { recursive: true });
345
345
  // Linux defaults are conservative because GitHub Actions free runners
346
346
  // (2 vCPU) can't sustain 30 fps. AUTOKAP_CLOUD_RUNNER=1 signals that the
347
- // process is running on managed cloud infra (Fly.io machines, ≥4 vCPU)
347
+ // process is running on managed cloud infra (Fly.io machines, 8 vCPU)
348
348
  // where the cap can safely lift to 30 fps for clips too. Set by the
349
349
  // `--cloud` flag and by the cloud-runner Docker image.
350
350
  const isCloudRunner = process.env.AUTOKAP_CLOUD_RUNNER === '1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autokap",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "AI-powered CLI tool for capturing clean screenshots of websites",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/readme.md CHANGED
@@ -16,18 +16,29 @@ cp .env.example .env.local
16
16
  npm run dev
17
17
  ```
18
18
 
19
- ## CLI in CI/CD
19
+ ## Recapture Cloud in CI/CD
20
20
 
21
- AutoKap can run non-interactively in CI by reading the CLI key from
22
- `AUTOKAP_API_KEY`; no local `~/.autokap/config.json` is required.
21
+ The official CI/CD flow is Recapture Cloud. Generate a signed webhook secret
22
+ from the project Recapture page, store it in your CI secret manager, then call
23
+ the webhook on deploy. CI never needs an AutoKap CLI key.
23
24
 
24
25
  ```bash
25
- AUTOKAP_API_KEY=ak_cli_... autokap run <preset-id> --env staging
26
- AUTOKAP_API_KEY=ak_cli_... autokap auto-recapture --project <project-id> --env local
26
+ BODY='{}'
27
+ SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$AUTOKAP_WEBHOOK_SECRET" -binary | xxd -p -c 256)
28
+ curl -X POST "https://autokap.app/api/webhooks/cloud-recapture/<project-id>" \
29
+ -H "Content-Type: application/json" \
30
+ -H "X-AutoKap-Signature: sha256=$SIG" \
31
+ -d "$BODY"
27
32
  ```
28
33
 
29
- Use project environments in the dashboard to map `local`, `staging`, and
30
- `production` to different base URLs without duplicating presets.
34
+ Recapture Cloud always captures the project's `prod` environment. Use the
35
+ other project environments for local debugging and staging checks.
36
+
37
+ The local CLI remains available for advanced debugging:
38
+
39
+ ```bash
40
+ AUTOKAP_API_KEY=ak_cli_... autokap run <preset-id> --env staging
41
+ ```
31
42
 
32
43
  ## Auth Setup (Supabase)
33
44