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
@@ -1,6 +1,6 @@
1
1
  # AutoKap Opcode Reference
2
2
 
3
- Detailed parameter documentation for all 25 opcodes. For workflow, rules, and examples, see [SKILL.md](SKILL.md).
3
+ Detailed parameter documentation for all 23 opcodes. For workflow, rules, and examples, see [SKILL.md](SKILL.md).
4
4
 
5
5
  ## Common Fields (all opcodes)
6
6
 
@@ -375,46 +375,6 @@ Capture a screenshot of the viewport or a specific element.
375
375
  { "kind": "CAPTURE_SCREENSHOT", "captureId": "dashboard-main", "captureName": "Dashboard", "elementSelector": "[data-ak=\"main-content\"]", "postcondition": { "type": "always" } }
376
376
  ```
377
377
 
378
- ## CAPTURE_DOM
379
-
380
- Capture a DOM snapshot for an interactive demo state.
381
-
382
- | Param | Type | Required | Description |
383
- |-------|------|----------|-------------|
384
- | `stateName` | string | yes | Stable state identifier used by the interactive demo player |
385
- | `selector` | string | no | CSS selector of a focused subtree to serialize instead of the whole page |
386
-
387
- **Can:** Capture the full page DOM for a state, or a focused subtree when only part of the page matters. Feed the interactive demo player with a state that can later host fragments and transitions.
388
- **Cannot:** Produce screenshots or clips. Use only when `mediaMode` is `dom`.
389
-
390
- **Tip:** Omit `selector` for page-level states. Add it when the interactive demo should ignore surrounding chrome and serialize only a mounted area.
391
-
392
- ```json
393
- { "kind": "CAPTURE_DOM", "stateName": "dashboard-default", "selector": "[data-ak=\"dashboard-shell\"]", "postcondition": { "type": "always" } }
394
- ```
395
-
396
- ## CAPTURE_FRAGMENT
397
-
398
- Capture a local DOM fragment that mounts on top of a previously captured state.
399
-
400
- | Param | Type | Required | Description |
401
- |-------|------|----------|-------------|
402
- | `fragmentName` | string | yes | Stable fragment identifier |
403
- | `parentState` | string | yes | Name of the CAPTURE_DOM state this fragment belongs to |
404
- | `selector` | string | yes | CSS selector of the fragment root to serialize |
405
- | `variantName` | string | no | Optional named fragment variant for in-place swaps |
406
- | `triggerSelector` | string | no | Selector of the element that should trigger this fragment in the player |
407
- | `mountStrategy` | `"replace"` \| `"append"` | no | How the player should mount the fragment relative to the parent state |
408
-
409
- **Can:** Capture modals, popovers, dropdowns, drawers, or any local subtree that should be replayed on top of a parent state. Capture multiple variants of the same fragment to enable in-place swaps in the player.
410
- **Cannot:** Stand alone without a `parentState`. Replace full-page routing states; use CAPTURE_DOM for those.
411
-
412
- **Tip:** Use `variantName` when the same fragment can appear in multiple visual forms, such as color/theme swaps or alternate content blocks on the same background state.
413
-
414
- ```json
415
- { "kind": "CAPTURE_FRAGMENT", "fragmentName": "pricing-modal", "parentState": "dashboard-default", "selector": "[data-ak=\"pricing-modal\"]", "triggerSelector": "[data-ak=\"open-pricing\"]", "mountStrategy": "append", "postcondition": { "type": "always" } }
416
- ```
417
-
418
378
  ## BEGIN_CLIP
419
379
 
420
380
  Start recording a clip. All interactions between BEGIN_CLIP and END_CLIP are recorded.
@@ -8,7 +8,6 @@ Edit files here first.
8
8
  - `SKILL.md` is the core preset contract and must stay short enough to remain
9
9
  readable.
10
10
  - `references/*.md` holds longer mode-specific guidance and examples.
11
- - `STUDIO-SKILL.md` is the separate studio/composition skill.
12
11
  - `src/skill-packaging.ts` compiles these sources into the outputs each agent
13
12
  expects.
14
13
 
@@ -2,7 +2,7 @@
2
2
  name: autokap-preset
3
3
  description: >
4
4
  Generate AutoKap capture programs — deterministic opcode sequences for automated
5
- screenshot, clip, and interactive demo capture of web apps. Use when: creating or updating presets,
5
+ screenshot and clip capture of web apps. Use when: creating or updating presets,
6
6
  adding data-ak attributes for capture automation, or debugging failed capture programs.
7
7
  metadata:
8
8
  author: AutoKap
@@ -21,7 +21,7 @@ This installed skill is the **source of truth** for the AutoKap contract: opcode
21
21
 
22
22
  ## When To Use This Skill
23
23
 
24
- - User wants to capture screenshots, clips, or interactive demos of their web app
24
+ - User wants to capture screenshots or clips of their web app
25
25
  - User asks to create or update an AutoKap preset
26
26
  - User needs `data-ak` attributes added to UI elements for capture
27
27
  - User is debugging a failed capture program
@@ -38,7 +38,7 @@ This installed skill is the **source of truth** for the AutoKap contract: opcode
38
38
 
39
39
  ## Quick Workflow
40
40
 
41
- 1. **Understand the capture goal** — What pages/states should be captured? Screenshot, clip, or interactive demo? Which viewports, locales, themes, and mockups matter?
41
+ 1. **Understand the capture goal** — What pages/states should be captured? Screenshot or clip? Which viewports, locales, themes, and mockups matter?
42
42
  2. **Inspect the implementation** — Confirm routes, auth, theme, locale, and any dynamic UI state in the codebase.
43
43
  3. **Add `data-ak` attributes** — Tag every element the opcodes must interact with using stable selectors.
44
44
  4. **Choose media mode and variants** — Set `mediaMode` and define the exact viewport/locale/theme combinations.
@@ -52,7 +52,6 @@ This installed skill is the **source of truth** for the AutoKap contract: opcode
52
52
  Load these only when the request actually needs them:
53
53
 
54
54
  - **Opcode parameters** — [OPCODE-REFERENCE.md](OPCODE-REFERENCE.md)
55
- - **Interactive demos** — [references/interactive-demo.md](references/interactive-demo.md)
56
55
  - **Mock data** — [references/mock-data.md](references/mock-data.md)
57
56
  - **Complete examples** — [references/examples.md](references/examples.md)
58
57
 
@@ -91,7 +90,7 @@ For every element you interact with in the opcodes, add a `data-ak="descriptive-
91
90
  interface ExecutionProgram {
92
91
  presetId: string; // Unique slug (e.g. "homepage-hero")
93
92
  programVersion: number; // Always 1 for new programs
94
- mediaMode: 'screenshot' | 'clip' | 'dom';
93
+ mediaMode: 'screenshot' | 'clip';
95
94
  baseUrl: string; // Root URL of the application
96
95
  variants: VariantSpec[]; // Viewport/locale/theme combinations
97
96
  preconditions: {
@@ -105,7 +104,7 @@ interface ExecutionProgram {
105
104
  };
106
105
  steps: ExecutionOpcode[];
107
106
  artifactPlan: {
108
- mediaMode: 'screenshot' | 'clip' | 'dom';
107
+ mediaMode: 'screenshot' | 'clip';
109
108
  cursorTheme?: 'minimal' | 'macos' | 'windows'; // Clip only. Default: 'minimal'
110
109
  format?: {
111
110
  clipFormat?: 'gif' | 'mp4' | 'both'; // Default: 'gif'
@@ -134,7 +133,7 @@ interface VariantSpec {
134
133
 
135
134
  ## Opcode Quick Reference
136
135
 
137
- 25 opcodes available. For full parameter documentation, see [OPCODE-REFERENCE.md](OPCODE-REFERENCE.md).
136
+ 23 opcodes available. For full parameter documentation, see [OPCODE-REFERENCE.md](OPCODE-REFERENCE.md).
138
137
 
139
138
  | Kind | Selector? | Key Params | Typical Postcondition | Notes |
140
139
  |------|-----------|-----------|----------------------|-------|
@@ -155,8 +154,6 @@ interface VariantSpec {
155
154
  | `ASSERT_ROUTE` | no | `urlPattern` | `route_matches` | Validation checkpoint |
156
155
  | `ASSERT_SURFACE` | no | `selectors[]`, `matchAll` | `always` | Validation checkpoint |
157
156
  | `CAPTURE_SCREENSHOT` | no | `captureId`, `captureName`, `elementSelector?` | `always` | `elementSelector` for element-level crop |
158
- | `CAPTURE_DOM` | no | `stateName`, `selector?` | `always` | Interactive Demo state — see [Interactive Demo Workflow](#interactive-demo-workflow). `mediaMode: "dom"` only. Add `selector` to capture a focused subtree instead of the whole page. |
159
- | `CAPTURE_FRAGMENT` | yes | `fragmentName`, `parentState`, `selector`, `variantName?`, `triggerSelector?`, `mountStrategy?` | `always` | Interactive Demo fragment (modal/popover/dropdown/local subtree) — see [Fragments](#fragments-and-local-interactions). Mounted on top of `parentState` by the player. Capture the same fragment under multiple `variantName`s to enable in-place swap (e.g. background colour change). |
160
157
  | `BEGIN_CLIP` | no | `clipId`, `clipName` | `always` | Start recording |
161
158
  | `END_CLIP` | no | `clipId`, `clipName` | `always` | Stop recording. Same `clipId` as BEGIN_CLIP |
162
159
  | `CLONE_ELEMENT` | yes | `sourceSelector`, `containerSelector`, `count` | `always` | **Non-blocking.** Duplicate a template element N times |
@@ -284,25 +281,6 @@ Place this step **after** DISMISS_OVERLAYS + any content WAIT_FOR, and **immedia
284
281
  }
285
282
  ```
286
283
 
287
- ## Interactive Demo Workflow
288
-
289
- Interactive demos are advanced and should only be used when the user wants a
290
- clickable DOM-based experience, not static screenshots or a clip.
291
-
292
- Key rules:
293
-
294
- - center the capture around the feature loop, not the whole app
295
- - use `CAPTURE_DOM` for base states
296
- - use `CAPTURE_FRAGMENT` for local overlays and subtree swaps
297
- - add authored markers such as `data-ak-interact`, `data-ak-fragment`,
298
- `data-ak-model`, and `data-ak-template`
299
- - prefer fragments / bindings / model-driven reconstruction before custom
300
- interaction `code`
301
-
302
- Read the full reference before generating an interactive demo:
303
-
304
- - [references/interactive-demo.md](references/interactive-demo.md)
305
-
306
284
  ## Recovery System Overview
307
285
 
308
286
  When an opcode fails, AutoKap tries 5 recovery strategies in order:
@@ -358,7 +336,7 @@ Read the full reference before adding mock data:
358
336
  4. **CLICK postconditions describe the result**, not the action (what changed, not what was clicked)
359
337
  5. **Add `WAIT_FOR` after page transitions** (login, route change, modal open)
360
338
  6. **Set `waitMs: 10000`** on postconditions involving async transitions
361
- 7. **Persist the preset via the CLI — the program lives INSIDE `config.program`**, not in a separate file. After generating the program JSON, write the full config to a temp file and run `autokap preset create` or `autokap preset update` so the saved preset contains `program: { ...the full ExecutionProgram... }` plus any `interactiveDemo` / `mockDataInjection` blocks. The user MUST be able to run `autokap run <preset-id>` afterwards with no `--program` flag and no extra files. **Never tell the user to save the JSON to a local file**, never suggest `--program <file>` as the normal run path. The CLI fetches the program from the server.
339
+ 7. **Persist the preset via the CLI — the program lives INSIDE `config.program`**, not in a separate file. After generating the program JSON, write the full config to a temp file and run `autokap preset create` or `autokap preset update` so the saved preset contains `program: { ...the full ExecutionProgram... }` plus any `mockDataInjection` blocks. The user MUST be able to run `autokap run <preset-id>` afterwards with no `--program` flag and no extra files. **Never tell the user to save the JSON to a local file**, never suggest `--program <file>` as the normal run path. The CLI fetches the program from the server.
362
340
  8. **Use `captureId`/`clipId`** on CAPTURE_SCREENSHOT/BEGIN_CLIP for Studio and dev links to work. Always set `"devLinksEnabled": true` in the preset config when creating via API so endpoints are visible on the dashboard
363
341
  9. **Mock data opcodes are non-blocking** — they log a warning and continue if selectors miss; always pair them with `recovery: { retries: 0, ... }` and `postcondition: { type: "always" }`
364
342
  10. **Hardcode the login URL — never use `{{loginUrl}}`.** The login URL is just another navigation. You generate the entire program, so you know which path the app's login lives at — write it directly (`https://app.example.com/login`, `http://localhost:3000/login`, etc.). The `{{loginUrl}}` placeholder is **deprecated** and produces broken navigations when the user hasn't filled the optional `loginUrl` credential field. `{{email}}` and `{{password}}` placeholders for `TYPE` opcodes are still correct and required (those are sensitive secrets the user fills in).
@@ -408,7 +386,7 @@ Use the AutoKap CLI commands to create, update, and query presets. The CLI reads
408
386
 
409
387
  ### Step 1: Write the config JSON to a temp file
410
388
 
411
- Build the full config object (with `program`, `pages`, and optionally `interactiveDemo`/`mockDataInjection`) and write it to a temporary file:
389
+ Build the full config object (with `program`, `pages`, and optionally `mockDataInjection`) and write it to a temporary file:
412
390
 
413
391
  ```bash
414
392
  cat > /tmp/autokap-preset.json << 'EOF'
@@ -441,7 +419,7 @@ EOF
441
419
  - **`pages`** — one entry per `CAPTURE_SCREENSHOT` opcode (`{ "id": "<captureId>", "name": "<captureName>" }`). Drives endpoint (dev link) creation. **Screenshot presets only.**
442
420
  - **`clips`** — one entry per `BEGIN_CLIP`/`END_CLIP` pair (`{ "id": "<clipId>", "name": "<clipName>" }`). Drives endpoint and Studio slot creation. **Clip presets only.** Do NOT put clip entries in `pages` — the server treats `pages` as screenshots.
443
421
  - **`program`** — the full `ExecutionProgram` with `steps`, `variants`, `artifactPlan`, etc.
444
- - **`captureMode`** — `"screenshot"`, `"clip"`, or `"interactive_demo"`.
422
+ - **`captureMode`** — `"screenshot"` or `"clip"`.
445
423
  - **`devLinksEnabled: true`** — enables endpoints on the dashboard.
446
424
 
447
425
  The server auto-syncs `langs`, `themes`, `targets`, and `viewports` from `program.variants` — you do NOT need to set these manually.
@@ -553,7 +531,6 @@ Use the examples reference for ready-made shapes of:
553
531
  - anonymous screenshot presets
554
532
  - authenticated screenshot presets
555
533
  - clip presets
556
- - interactive demo presets
557
534
  - mock-data-enabled presets
558
535
 
559
536
  Read:
@@ -59,23 +59,7 @@ Pattern:
59
59
 
60
60
  Prefer short, deterministic flows.
61
61
 
62
- ## Example 4 — Interactive demo preset
63
-
64
- Good for:
65
-
66
- - embeddable product demos
67
- - focused feature loops
68
- - local UI swaps and fragment-driven demos
69
-
70
- Pattern:
71
-
72
- 1. `mediaMode: "dom"`
73
- 2. capture a small number of base states with `CAPTURE_DOM`
74
- 3. capture overlays/local subtrees with `CAPTURE_FRAGMENT`
75
- 4. wire `interactiveDemo.script`
76
- 5. use fragments, model bindings, and small `code` blocks for reconstruction
77
-
78
- ## Example 5 — Mock data preset
62
+ ## Example 4 — Mock data preset
79
63
 
80
64
  Good for:
81
65
 
@@ -1,6 +1,6 @@
1
1
  import type { SupabaseClient } from '@supabase/supabase-js';
2
2
  import type { StepUsage } from './types.js';
3
- export type BillingOperationType = 'screenshot' | 'clip' | 'interactive_demo';
3
+ export type BillingOperationType = 'screenshot' | 'clip';
4
4
  type BillingOperationOutcome = 'succeeded' | 'failed' | 'cancelled';
5
5
  interface BillingOperationContext {
6
6
  runId: string;
@@ -10,7 +10,6 @@ interface BillingOperationContext {
10
10
  captureId?: string | null;
11
11
  videoId?: string | null;
12
12
  clipRecordId?: string | null;
13
- interactiveDemoStateId?: string | null;
14
13
  operationType: BillingOperationType;
15
14
  captureType?: 'fullpage' | 'element' | null;
16
15
  lang?: string | null;
@@ -33,7 +32,6 @@ interface BillingOperationParams {
33
32
  export declare function insertBillingOperationLog(supabase: SupabaseClient, ctx: BillingOperationContext, params: BillingOperationParams): Promise<string | null>;
34
33
  export declare function insertScreenshotOperationLog(supabase: SupabaseClient, ctx: Omit<BillingOperationContext, 'operationType'>, params: BillingOperationParams): Promise<string | null>;
35
34
  export declare function insertClipOperationLog(supabase: SupabaseClient, ctx: Omit<BillingOperationContext, 'operationType'>, params: BillingOperationParams): Promise<string | null>;
36
- export declare function insertInteractiveDemoOperationLog(supabase: SupabaseClient, ctx: Omit<BillingOperationContext, 'operationType'>, params: BillingOperationParams): Promise<string | null>;
37
35
  export declare function cancelScreenshotOperationLogsForCapture(supabase: SupabaseClient, captureId: string, reason: string): Promise<void>;
38
36
  export declare function reconcilePendingBillingOperationCosts(supabase: SupabaseClient, operationIds?: string[]): Promise<void>;
39
37
  export {};
@@ -131,7 +131,6 @@ export async function insertBillingOperationLog(supabase, ctx, params) {
131
131
  capture_id: ctx.captureId ?? null,
132
132
  video_id: ctx.videoId ?? null,
133
133
  clip_record_id: ctx.clipRecordId ?? null,
134
- interactive_demo_state_id: ctx.interactiveDemoStateId ?? null,
135
134
  operation_type: ctx.operationType,
136
135
  operation_outcome: params.outcome,
137
136
  outcome_reason: params.outcomeReason ?? null,
@@ -178,9 +177,6 @@ export async function insertScreenshotOperationLog(supabase, ctx, params) {
178
177
  export async function insertClipOperationLog(supabase, ctx, params) {
179
178
  return insertBillingOperationLog(supabase, { ...ctx, operationType: 'clip' }, params);
180
179
  }
181
- export async function insertInteractiveDemoOperationLog(supabase, ctx, params) {
182
- return insertBillingOperationLog(supabase, { ...ctx, operationType: 'interactive_demo' }, params);
183
- }
184
180
  export async function cancelScreenshotOperationLogsForCapture(supabase, captureId, reason) {
185
181
  try {
186
182
  const { error } = await supabase
package/dist/browser.js CHANGED
@@ -98,7 +98,7 @@ function resolveEffectivePadding(config, bbox) {
98
98
  }
99
99
  import { dismissCookiesAndWidgets, ensureCaptureHideStyles } from './cookie-dismiss.js';
100
100
  import { CHROMIUM_ARGS, browserPool } from './browser-pool.js';
101
- import { logger } from './logger.js';
101
+ import { isDebugEnabled, logger } from './logger.js';
102
102
  async function withHelperTimeout(label, timeoutMs, work) {
103
103
  if (!timeoutMs || timeoutMs <= 0) {
104
104
  return work();
@@ -117,6 +117,82 @@ async function withHelperTimeout(label, timeoutMs, work) {
117
117
  clearTimeout(timer);
118
118
  }
119
119
  }
120
+ function isLikelyFontUrl(url) {
121
+ return /\.(?:woff2?|ttf|otf)(?:[?#]|$)/i.test(url);
122
+ }
123
+ async function logFontDiagnostics(page, stage) {
124
+ if (!isDebugEnabled())
125
+ return;
126
+ try {
127
+ const diagnostics = await page.evaluate(async () => {
128
+ const fontPreloads = Array.from(document.querySelectorAll('link[rel="preload"][as="font"], link[as="font"]')).map((link) => ({
129
+ href: link.href,
130
+ type: link.type || null,
131
+ crossOrigin: link.crossOrigin || null,
132
+ }));
133
+ const fetches = [];
134
+ for (const preload of fontPreloads) {
135
+ const startedAt = performance.now();
136
+ try {
137
+ const response = await fetch(preload.href, {
138
+ cache: 'no-store',
139
+ credentials: 'include',
140
+ });
141
+ fetches.push({
142
+ url: preload.href,
143
+ ok: response.ok,
144
+ status: response.status,
145
+ contentType: response.headers.get('content-type'),
146
+ contentLength: response.headers.get('content-length'),
147
+ durationMs: Math.round(performance.now() - startedAt),
148
+ });
149
+ }
150
+ catch (error) {
151
+ fetches.push({
152
+ url: preload.href,
153
+ durationMs: Math.round(performance.now() - startedAt),
154
+ error: error instanceof Error ? error.message : String(error),
155
+ });
156
+ }
157
+ }
158
+ const normalizeFamily = (family) => family.trim().replace(/^['"]|['"]$/g, '');
159
+ const computedFamilies = Array.from(new Set([document.body, ...Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6,p,a,button,input,textarea,label,span,[data-ak]')).slice(0, 40)]
160
+ .filter(Boolean)
161
+ .flatMap((node) => getComputedStyle(node).fontFamily
162
+ .split(',')
163
+ .map(normalizeFamily)
164
+ .filter(Boolean))));
165
+ const fontChecks = computedFamilies.map((family) => ({
166
+ family,
167
+ weight400: document.fonts.check(`400 16px "${family}"`, 'AutoKap Aa 0123456789'),
168
+ weight600: document.fonts.check(`600 16px "${family}"`, 'AutoKap Aa 0123456789'),
169
+ }));
170
+ return {
171
+ url: location.href,
172
+ count: document.fonts.size,
173
+ status: document.fonts.status,
174
+ faces: Array.from(document.fonts).map((font) => ({
175
+ family: font.family,
176
+ weight: font.weight,
177
+ style: font.style,
178
+ stretch: font.stretch,
179
+ status: font.status,
180
+ })),
181
+ bodyComputed: getComputedStyle(document.body).fontFamily,
182
+ documentElementClass: document.documentElement.className,
183
+ bodyClass: document.body.className,
184
+ computedFamilies,
185
+ fontChecks,
186
+ preloads: fontPreloads,
187
+ fetches,
188
+ };
189
+ });
190
+ logger.debug(`[capture] font diagnostics ${stage}: ${JSON.stringify(diagnostics)}`);
191
+ }
192
+ catch (error) {
193
+ logger.debug(`[capture] font diagnostics ${stage} failed: ${error instanceof Error ? error.message : String(error)}`);
194
+ }
195
+ }
120
196
  /**
121
197
  * Map a BCP-47 language tag to a Playwright-compatible locale string.
122
198
  * Playwright accepts both "fr" and "fr-FR". We normalize 2-char codes to their
@@ -401,6 +477,8 @@ export class Browser {
401
477
  attachDebugLifecycleListeners() {
402
478
  if (!this.page || !this.context)
403
479
  return;
480
+ if (!isDebugEnabled())
481
+ return;
404
482
  const page = this.page;
405
483
  const context = this.context;
406
484
  page.on('crash', () => {
@@ -423,6 +501,21 @@ export class Browser {
423
501
  logger.debug(`[page] console.${type}: ${msg.text().slice(0, 200)}`);
424
502
  }
425
503
  });
504
+ page.on('requestfailed', (request) => {
505
+ const url = request.url();
506
+ if (request.resourceType() !== 'font' && !isLikelyFontUrl(url))
507
+ return;
508
+ logger.debug(`[page] font request failed: ${url} — ${request.failure()?.errorText ?? 'unknown error'}`);
509
+ });
510
+ page.on('response', (response) => {
511
+ const request = response.request();
512
+ const url = response.url();
513
+ if (request.resourceType() !== 'font' && !isLikelyFontUrl(url))
514
+ return;
515
+ const headers = response.headers();
516
+ logger.debug(`[page] font response: status=${response.status()} type=${headers['content-type'] ?? 'unknown'} ` +
517
+ `length=${headers['content-length'] ?? 'unknown'} url=${url}`);
518
+ });
426
519
  context.on('close', () => {
427
520
  logger.debug(`[context] CLOSE event`);
428
521
  });
@@ -466,6 +559,7 @@ export class Browser {
466
559
  this.context = await this.browser.newContext(this.buildContextOptions());
467
560
  this.page = await this.context.newPage();
468
561
  this.elementMap.clear();
562
+ this.attachDebugLifecycleListeners();
469
563
  }
470
564
  async setDeviceScaleFactor(deviceScaleFactor) {
471
565
  const normalizedScale = normalizeDeviceScaleFactor(deviceScaleFactor);
@@ -600,6 +694,75 @@ export class Browser {
600
694
  // Move cursor off-screen to avoid hover effects in screenshots
601
695
  await page.mouse.move(0, 0);
602
696
  await ensureCaptureHideStyles(page);
697
+ // Wait for web fonts to be loaded AND applied to the rendered page.
698
+ // next/font and other `font-display: swap` setups can report the
699
+ // FontFaceSet as "ready" while visible text is still painted with fallback.
700
+ // We force-load declared and currently computed families, verify the
701
+ // primary rendered families with document.fonts.check(), then wait for a
702
+ // committed repaint.
703
+ await page.evaluate(() => Promise.race([
704
+ (async () => {
705
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
706
+ const nextPaint = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
707
+ const normalizeFamily = (family) => family.trim().replace(/^['"]|['"]$/g, '');
708
+ const computedFamilies = () => {
709
+ const families = new Set();
710
+ const nodes = [
711
+ document.body,
712
+ ...Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6,p,a,button,input,textarea,label,span,[data-ak]')).slice(0, 80),
713
+ ].filter(Boolean);
714
+ for (const node of nodes) {
715
+ const stack = getComputedStyle(node).fontFamily;
716
+ for (const family of stack.split(',')) {
717
+ const normalized = normalizeFamily(family);
718
+ if (normalized
719
+ && !/^(serif|sans-serif|monospace|system-ui|ui-sans-serif|ui-monospace)$/i.test(normalized)) {
720
+ families.add(normalized);
721
+ }
722
+ }
723
+ }
724
+ return families;
725
+ };
726
+ const allFamilies = () => new Set([
727
+ ...Array.from(document.fonts).map((font) => font.family),
728
+ ...computedFamilies(),
729
+ ]);
730
+ const probe = document.createElement('div');
731
+ probe.setAttribute('aria-hidden', 'true');
732
+ probe.style.cssText = 'position:fixed;left:-9999px;top:-9999px;visibility:hidden;pointer-events:none;';
733
+ for (const family of allFamilies()) {
734
+ const span = document.createElement('span');
735
+ span.style.fontFamily = `"${family}"`;
736
+ span.textContent = 'AutoKap Aa 0123456789';
737
+ probe.appendChild(span);
738
+ }
739
+ document.body.appendChild(probe);
740
+ void probe.offsetHeight;
741
+ const sample = 'AutoKap Aa 0123456789';
742
+ await Promise.all([
743
+ ...Array.from(document.fonts).map((font) => font.load().catch(() => null)),
744
+ ...Array.from(allFamilies()).flatMap((family) => [
745
+ document.fonts.load(`400 16px "${family}"`, sample).catch(() => null),
746
+ document.fonts.load(`600 16px "${family}"`, sample).catch(() => null),
747
+ ]),
748
+ ]);
749
+ await document.fonts.ready;
750
+ const deadline = performance.now() + 7000;
751
+ while (performance.now() < deadline) {
752
+ const renderedFamilies = Array.from(computedFamilies()).filter((family) => !/\bFallback\b/i.test(family));
753
+ const loaded = renderedFamilies.length === 0
754
+ || renderedFamilies.every((family) => document.fonts.check(`400 16px "${family}"`, sample)
755
+ || document.fonts.check(`600 16px "${family}"`, sample));
756
+ if (document.fonts.status === 'loaded' && loaded)
757
+ break;
758
+ await sleep(100);
759
+ }
760
+ probe.remove();
761
+ await nextPaint();
762
+ })(),
763
+ new Promise((resolve) => setTimeout(resolve, 8000)),
764
+ ])).catch(() => { });
765
+ await logFontDiagnostics(page, 'before screenshot');
603
766
  return Buffer.from(await page.screenshot({ type: 'png', fullPage: false }));
604
767
  }
605
768
  async takeScreenshotForAI(options = {}) {
@@ -2,8 +2,8 @@
2
2
  * Capture Agent — Capture Strategy
3
3
  *
4
4
  * Abstraction over the supported media modes: screenshot and clip.
5
- * The opcode runner is identical for all three — only the capture
6
- * opcodes dispatch to different strategies.
5
+ * The opcode runner is identical for both — only the capture opcodes dispatch
6
+ * to different strategies.
7
7
  */
