autokap 1.0.10 → 1.1.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.
Files changed (42) hide show
  1. package/assets/skill/OPCODE-REFERENCE.md +1 -41
  2. package/assets/skill/README.md +0 -1
  3. package/assets/skill/SKILL.md +9 -32
  4. package/assets/skill/references/examples.md +1 -17
  5. package/dist/billing-operation-logging.d.ts +2 -5
  6. package/dist/billing-operation-logging.js +0 -7
  7. package/dist/capture-strategy.d.ts +2 -8
  8. package/dist/capture-strategy.js +2 -30
  9. package/dist/cli-config.d.ts +2 -1
  10. package/dist/cli-config.js +18 -2
  11. package/dist/cli-contract.d.ts +1 -0
  12. package/dist/cli-contract.js +8 -2
  13. package/dist/cli-runner-local.d.ts +2 -0
  14. package/dist/cli-runner-local.js +12 -21
  15. package/dist/cli-runner.d.ts +4 -0
  16. package/dist/cli-runner.js +30 -50
  17. package/dist/cli.js +89 -44
  18. package/dist/cost-logging.d.ts +1 -1
  19. package/dist/execution-schema.d.ts +143 -331
  20. package/dist/execution-schema.js +43 -28
  21. package/dist/execution-types.d.ts +6 -151
  22. package/dist/execution-types.js +1 -3
  23. package/dist/logger.js +1 -1
  24. package/dist/mockup-html.d.ts +2 -0
  25. package/dist/mockup-html.js +13 -10
  26. package/dist/mockup.js +2 -2
  27. package/dist/opcode-actions.js +0 -2
  28. package/dist/opcode-runner.js +0 -121
  29. package/dist/program-signing.d.ts +50 -72
  30. package/dist/security.js +2 -2
  31. package/dist/server-capture-runtime.d.ts +0 -1
  32. package/dist/server-capture-runtime.js +0 -3
  33. package/dist/server-credit-usage.d.ts +1 -1
  34. package/dist/skill-packaging.d.ts +1 -1
  35. package/dist/skill-packaging.js +1 -11
  36. package/dist/types.d.ts +2 -2
  37. package/dist/web-playwright-local.d.ts +0 -14
  38. package/dist/web-playwright-local.js +0 -194
  39. package/package.json +1 -18
  40. package/readme.md +13 -0
  41. package/assets/skill/STUDIO-SKILL.md +0 -476
  42. package/assets/skill/references/interactive-demo.md +0 -225
@@ -46,7 +46,7 @@ export async function runCapture(options) {
46
46
  };
47
47
  }
48
48
  else {
49
- const fetched = await fetchProgram(config, options.presetId);
49
+ const fetched = await fetchProgram(config, options.presetId, options.env);
50
50
  if (!fetched.success) {
51
51
  return { success: false, error: fetched.error };
52
52
  }
@@ -151,15 +151,29 @@ export async function runCapture(options) {
151
151
  logger.info(`[capture] Captures saved successfully — total ${totalDurationSec}s`);
152
152
  }
153
153
  catch (err) {
154
- logger.error(`[capture] Failed to upload results: ${err instanceof Error ? err.message : String(err)}`);
154
+ const message = err instanceof Error ? err.message : String(err);
155
+ logger.error(`[capture] Failed to upload results: ${message}`);
156
+ if (!options.allowUploadFailure) {
157
+ return {
158
+ success: false,
159
+ runResult,
160
+ error: runResult.success
161
+ ? `upload failed: ${message}`
162
+ : `${runResult.error ?? 'capture failed'}; upload failed: ${message}`,
163
+ };
164
+ }
165
+ logger.warn('[capture] Continuing after upload failure because --allow-upload-failure was set');
155
166
  }
156
167
  return { success: runResult.success, runResult };
157
168
  }
158
169
  // ── Server communication ────────────────────────────────────────────
