autokap 1.1.0 → 1.1.3

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 +1 -3
  6. package/dist/billing-operation-logging.js +0 -4
  7. package/dist/browser.js +164 -1
  8. package/dist/capture-strategy.d.ts +2 -8
  9. package/dist/capture-strategy.js +2 -30
  10. package/dist/cli-config.d.ts +2 -1
  11. package/dist/cli-config.js +18 -2
  12. package/dist/cli-contract.d.ts +1 -0
  13. package/dist/cli-contract.js +8 -2
  14. package/dist/cli-runner-local.d.ts +2 -0
  15. package/dist/cli-runner-local.js +12 -21
  16. package/dist/cli-runner.d.ts +4 -0
  17. package/dist/cli-runner.js +45 -53
  18. package/dist/cli.js +89 -44
  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 +2 -194
  39. package/package.json +2 -19
  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
@@ -24,7 +24,9 @@ export async function runLocal(presetId, opts) {
24
24
  }
25
25
  const run = await runCapture({
26
26
  presetId,
27
+ env: opts.env,
27
28
  program,
29
+ allowUploadFailure: opts.allowUploadFailure,
28
30
  headed: opts.headed,
29
31
  onProgress: (event) => {
30
32
  const prefix = `[capture][${event.variantId}]`;
@@ -73,27 +75,16 @@ async function persistArtifactsLocally(presetId, outputDirOption, variants) {
73
75
  for (const variant of variants) {
74
76
  for (let index = 0; index < variant.artifacts.length; index += 1) {
75
77
  const artifact = variant.artifacts[index];
76
- const ext = artifact.mediaMode === 'dom'
77
- ? 'html'
78
- : artifact.mimeType === 'image/png'
79
- ? 'png'
80
- : artifact.mimeType === 'image/jpeg'
81
- ? 'jpg'
82
- : artifact.mimeType.includes('gif')
83
- ? 'gif'
84
- : artifact.mimeType.includes('mp4')
85
- ? 'mp4'
86
- : 'webm';
87
- let suffix = '';
88
- if (artifact.mediaMode === 'dom') {
89
- if (artifact.fragmentName && artifact.parentStateName) {
90
- suffix = `-${sanitizePathToken(artifact.parentStateName)}-fragment-${sanitizePathToken(artifact.fragmentName)}`;
91
- }
92
- else if (artifact.stateName) {
93
- suffix = `-${sanitizePathToken(artifact.stateName)}`;
94
- }
95
- }
96
- const fileName = `capture-${sanitizePathToken(presetId.slice(0, 8))}-${sanitizePathToken(variant.variantId)}${suffix}-${index}.${ext}`;
78
+ const ext = artifact.mimeType === 'image/png'
79
+ ? 'png'
80
+ : artifact.mimeType === 'image/jpeg'
81
+ ? 'jpg'
82
+ : artifact.mimeType.includes('gif')
83
+ ? 'gif'
84
+ : artifact.mimeType.includes('mp4')
85
+ ? 'mp4'
86
+ : 'webm';
87
+ const fileName = `capture-${sanitizePathToken(presetId.slice(0, 8))}-${sanitizePathToken(variant.variantId)}-${index}.${ext}`;
97
88
  const filePath = resolveContainedPath(outputDir, fileName);
98
89
  await fs.writeFile(filePath, artifact.buffer);
99
90
  logger.info(`[capture] Artifact saved locally: ${filePath}`);
@@ -15,8 +15,12 @@ import type { ExecutionProgram, RunResult } from './execution-types.js';
15
15
  export interface CLIRunnerOptions {
16
16
  /** Preset ID to run */
17
17
  presetId: string;
18
+ /** Project environment name used by the server to resolve capture URLs */
19
+ env?: string;
18
20
  /** Override: provide program directly instead of fetching from server */
19
21
  program?: ExecutionProgram;
22
+ /** Keep the legacy success result when upload/telemetry persistence fails */
23
+ allowUploadFailure?: boolean;
20
24
  /** Selector memory map (fetched from server or cached locally) */
21
25
  selectorMemory?: Record<string, string[]>;
22
26
  /** Show browser window. Default: false (headless) */
@@ -26,6 +26,7 @@ import { logger } from './logger.js';
26
26
  import { callLLM } from './llm-provider.js';
27
27
  import { APP_VERSION } from './version.js';
28
28
  import { normalizeAllowedOrigins, normalizeHttpOrigin, verifySignedExecutionProgramEnvelope, } from './program-signing.js';
29
+ const MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR = 1;
29
30
  const HEALER_SYSTEM_PROMPT = 'You repair failed deterministic browser opcodes. Respond only with JSON.';
30
31
  // ── Main entry point ────────────────────────────────────────────────
31
32
  export async function runCapture(options) {
@@ -46,7 +47,7 @@ export async function runCapture(options) {
46
47
  };
47
48
  }
48
49
  else {
49
- const fetched = await fetchProgram(config, options.presetId);
50
+ const fetched = await fetchProgram(config, options.presetId, options.env);
50
51
  if (!fetched.success) {
51
52
  return { success: false, error: fetched.error };
52
53
  }
@@ -109,18 +110,26 @@ export async function runCapture(options) {
109
110
  logger.info(`[capture] Concurrency cap resolved to ${program.maxParallelCaptures} parallel variant(s)`);
110
111
  }
111
112
  const createAdapter = async (variant) => {
113
+ const recordable = program.mediaMode === 'clip';
114
+ const requestedDeviceScaleFactor = variant.deviceScaleFactor ?? program.outputScale ?? 2;
115
+ const runtimeDeviceScaleFactor = recordable && Number.isFinite(requestedDeviceScaleFactor)
116
+ ? Math.min(Number(requestedDeviceScaleFactor), MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR)
117
+ : requestedDeviceScaleFactor;
112
118
  const browserOptions = {
113
119
  headed: options.headed ?? false,
114
120
  viewport: variant.viewport,
115
- deviceScaleFactor: variant.deviceScaleFactor ?? program.outputScale,
121
+ deviceScaleFactor: runtimeDeviceScaleFactor,
116
122
  lang: variant.locale,
117
123
  colorScheme: variant.theme,
118
124
  storageState: program.preconditions.storageState,
119
125
  };
120
- const recordable = program.mediaMode === 'clip';
121
126
  let recordingDir;
122
127
  let browser;
123
128
  logger.info(`[capture] Launching browser${browserOptions.headed ? ' (headed)' : ''}…`);
129
+ if (recordable && runtimeDeviceScaleFactor !== requestedDeviceScaleFactor) {
130
+ logger.info(`[capture] Clip capture scale capped at ${runtimeDeviceScaleFactor} ` +
131
+ `(requested ${requestedDeviceScaleFactor}) to preserve recording FPS`);
132
+ }
124
133
  if (recordable) {
125
134
  recordingDir = await fs.mkdtemp(path.join(os.tmpdir(), `autokap-${program.mediaMode}-`));
126
135
  browser = await Browser.forClipCapture(browserOptions, buildCursorOverlayScript(program.artifactPlan.cursorTheme ?? 'minimal'));
@@ -151,15 +160,29 @@ export async function runCapture(options) {
151
160
  logger.info(`[capture] Captures saved successfully — total ${totalDurationSec}s`);
152
161
  }
153
162
  catch (err) {
154
- logger.error(`[capture] Failed to upload results: ${err instanceof Error ? err.message : String(err)}`);
163
+ const message = err instanceof Error ? err.message : String(err);
164
+ logger.error(`[capture] Failed to upload results: ${message}`);
165
+ if (!options.allowUploadFailure) {
166
+ return {
167
+ success: false,
168
+ runResult,
169
+ error: runResult.success
170
+ ? `upload failed: ${message}`
171
+ : `${runResult.error ?? 'capture failed'}; upload failed: ${message}`,
172
+ };
173
+ }
174
+ logger.warn('[capture] Continuing after upload failure because --allow-upload-failure was set');
155
175
  }
156
176
  return { success: runResult.success, runResult };
157
177
  }
158
178
  // ── Server communication ────────────────────────────────────────────
159
- async function fetchProgram(config, presetId) {
179
+ async function fetchProgram(config, presetId, environmentName) {
160
180
  try {
161
- const url = `${config.apiBaseUrl}/api/cli/programs/${presetId}`;
162
- const response = await fetch(url, {
181
+ const url = new URL(`${config.apiBaseUrl}/api/cli/programs/${presetId}`);
182
+ if (environmentName) {
183
+ url.searchParams.set('env', environmentName);
184
+ }
185
+ const response = await fetch(url.toString(), {
163
186
  headers: {
164
187
  'Authorization': `Bearer ${config.apiKey}`,
165
188
  'Content-Type': 'application/json',
@@ -167,7 +190,7 @@ async function fetchProgram(config, presetId) {
167
190
  },
168
191
  });
169
192
  if (!response.ok) {
170
- return { success: false, error: await formatServerError(response, url) };
193
+ return { success: false, error: await formatServerError(response, url.toString()) };
171
194
  }
172
195
  const data = await response.json();
173
196
  const envelope = verifySignedExecutionProgramEnvelope({
@@ -195,7 +218,7 @@ async function uploadResults(config, program, result) {
195
218
  const formData = new FormData();
196
219
  const filename = buildArtifactFilename(program.presetId, variant.variantId, artifact);
197
220
  uploadedCount += 1;
198
- const label = artifact.captureName ?? artifact.clipName ?? artifact.fragmentName ?? artifact.stateName ?? filename;
221
+ const label = artifact.captureName ?? artifact.clipName ?? filename;
199
222
  logger.info(`[capture] Exporting capture ${uploadedCount}/${totalArtifacts}: ${label}`);
200
223
  formData.append('file', new Blob([new Uint8Array(artifact.buffer)], { type: artifact.mimeType }), filename);
201
224
  formData.append('presetId', program.presetId);
@@ -214,7 +237,10 @@ async function uploadResults(config, program, result) {
214
237
  if (variantSpec?.deviceFrame) {
215
238
  formData.append('deviceFrame', variantSpec.deviceFrame);
216
239
  }
217
- const deviceScaleFactor = variantSpec?.deviceScaleFactor ?? program.outputScale;
240
+ const requestedDeviceScaleFactor = variantSpec?.deviceScaleFactor ?? program.outputScale ?? 2;
241
+ const deviceScaleFactor = artifact.mediaMode === 'clip' && Number.isFinite(requestedDeviceScaleFactor)
242
+ ? Math.min(Number(requestedDeviceScaleFactor), MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR)
243
+ : requestedDeviceScaleFactor;
218
244
  if (Number.isFinite(deviceScaleFactor)) {
219
245
  formData.append('deviceScaleFactor', String(deviceScaleFactor));
220
246
  }
@@ -253,35 +279,6 @@ async function uploadResults(config, program, result) {
253
279
  if (typeof artifact.trimStartMs === 'number') {
254
280
  formData.append('trimStartMs', String(artifact.trimStartMs));
255
281
  }
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
282
  const response = await fetch(`${config.apiBaseUrl}/api/cli/artifacts`, {
286
283
  method: 'POST',
287
284
  headers: { 'Authorization': `Bearer ${config.apiKey}` },
@@ -438,17 +435,15 @@ function createHealerLLMProvider(llmConfig) {
438
435
  };
439
436
  }
440
437
  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';
438
+ const ext = artifact.mimeType === 'image/jpeg'
439
+ ? 'jpg'
440
+ : artifact.mimeType === 'image/png'
441
+ ? 'png'
442
+ : artifact.mimeType.includes('gif')
443
+ ? 'gif'
444
+ : artifact.mimeType.includes('mp4')
445
+ ? 'mp4'
446
+ : 'webm';
452
447
  const stepToken = typeof artifact.stepIndex === 'number' ? `-${artifact.stepIndex}` : '';
453
448
  return `${sanitizeArtifactToken(presetId)}-${sanitizeArtifactToken(variantId)}${stepToken}.${ext}`;
454
449
  }
@@ -497,9 +492,6 @@ function sanitizeArtifactForTelemetry(artifact) {
497
492
  altText: redactTelemetryText(artifact.altText),
498
493
  captureUrl: redactUrl(artifact.captureUrl),
499
494
  elementSelector: undefined,
500
- domAssetUrls: artifact.domAssetUrls
501
- ?.map((entry) => redactUrl(entry))
502
- .filter((entry) => Boolean(entry)),
503
495
  };
504
496
  }
505
497
  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) => {