8
8
  import type { ArtifactSpec, ArtifactResult, RuntimeAdapter, MediaMode } from './execution-types.js';
9
9
  export interface CaptureStrategy {
@@ -27,10 +27,4 @@ export declare class ClipStrategy implements CaptureStrategy {
27
27
  capture(adapter: RuntimeAdapter, _spec: ArtifactSpec): Promise<ArtifactResult>;
28
28
  postProcess(artifact: ArtifactResult, spec: ArtifactSpec): Promise<ArtifactResult>;
29
29
  }
30
- export declare class DomStrategy implements CaptureStrategy {
31
- readonly mediaMode: "dom";
32
- prepare(_adapter: RuntimeAdapter, _spec: ArtifactSpec): Promise<void>;
33
- capture(adapter: RuntimeAdapter, _spec: ArtifactSpec): Promise<ArtifactResult>;
34
- postProcess(artifact: ArtifactResult, _spec: ArtifactSpec): Promise<ArtifactResult>;
35
- }
36
30
  export declare function createCaptureStrategy(mediaMode: MediaMode): CaptureStrategy;
@@ -2,8 +2,8 @@
2
2
  * Capture Agent — Capture Strategy
3
3
  *
4
4
  * Abstraction over the supported media modes: screenshot and clip.
5
- * The opcode runner is identical for all three — only the capture
6
- * opcodes dispatch to different strategies.
5
+ * The opcode runner is identical for both — only the capture opcodes dispatch
6
+ * to different strategies.
7
7
  */
8
8
  // ── Screenshot strategy ─────────────────────────────────────────────
9
9
  export class ScreenshotStrategy {
@@ -58,39 +58,11 @@ export class ClipStrategy {
58
58
  return artifact;
59
59
  }
60
60
  }
61
- // ── DOM strategy (Interactive Demos — AUT-121) ──────────────────────
62
- export class DomStrategy {
63
- mediaMode = 'dom';
64
- async prepare(_adapter, _spec) {
65
- // Nothing to prepare — DOM serialization happens inline in the
66
- // CAPTURE_DOM opcode handler in opcode-runner.ts.
67
- }
68
- async capture(adapter, _spec) {
69
- if (!adapter.serializeDom) {
70
- throw new Error('DOM capture requires an adapter that implements serializeDom()');
71
- }
72
- const result = await adapter.serializeDom();
73
- return {
74
- mediaMode: 'dom',
75
- buffer: Buffer.from(result.html, 'utf8'),
76
- mimeType: 'text/html; charset=utf-8',
77
- domHtml: result.html,
78
- domAssetUrls: result.assetUrls,
79
- domHtmlBytes: result.html.length,
80
- dimensions: result.viewport,
81
- };
82
- }
83
- async postProcess(artifact, _spec) {
84
- // Phase 3 will add asset extraction (CAS), PurgeCSS and Brotli here.
85
- return artifact;
86
- }
87
- }
88
61
  // ── Factory ─────────────────────────────────────────────────────────
89
62
  export function createCaptureStrategy(mediaMode) {
90
63
  switch (mediaMode) {
91
64
  case 'screenshot': return new ScreenshotStrategy();
92
65
  case 'clip': return new ClipStrategy();
93
- case 'dom': return new DomStrategy();
94
66
  }
95
67
  }
96
68
  //# sourceMappingURL=capture-strategy.js.map
@@ -7,6 +7,7 @@ declare const DEFAULT_API_BASE_URL = "https://autokap.app";
7
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
+ declare const API_KEY_ENV_VAR = "AUTOKAP_API_KEY";
10
11
  declare const API_BASE_URL_ENV_VAR = "AUTOKAP_API_BASE_URL";
11
12
  declare const WS_URL_ENV_VAR = "AUTOKAP_WS_URL";
12
13
  declare const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = "AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN";
@@ -18,4 +19,4 @@ export declare function readConfig(): Promise<AutokapConfig | null>;
18
19
  export declare function writeConfig(config: AutokapConfig): Promise<void>;
19
20
  export declare function deleteConfig(): Promise<void>;
20
21
  export declare function requireConfig(): Promise<AutokapConfig>;
21
- export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
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, };
@@ -6,6 +6,7 @@ const DEFAULT_API_BASE_URL = 'https://autokap.app';
6
6
  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