159
- async function fetchProgram(config, presetId) {
170
+ async function fetchProgram(config, presetId, environmentName) {
160
171
  try {
161
- const url = `${config.apiBaseUrl}/api/cli/programs/${presetId}`;
162
- const response = await fetch(url, {
172
+ const url = new URL(`${config.apiBaseUrl}/api/cli/programs/${presetId}`);
173
+ if (environmentName) {
174
+ url.searchParams.set('env', environmentName);
175
+ }
176
+ const response = await fetch(url.toString(), {
163
177
  headers: {
164
178
  'Authorization': `Bearer ${config.apiKey}`,
165
179
  'Content-Type': 'application/json',
@@ -167,7 +181,7 @@ async function fetchProgram(config, presetId) {
167
181
  },
168
182
  });
169
183
  if (!response.ok) {
170
- return { success: false, error: await formatServerError(response, url) };
184
+ return { success: false, error: await formatServerError(response, url.toString()) };
171
185
  }
172
186
  const data = await response.json();
173
187
  const envelope = verifySignedExecutionProgramEnvelope({
@@ -195,7 +209,7 @@ async function uploadResults(config, program, result) {
195
209
  const formData = new FormData();
196
210
  const filename = buildArtifactFilename(program.presetId, variant.variantId, artifact);
197
211
  uploadedCount += 1;
198
- const label = artifact.captureName ?? artifact.clipName ?? artifact.fragmentName ?? artifact.stateName ?? filename;
212
+ const label = artifact.captureName ?? artifact.clipName ?? filename;
199
213
  logger.info(`[capture] Exporting capture ${uploadedCount}/${totalArtifacts}: ${label}`);
200
214
  formData.append('file', new Blob([new Uint8Array(artifact.buffer)], { type: artifact.mimeType }), filename);
201
215
  formData.append('presetId', program.presetId);
@@ -253,35 +267,6 @@ async function uploadResults(config, program, result) {
253
267
  if (typeof artifact.trimStartMs === 'number') {
254
268
  formData.append('trimStartMs', String(artifact.trimStartMs));
255
269
  }
256
- // ── Interactive demo (mediaMode === 'dom') ──
257
- if (artifact.stateName) {
258
- formData.append('stateName', artifact.stateName);
259
- }
260
- if (artifact.domAssetUrls && artifact.domAssetUrls.length > 0) {
261
- formData.append('domAssetUrls', JSON.stringify(artifact.domAssetUrls));
262
- }
263
- if (artifact.domThumbnailBuffer) {
264
- formData.append('domThumbnail', new Blob([new Uint8Array(artifact.domThumbnailBuffer)], { type: 'image/png' }), 'thumbnail.png');
265
- }
266
- // Phase 5: fragment fields
267
- if (artifact.fragmentName) {
268
- formData.append('fragmentName', artifact.fragmentName);
269
- }
270
- // Phase 8: variant of the fragment capture (defaults to 'default'
271
- // server-side when omitted, but we always send it explicitly so the
272
- // upload row's unique key includes it).
273
- if (artifact.fragmentVariantName) {
274
- formData.append('fragmentVariantName', artifact.fragmentVariantName);
275
- }
276
- if (artifact.parentStateName) {
277
- formData.append('parentStateName', artifact.parentStateName);
278
- }
279
- if (artifact.mountStrategy) {
280
- formData.append('mountStrategy', artifact.mountStrategy);
281
- }
282
- if (artifact.mountTargetSelector) {
283
- formData.append('mountTargetSelector', artifact.mountTargetSelector);
284
- }
285
270
  const response = await fetch(`${config.apiBaseUrl}/api/cli/artifacts`, {
286
271
  method: 'POST',
287
272
  headers: { 'Authorization': `Bearer ${config.apiKey}` },
@@ -438,17 +423,15 @@ function createHealerLLMProvider(llmConfig) {
438
423
  };
439
424
  }
440
425
  function buildArtifactFilename(presetId, variantId, artifact) {
441
- const ext = artifact.mediaMode === 'dom'
442
- ? 'html'
443
- : artifact.mimeType === 'image/jpeg'
444
- ? 'jpg'
445
- : artifact.mimeType === 'image/png'
446
- ? 'png'
447
- : artifact.mimeType.includes('gif')
448
- ? 'gif'
449
- : artifact.mimeType.includes('mp4')
450
- ? 'mp4'
451
- : 'webm';
426
+ const ext = artifact.mimeType === 'image/jpeg'
427
+ ? 'jpg'
428
+ : artifact.mimeType === 'image/png'
429
+ ? 'png'
430
+ : artifact.mimeType.includes('gif')
431
+ ? 'gif'
432
+ : artifact.mimeType.includes('mp4')
433
+ ? 'mp4'
434
+ : 'webm';
452
435
  const stepToken = typeof artifact.stepIndex === 'number' ? `-${artifact.stepIndex}` : '';
453
436
  return `${sanitizeArtifactToken(presetId)}-${sanitizeArtifactToken(variantId)}${stepToken}.${ext}`;
454
437
  }
@@ -497,9 +480,6 @@ function sanitizeArtifactForTelemetry(artifact) {
497
480
  altText: redactTelemetryText(artifact.altText),
498
481
  captureUrl: redactUrl(artifact.captureUrl),
499
482
  elementSelector: undefined,
500
- domAssetUrls: artifact.domAssetUrls
501
- ?.map((entry) => redactUrl(entry))
502
- .filter((entry) => Boolean(entry)),
503
483
  };
504
484
  }
505
485
  function sanitizeHealerPatches(patches) {
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import fs from 'node:fs/promises';
6
6
  const require = createRequire(import.meta.url);
7
7
  const { version } = require('../package.json');
8
8
  import { logger } from './logger.js';
9
- import { writeConfig, deleteConfig, requireConfig, getConfigPath, DEFAULT_API_BASE_URL, getDefaultApiBaseUrl, getDefaultWsUrl, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, } from './cli-config.js';
9
+ import { writeConfig, deleteConfig, requireConfig, getConfigPath, DEFAULT_API_BASE_URL, getDefaultApiBaseUrl, getDefaultWsUrl, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, } from './cli-config.js';
10
10
  import { renderSkillSingleFile, writeSkillExport } from './skill-packaging.js';
11
11
  // ── Program definition ──────────────────────────────────────────────
12
12
  export const program = new Command();
@@ -14,6 +14,12 @@ program
14
14
  .name('autokap')
15
15
  .version(version)
16
16
  .description('AI-powered screenshot capture — local Playwright proxy');
17
+ function getProjectPrimaryUrl(project) {
18
+ return (project.environments.local ??
19
+ project.environments.staging ??
20
+ project.environments.prod ??
21
+ null);
22
+ }
17
23
  function fatal(message) {
18
24
  logger.error(message);
19
25
  process.exit(1);
@@ -205,6 +211,8 @@ program
205
211
  .description('Run a capture using the deterministic opcode engine (local Playwright)')
206
212
  .option('--headed', 'Show browser window for debugging', false)
207
213
  .option('--local', `Use the local AutoKap dev server (${LOCAL_API_BASE_URL})`, false)
214
+ .option('--env <name>', "Project environment to capture against. Falls back to the project's default environment when omitted.")
215
+ .option('--allow-upload-failure', 'Keep a successful capture exit code even if artifact upload fails', false)
208
216
  .option('--output <dir>', 'Optional output directory for local artifact copies')
209
217
  .option('--program <file>', 'Path to a program JSON file')
210
218
  .option('--debug', 'Verbose logging: per-substep timing, opcode dumps, recovery strategy traces', false)
@@ -222,6 +230,59 @@ program
222
230
  const { runLocal } = await import('./cli-runner-local.js');
223
231
  await runLocal(presetId, opts);
224
232
  });
233
+ // ── auto-recapture command ─────────────────────────────────────────
234
+ program
235
+ .command('auto-recapture')
236
+ .description('Run every preset enabled for CI auto-recapture in a project')
237
+ .requiredOption('--project <id>', 'Project ID')
238
+ .option('--env <name>', "Project environment to capture against. Falls back to the project's default environment when omitted.")
239
+ .option('--headed', 'Show browser window for debugging', false)
240
+ .option('--local', `Use the local AutoKap dev server (${LOCAL_API_BASE_URL})`, false)
241
+ .option('--allow-upload-failure', 'Keep a successful capture exit code even if artifact upload fails', false)
242
+ .option('--debug', 'Verbose logging: per-substep timing, opcode dumps, recovery strategy traces', false)
243
+ .action(async (opts) => {
244
+ if (opts.debug) {
245
+ const { setDebugEnabled } = await import('./logger.js');
246
+ setDebugEnabled(true);
247
+ logger.info('[capture] Debug mode enabled — verbose logging on');
248
+ }
249
+ if (opts.local) {
250
+ process.env[API_BASE_URL_ENV_VAR] = LOCAL_API_BASE_URL;
251
+ process.env[WS_URL_ENV_VAR] = LOCAL_WS_URL;
252
+ logger.info(`Using local AutoKap dev server: ${LOCAL_API_BASE_URL}`);
253
+ }
254
+ const config = await requireConfig();
255
+ const data = await requestJson(config, `/api/cli/projects/${opts.project}/auto-recapture-presets`, { headers: authHeaders(config) }, 'Failed to list auto-recapture presets');
256
+ if (data.presets.length === 0) {
257
+ logger.info(`[auto-recapture] No presets enabled for project ${opts.project}`);
258
+ process.exit(0);
259
+ }
260
+ const { runCapture } = await import('./cli-runner.js');
261
+ const failures = [];
262
+ for (const preset of data.presets) {
263
+ const label = preset.name ? `${preset.name} (${preset.id})` : preset.id;
264
+ logger.info(`[auto-recapture] Running ${label}`);
265
+ const result = await runCapture({
266
+ presetId: preset.id,
267
+ env: opts.env,
268
+ headed: opts.headed,
269
+ allowUploadFailure: opts.allowUploadFailure,
270
+ });
271
+ if (!result.success) {
272
+ const error = result.error ?? result.runResult?.error ?? 'capture failed';
273
+ failures.push({ id: preset.id, name: preset.name, error });
274
+ logger.error(`[auto-recapture] Failed ${label}: ${error}`);
275
+ }
276
+ }
277
+ if (failures.length > 0) {
278
+ logger.error(`[auto-recapture] ${failures.length}/${data.presets.length} preset(s) failed: ${failures
279
+ .map((failure) => failure.name ?? failure.id)
280
+ .join(', ')}`);
281
+ process.exit(1);
282
+ }
283
+ logger.success(`[auto-recapture] ${data.presets.length} preset(s) recaptured successfully`);
284
+ process.exit(0);
285
+ });
225
286
  // ── project commands ───────────────────────────────────────────────
226
287
  const projectCmd = program
227
288
  .command('project')
@@ -248,16 +309,23 @@ projectCmd
248
309
  .command('create')
249
310
  .description('Create a project')
250
311
  .requiredOption('--name <name>', 'Project name')
251
- .requiredOption('--url <url>', 'Project URL')
312
+ .requiredOption('--base-url <url>', 'Base URL of the chosen environment')
313
+ .option('--environment <name>', "Environment slot to seed: 'local' or 'prod'", 'local')
252
314
  .option('--description <text>', 'Project description')
253
315
  .action(async (opts) => {
254
316
  const config = await requireConfig();
317
+ const env = opts.environment.toLowerCase();
318
+ if (env !== 'local' && env !== 'prod') {
319
+ logger.error("--environment must be 'local' or 'prod'");
320
+ process.exit(1);
321
+ }
255
322
  const data = await requestJson(config, '/api/v1/projects', {
256
323
  method: 'POST',
257
324
  headers: authHeaders(config, { 'Content-Type': 'application/json' }),
258
325
  body: JSON.stringify({
259
326
  name: opts.name,
260
- url: opts.url,
327
+ environment: env,
328
+ base_url: opts.baseUrl,
261
329
  description: opts.description,
262
330
  }),
263
331
  }, 'Failed to create project');
@@ -492,19 +560,9 @@ presetCmd
492
560
  // Build per-endpoint info with type-specific URL params
493
561
  const endpointEntries = endpoints.map((ep) => {
494
562
  const assetType = ep.asset_type ?? 'screenshot';
495
- const url = assetType === 'interactive_demo'
496
- ? `${cfg.apiBaseUrl}/demo/${presetId}`
497
- : `${cfg.apiBaseUrl}/api/v1/assets/${ep.id}`;
563
+ const url = `${cfg.apiBaseUrl}/api/v1/assets/${ep.id}`;
498
564
  let urlParams;
499
- if (assetType === 'interactive_demo') {
500
- urlParams = {
501
- embed: 'Set to "1" for iframe embedding',
502
- lang: 'Language variant',
503
- theme: 'Color theme ("light" or "dark")',
504
- target: 'Device target ID',
505
- };
506
- }
507
- else if (assetType === 'screenshot') {
565
+ if (assetType === 'screenshot') {
508
566
  urlParams = {
509
567
  lang: 'Language variant',
510
568
  theme: 'Color theme ("light" or "dark")',
@@ -513,8 +571,6 @@ presetCmd
513
571
  quality: 'Image quality 1-100',
514
572
  format: 'Output format: "webp", "png", "jpg"',
515
573
  scale: 'Resolution multiplier (0.5-4)',
516
- render: 'Set to "studio" for Studio render',
517
- slot: 'Studio composition slot',
518
574
  };
519
575
  }
520
576
  else if (assetType === 'clip') {
@@ -523,14 +579,6 @@ presetCmd
523
579
  theme: 'Color theme ("light" or "dark")',
524
580
  target: 'Device target ID',
525
581
  format: 'Output format: "gif" (default), "mp4"',
526
- render: 'Set to "studio" for Studio render',
527
- slot: 'Studio composition slot',
528
- };
529
- }
530
- else if (assetType === 'composition') {
531
- urlParams = {
532
- scale: 'Resolution multiplier (0.5-4)',
533
- format: 'Output format: "png", "webp", "jpg"',
534
582
  };
535
583
  }
536
584
  else {
@@ -555,12 +603,6 @@ presetCmd
555
603
  variants: { langs, themes, targets },
556
604
  endpoints: endpointEntries,
557
605
  };
558
- if (captureMode === 'interactive_demo') {
559
- info.interactive_demo = {
560
- url: `${cfg.apiBaseUrl}/demo/${presetId}`,
561
- embed_url: `${cfg.apiBaseUrl}/demo/${presetId}?embed=1`,
562
- };
563
- }
564
606
  console.log(JSON.stringify(info, null, 2));
565
607
  process.exit(0);
566
608
  });
@@ -617,7 +659,15 @@ authCmd
617
659
  }
618
660
  process.exit(0);
619
661
  }
620
- const startUrl = opts.url || (await loadProject(cfg, projectId)).url;
662
+ let startUrl = opts.url;
663
+ if (!startUrl) {
664
+ const project = await loadProject(cfg, projectId);
665
+ startUrl = getProjectPrimaryUrl(project) ?? undefined;
666
+ }
667
+ if (!startUrl) {
668
+ logger.error(`No environment URL configured for project ${projectId}. Configure local/staging/prod in the AutoKap web UI, or pass --url.`);
669
+ process.exit(1);
670
+ }
621
671
  logger.info(`Opening ${startUrl}`);
622
672
  const { captureAuthSession } = await import('./auth-capture.js');
623
673
  await captureAuthSession({
@@ -840,25 +890,16 @@ const AGENT_PATHS = {
840
890
  windsurf: '.windsurf/rules/autokap-preset.md',
841
891
  copilot: '.github/instructions/autokap-preset.instructions.md',
842
892
  };
843
- const STUDIO_AGENT_PATHS = {
844
- claude: '.claude/commands/autokap-studio.md',
845
- codex: '.agents/skills/autokap-studio/SKILL.md',
846
- cursor: '.cursor/rules/autokap-studio.md',
847
- windsurf: '.windsurf/rules/autokap-studio.md',
848
- copilot: '.github/instructions/autokap-studio.instructions.md',
849
- };
850
893
  program
851
894
  .command('skill')
852
895
  .description('Output or install an AutoKap skill for AI coding agents')
853
896
  .option('--output <path>', 'Write the generated skill output to this path instead of stdout')
854
897
  .option('--agent <name>', 'Target AI coding agent: claude, codex, cursor, windsurf, copilot (auto-resolves output path and packaging mode)')
855
- .option('--type <type>', 'Skill type: preset (default) or studio (composition designer)')
856
898
  .option('--project-url <url>', 'Replace the project URL placeholder in the skill')
857
899
  .option('--project-id <id>', 'Replace the project ID placeholder in the skill')
858
900
  .option('--api-base-url <url>', 'Replace the API base URL placeholder (default: https://autokap.app)')
859
901
  .action(async (opts) => {
860
- const skillType = opts.type === 'studio' ? 'studio' : undefined;
861
- const pathMap = skillType === 'studio' ? STUDIO_AGENT_PATHS : AGENT_PATHS;
902
+ const pathMap = AGENT_PATHS;
862
903
  // Resolve --agent to an output path
863
904
  if (opts.agent) {
864
905
  const agentKey = opts.agent.toLowerCase();
@@ -875,7 +916,7 @@ program
875
916
  try {
876
917
  if (opts.output) {
877
918
  const result = await writeSkillExport({
878
- type: skillType === 'studio' ? 'studio' : 'preset',
919
+ type: 'preset',
879
920
  agent: opts.agent?.toLowerCase(),
880
921
  outputPath: opts.output,
881
922
  placeholders: opts,
@@ -889,7 +930,7 @@ program
889
930
  }
890
931
  else {
891
932
  const content = await renderSkillSingleFile({
892
- type: skillType === 'studio' ? 'studio' : 'preset',
933
+ type: 'preset',
893
934
  agent: opts.agent?.toLowerCase(),
894
935
  placeholders: opts,
895
936
  });
@@ -916,6 +957,10 @@ program
916
957
  // If --cli-key not provided, prompt interactively
917
958
  let cliKey = opts.cliKey;
918
959
  if (!cliKey) {
960
+ if (process.stdin.isTTY === false) {
961
+ logger.error(`CLI key is required in non-interactive shells. Pass --cli-key <key> or set ${API_KEY_ENV_VAR} for CI runs.`);
962
+ process.exit(1);
963
+ }
919
964
  const readline = await import('node:readline');
920
965
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
921
966
  cliKey = await new Promise((resolve) => {
@@ -7,7 +7,7 @@ export interface CostLogContext {
7
7
  presetId?: string;
8
8
  lang?: string;
9
9
  theme?: string;
10
- captureType?: 'fullpage' | 'element' | 'video';
10
+ captureType?: 'fullpage' | 'element';
11
11
  elementName?: string;
12
12
  targetId?: string;
13
13
  captureId?: string | null;