+ const API_KEY_ENV_VAR = 'AUTOKAP_API_KEY';
9
10
  const API_BASE_URL_ENV_VAR = 'AUTOKAP_API_BASE_URL';
10
11
  const WS_URL_ENV_VAR = 'AUTOKAP_WS_URL';
11
12
  const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = 'AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN';
@@ -22,6 +23,17 @@ export function getDefaultWsUrl(apiBaseUrl = getDefaultApiBaseUrl()) {
22
23
  return normalizeUrl(process.env[WS_URL_ENV_VAR]) ?? deriveWsUrl(apiBaseUrl);
23
24
  }
24
25
  export async function readConfig() {
26
+ const envApiKey = normalizeApiKey(process.env[API_KEY_ENV_VAR]);
27
+ if (envApiKey) {
28
+ const apiBaseUrl = getDefaultApiBaseUrl();
29
+ const wsUrl = getDefaultWsUrl(apiBaseUrl);
30
+ assertAllowedApiOrigin(apiBaseUrl, DEFAULT_API_BASE_URL, API_BASE_URL_ENV_VAR);
31
+ return {
32
+ apiKey: envApiKey,
33
+ apiBaseUrl,
34
+ wsUrl,
35
+ };
36
+ }
25
37
  try {
26
38
  const raw = await fs.readFile(getConfigPath(), 'utf-8');
27
39
  const parsed = JSON.parse(raw);
@@ -81,11 +93,15 @@ export async function requireConfig() {
81
93
  process.exit(1);
82
94
  }
83
95
  if (!config) {
84
- logger.error('Not authenticated. Run `npx autokap@latest init --cli-key <key>` first.');
96
+ logger.error(`Not authenticated. Run \`npx autokap@latest init --cli-key <key>\` first, or set ${API_KEY_ENV_VAR} in CI.`);
85
97
  process.exit(1);
86
98
  }
87
99
  return config;
88
100
  }
101
+ function normalizeApiKey(value) {
102
+ const trimmed = value?.trim();
103
+ return trimmed || null;
104
+ }
89
105
  function normalizeUrl(value) {
90
106
  const trimmed = value?.trim();
91
107
  if (!trimmed)
@@ -143,5 +159,5 @@ function assertAllowedApiOrigin(candidateUrl, baselineUrl, envVar) {
143
159
  }
144
160
  throw new Error(`Refusing unsafe server override to ${candidateOrigin}. Set ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1 to allow ${envVar ?? 'this override'} explicitly.`);
145
161
  }
146
- export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
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, };
147
163
  //# sourceMappingURL=cli-config.js.map
@@ -14,6 +14,7 @@ export declare const CLI_ADVANCED_SKILL_COMMAND = "autokap skill --agent <agent>
14
14
  export declare const CLI_FALLBACK_PROGRAM_COMMAND = "autokap run <preset-id> --program <file>";
15
15
  export declare function buildCliRunCommand(presetId: string, options?: {
16
16
  local?: boolean;
17
+ env?: string;
17
18
  }): string;
18
19
  export declare function buildCliInstalledSetupCommand(cliKey: string): string;
19
20
  export declare const CLI_PUBLIC_COMMANDS: CliPublicCommandDescriptor[];
@@ -7,7 +7,7 @@ export const CLI_DEFAULT_SETUP_COMMAND = "npx autokap@latest init --cli-key <you
7
7
  export const CLI_ADVANCED_SKILL_COMMAND = "autokap skill --agent <agent>";
8
8
  export const CLI_FALLBACK_PROGRAM_COMMAND = "autokap run <preset-id> --program <file>";
9
9
  export function buildCliRunCommand(presetId, options = {}) {
10
- return `autokap run${options.local ? " --local" : ""} ${presetId}`;
10
+ return `autokap run${options.local ? " --local" : ""}${options.env ? ` --env ${options.env}` : ""} ${presetId}`;
11
11
  }
12
12
  export function buildCliInstalledSetupCommand(cliKey) {
13
13
  return `autokap init --cli-key ${cliKey}`;
@@ -45,10 +45,16 @@ export const CLI_PUBLIC_COMMANDS = [
45
45
  },
46
46
  {
47
47
  id: "run",
48
- command: "autokap run <preset-id>",
48
+ command: "autokap run <preset-id> --env local",
49
49
  summary: "Run a preset capture using local Playwright",
50
50
  docsDescriptionKey: "cliCmdRun",
51
51
  },
52
+ {
53
+ id: "auto-recapture",
54
+ command: "autokap auto-recapture --project <project-id> --env local",
55
+ summary: "Run every preset enabled for CI auto-recapture in a project",
56
+ docsDescriptionKey: "cliCmdAutoRecapture",
57
+ },
52
58
  {
53
59
  id: "run-headed",
54
60
  command: "autokap run <preset-id> --headed",
@@ -9,4 +9,6 @@ export declare function runLocal(presetId: string, opts: {
9
9
  output?: string;
10
10
  program?: string;
11
11
  debug?: boolean;
12
+ env?: string;
13
+ allowUploadFailure?: boolean;
12
14
  }): Promise<void>;