autokap 1.5.2 → 1.5.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.
@@ -1,5 +1,6 @@
1
1
  import { chromium } from 'playwright';
2
2
  import { logger } from './logger.js';
3
+ import { ensureChromiumInstalled } from './playwright-installer.js';
3
4
  /**
4
5
  * Opens a headed Chromium window, lets the user log in, and uploads the
5
6
  * resulting storageState (cookies + localStorage, HttpOnly included) to the
@@ -11,6 +12,7 @@ import { logger } from './logger.js';
11
12
  */
12
13
  export async function captureAuthSession(options) {
13
14
  const { apiBaseUrl, apiKey, projectId, accountId, startUrl } = options;
15
+ await ensureChromiumInstalled();
14
16
  logger.info('[auth] Launching Chromium…');
15
17
  const browser = await chromium.launch({ headless: false });
16
18
  let context = null;
@@ -58,6 +58,13 @@ export interface VideoClipMetadata {
58
58
  * SFX in the video compositor.
59
59
  */
60
60
  keystrokeOffsetsMs?: number[];
61
+ /**
62
+ * For CLICK / DOUBLE_CLICK / CHECK opcodes captured in clipCursor mode:
63
+ * clip-relative ms timestamp of each actual click dispatched by Playwright
64
+ * (measured AFTER the cursor animation settled). Drives mouse SFX in
65
+ * sync with the visible click.
66
+ */
67
+ clickOffsetsMs?: number[];
61
68
  }>;
62
69
  }
63
70
  export interface VideoAudioAsset {
@@ -19,6 +19,7 @@ import { Browser } from './browser.js';
19
19
  import { API_BASE_URL_ENV_VAR, requireConfig } from './cli-config.js';
20
20
  import { WebPlaywrightLocal } from './web-playwright-local.js';
21
21
  import { executeProgram } from './opcode-runner.js';
22
+ import { ensureChromiumInstalled } from './playwright-installer.js';
22
23
  import { RecoveryChainImpl } from './recovery-chain.js';
23
24
  import { parseProgram } from './execution-schema.js';
24
25
  import { buildCursorOverlayScript } from './cursor-overlay-script.js';
@@ -108,6 +109,18 @@ function normalizeNumericScale(value) {
108
109
  const HEALER_SYSTEM_PROMPT = 'You repair failed deterministic browser opcodes. Respond only with JSON.';
109
110
  // ── Main entry point ────────────────────────────────────────────────
110
111
  export async function runCapture(options) {
112
+ // Self-heal a missing Playwright Chromium binary BEFORE anything else.
113
+ // Skipped postinstalls or downstream Playwright version bumps would
114
+ // otherwise surface as a cryptic launch failure mid-capture.
115
+ try {
116
+ await ensureChromiumInstalled();
117
+ }
118
+ catch (err) {
119
+ return {
120
+ success: false,
121
+ error: `playwright chromium install failed: ${err instanceof Error ? err.message : String(err)}`,
122
+ };
123
+ }
111
124
  const config = await requireConfig();
112
125
  // Step 1: Get the compiled program
113
126
  let resolvedProgram;
@@ -751,6 +764,9 @@ export function buildVideoClipMetadata(videoId, result, program, runId) {
751
764
  ...(t.keystrokeOffsetsMs && t.keystrokeOffsetsMs.length > 0
752
765
  ? { keystrokeOffsetsMs: t.keystrokeOffsetsMs }
753
766
  : {}),
767
+ ...(t.clickOffsetsMs && t.clickOffsetsMs.length > 0
768
+ ? { clickOffsetsMs: t.clickOffsetsMs }
769
+ : {}),
754
770
  }));
755
771
  clipsByKey.set(`${variantId}:${artifact.clipId}`, {
756
772
  variantId,
@@ -954,8 +970,8 @@ async function prepareDirectArtifactUpload(params) {
954
970
  clipName: artifact.clipName ?? null,
955
971
  stepDescription: artifact.stepDescription ?? null,
956
972
  stepIndex: typeof artifact.stepIndex === 'number' ? artifact.stepIndex : null,
957
- durationMs: typeof artifact.durationMs === 'number' ? artifact.durationMs : null,
958
- trimStartMs: typeof artifact.trimStartMs === 'number' ? artifact.trimStartMs : null,
973
+ durationMs: typeof artifact.durationMs === 'number' ? Math.round(artifact.durationMs) : null,
974
+ trimStartMs: typeof artifact.trimStartMs === 'number' ? Math.round(artifact.trimStartMs) : null,
959
975
  artifactPlan: program.artifactPlan,
960
976
  tabIconMimeType: artifact.tabIconData ? (artifact.tabIconMimeType ?? 'image/png') : null,
961
977
  tabIconSha256,
@@ -700,6 +700,14 @@ export interface OpcodeTiming {
700
700
  * for non-TYPE opcodes and for typing paths that bypass humanType.
701
701
  */
702
702
  keystrokeOffsetsMs?: number[];
703
+ /**
704
+ * For CLICK / DOUBLE_CLICK / CHECK opcodes captured in clipCursor mode:
705
+ * timestamp (ms relative to the active clip start) at which Playwright
706
+ * dispatched each actual click — measured AFTER the cursor animation
707
+ * settled on the target. Drives mouse SFX in sync with the visible click.
708
+ * Empty/undefined for opcodes whose adapter doesn't surface the timestamp.
709
+ */
710
+ clickOffsetsMs?: number[];
703
711
  }
704
712
  export interface RunResult {
705
713
  programId: string;
@@ -736,6 +744,25 @@ export interface ClickOptions {
736
744
  };
737
745
  /** Mouse button. Default: 'left' */
738
746
  button?: 'left' | 'right' | 'middle';
747
+ /**
748
+ * Fired with `Date.now()` right before Playwright dispatches the actual
749
+ * click — i.e. AFTER the visible cursor animation has settled on the
750
+ * target. The runner converts the wall-clock to clip-relative offsets so
751
+ * the video compositor can fire mouse SFX in lock-step with the visible
752
+ * click (instead of when the cursor was still travelling).
753
+ */
754
+ onClick?: (timestampMs: number) => void;
755
+ }
756
+ export interface ClickByTargetOptions {
757
+ selector?: string;
758
+ target?: SemanticTarget;
759
+ selectorAlternates?: string[];
760
+ onClick?: (timestampMs: number) => void;
761
+ }
762
+ export interface MouseActionOptions {
763
+ /** Same semantics as `ClickOptions.onClick` — fires right before the
764
+ * actual click is dispatched (CHECK / DOUBLE_CLICK). */
765
+ onClick?: (timestampMs: number) => void;
739
766
  }
740
767
  export interface RecordingOptions {
741
768
  mediaMode: 'clip' | 'video';
@@ -815,11 +842,7 @@ export interface RuntimeAdapter {
815
842
  } | null>;
816
843
  close(): Promise<void>;
817
844
  /** Click an element by semantic target. Falls back to selector if target not found. */
818
- clickByTarget?(opts: {
819
- selector?: string;
820
- target?: SemanticTarget;
821
- selectorAlternates?: string[];
822
- }): Promise<void>;
845
+ clickByTarget?(opts: ClickByTargetOptions): Promise<void>;
823
846
  /** Type into an element by semantic target. */
824
847
  typeByTarget?(opts: {
825
848
  selector?: string;
@@ -849,8 +872,8 @@ export interface RuntimeAdapter {
849
872
  value?: string;
850
873
  index?: number;
851
874
  }): Promise<void>;
852
- check?(selector: string, checked: boolean): Promise<void>;
853
- doubleClick?(selector: string): Promise<void>;
875
+ check?(selector: string, checked: boolean, opts?: MouseActionOptions): Promise<void>;
876
+ doubleClick?(selector: string, opts?: MouseActionOptions): Promise<void>;
854
877
  /**
855
878
  * Drag the source element from point A to point B with an animated cursor
856
879
  * when a clip is recording. Destination is either another element
@@ -46,5 +46,13 @@ export interface OpcodeActionResult {
46
46
  * clip-relative offsets so the video compositor can fire per-keystroke SFX.
47
47
  */
48
48
  keystrokeTimestampsMs?: number[];
49
+ /**
50
+ * For CLICK / DOUBLE_CLICK / CHECK opcodes: absolute wall-clock timestamps
51
+ * captured INSIDE the adapter, just before Playwright dispatches the
52
+ * actual click — i.e. AFTER the cursor animation has settled on the
53
+ * target. Lets the compositor place the mouse SFX in sync with the visible
54
+ * click instead of when the cursor was still travelling.
55
+ */
56
+ clickTimestampsMs?: number[];
49
57
  }
50
58
  export declare function executeOpcodeCoreAction(opcode: ExecutionOpcode, adapter: RuntimeAdapter, context?: OpcodeActionContext): Promise<OpcodeActionResult>;
@@ -62,9 +62,13 @@ export async function executeOpcodeCoreAction(opcode, adapter, context = {}) {
62
62
  case 'DISMISS_OVERLAYS':
63
63
  await dismissAllOverlays(adapter);
64
64
  break;
65
- case 'CLICK':
65
+ case 'CLICK': {
66
+ const clickTimestampsMs = [];
67
+ const onClick = (timestampMs) => {
68
+ clickTimestampsMs.push(timestampMs);
69
+ };
66
70
  try {
67
- await adapter.click(opcode.selector, opcode.button ? { button: opcode.button } : undefined);
71
+ await adapter.click(opcode.selector, { ...(opcode.button ? { button: opcode.button } : {}), onClick });
68
72
  }
69
73
  catch (error) {
70
74
  if (!opcode.target || !adapter.clickByTarget)
@@ -73,9 +77,11 @@ export async function executeOpcodeCoreAction(opcode, adapter, context = {}) {
73
77
  selector: opcode.selector,
74
78
  target: opcode.target,
75
79
  selectorAlternates: opcode.selectorAlternates,
80
+ onClick,
76
81
  });
77
82
  }
78
- break;
83
+ return { success: true, clickTimestampsMs };
84
+ }
79
85
  case 'TYPE': {
80
86
  const rawText = (opcode.textByLocale && context.currentVariant?.locale
81
87
  ? opcode.textByLocale[context.currentVariant.locale] ?? opcode.text
@@ -174,16 +180,24 @@ export async function executeOpcodeCoreAction(opcode, adapter, context = {}) {
174
180
  index: opcode.optionIndex,
175
181
  });
176
182
  break;
177
- case 'CHECK':
183
+ case 'CHECK': {
178
184
  if (!adapter.check)
179
185
  return { success: false, error: 'adapter does not support CHECK' };
180
- await adapter.check(opcode.selector, opcode.checked);
181
- break;
182
- case 'DOUBLE_CLICK':
186
+ const clickTimestampsMs = [];
187
+ await adapter.check(opcode.selector, opcode.checked, {
188
+ onClick: (timestampMs) => clickTimestampsMs.push(timestampMs),
189
+ });
190
+ return { success: true, clickTimestampsMs };
191
+ }
192
+ case 'DOUBLE_CLICK': {
183
193
  if (!adapter.doubleClick)
184
194
  return { success: false, error: 'adapter does not support DOUBLE_CLICK' };
185
- await adapter.doubleClick(opcode.selector);
186
- break;
195
+ const clickTimestampsMs = [];
196
+ await adapter.doubleClick(opcode.selector, {
197
+ onClick: (timestampMs) => clickTimestampsMs.push(timestampMs),
198
+ });
199
+ return { success: true, clickTimestampsMs };
200
+ }
187
201
  case 'DRAG':
188
202
  if (!adapter.drag)
189
203
  return { success: false, error: 'adapter does not support DRAG' };
@@ -290,6 +290,9 @@ async function executeOpcode(opcode, index, adapter, verifier, breaker, recovery
290
290
  const keystrokeOffsetsMs = result.keystrokeTimestampsMs && result.keystrokeTimestampsMs.length > 0
291
291
  ? result.keystrokeTimestampsMs.map((t) => Math.max(0, t - preTiming.clipStartedAt))
292
292
  : undefined;
293
+ const clickOffsetsMs = result.clickTimestampsMs && result.clickTimestampsMs.length > 0
294
+ ? result.clickTimestampsMs.map((t) => Math.max(0, t - preTiming.clipStartedAt))
295
+ : undefined;
293
296
  opcodeTimings.push({
294
297
  stepIndex: index,
295
298
  stepId: opcode.stepId,
@@ -300,6 +303,7 @@ async function executeOpcode(opcode, index, adapter, verifier, breaker, recovery
300
303
  timecodeEndMs: Math.max(0, Date.now() - preTiming.clipStartedAt),
301
304
  bbox: preTiming.bbox,
302
305
  ...(keystrokeOffsetsMs ? { keystrokeOffsetsMs } : {}),
306
+ ...(clickOffsetsMs ? { clickOffsetsMs } : {}),
303
307
  });
304
308
  }
305
309
  if (!result.success) {
@@ -666,6 +670,12 @@ async function executeOpcodeAction(opcode, opcodeIndex, adapter, artifacts, tele
666
670
  }
667
671
  case 'END_CLIP': {
668
672
  const clipIdentity = resolveClipIdentity(executionState.activeClip, opcode);
673
+ // Capture the URL BEFORE endRecording(): the local CDP branch of the
674
+ // adapter closes the browser context inside endRecording() to release
675
+ // the CDP session, which makes any subsequent browser operation throw
676
+ // "Browser not launched". The clip ends immediately before this call,
677
+ // so the URL is still accurate.
678
+ const captureUrl = await adapter.getCurrentUrl();
669
679
  const recording = await adapter.endRecording();
670
680
  executionState.activeClip = undefined;
671
681
  // Match the artifact's mediaMode to the program's so the upload route
@@ -680,7 +690,7 @@ async function executeOpcodeAction(opcode, opcodeIndex, adapter, artifacts, tele
680
690
  trimStartMs: recording.trimStartMs,
681
691
  dimensions: undefined,
682
692
  captureType: 'fullpage',
683
- captureUrl: await adapter.getCurrentUrl(),
693
+ captureUrl,
684
694
  clipId: clipIdentity.clipId,
685
695
  clipName: clipIdentity.clipName,
686
696
  stepDescription: opcode.description,
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Preflight Playwright Chromium binary install.
3
+ *
4
+ * Run before any `chromium.launch(...)` to make sure the Playwright Chromium
5
+ * binary exists on disk. The package ships a `postinstall` hook that fetches
6
+ * Chromium at install time, but that step can be silently skipped on some
7
+ * setups (CI caches, monorepos, npm scripts) — and any Playwright version
8
+ * bump downstream invalidates the existing binary too. Catching this once at
9
+ * run start avoids the famously confusing "Executable doesn't exist at …"
10
+ * launch failure mid-capture.
11
+ *
12
+ * Cross-platform: uses `npx playwright install chromium` (works on macOS,
13
+ * Linux, Windows). Output is streamed inherit so the user sees the
14
+ * download progress.
15
+ */
16
+ export declare function ensureChromiumInstalled(): Promise<void>;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Preflight Playwright Chromium binary install.
3
+ *
4
+ * Run before any `chromium.launch(...)` to make sure the Playwright Chromium
5
+ * binary exists on disk. The package ships a `postinstall` hook that fetches
6
+ * Chromium at install time, but that step can be silently skipped on some
7
+ * setups (CI caches, monorepos, npm scripts) — and any Playwright version
8
+ * bump downstream invalidates the existing binary too. Catching this once at
9
+ * run start avoids the famously confusing "Executable doesn't exist at …"
10
+ * launch failure mid-capture.
11
+ *
12
+ * Cross-platform: uses `npx playwright install chromium` (works on macOS,
13
+ * Linux, Windows). Output is streamed inherit so the user sees the
14
+ * download progress.
15
+ */
16
+ import { spawn } from 'node:child_process';
17
+ import { existsSync } from 'node:fs';
18
+ import { chromium } from 'playwright';
19
+ import { logger } from './logger.js';
20
+ let cachedCheckResult = null;
21
+ export async function ensureChromiumInstalled() {
22
+ // One-shot per process: once we've verified (or installed) the binary,
23
+ // skip every subsequent call. `chromium.executablePath()` is cheap but
24
+ // `existsSync` adds up across recovery retries.
25
+ if (cachedCheckResult === 'ok')
26
+ return;
27
+ let execPath = '';
28
+ try {
29
+ execPath = chromium.executablePath();
30
+ }
31
+ catch {
32
+ // executablePath() throws if Playwright has no path resolved at all.
33
+ // Treat the same as "not installed" so we run the install.
34
+ execPath = '';
35
+ }
36
+ if (execPath && existsSync(execPath)) {
37
+ cachedCheckResult = 'ok';
38
+ return;
39
+ }
40
+ logger.info('[playwright] Chromium browser is missing — installing it now (one-time, takes ~30s)…');
41
+ await runPlaywrightInstall();
42
+ // Re-check; if the install genuinely succeeded `executablePath()` now
43
+ // resolves to a real file. If it still doesn't, surface a clearer error
44
+ // than the raw launch failure.
45
+ try {
46
+ const refreshed = chromium.executablePath();
47
+ if (existsSync(refreshed)) {
48
+ cachedCheckResult = 'ok';
49
+ logger.info('[playwright] Chromium installed.');
50
+ return;
51
+ }
52
+ }
53
+ catch {
54
+ // fallthrough to error below
55
+ }
56
+ throw new Error('Playwright Chromium install completed but the binary is still missing. ' +
57
+ 'Run `npx playwright install chromium` manually to diagnose.');
58
+ }
59
+ async function runPlaywrightInstall() {
60
+ return new Promise((resolve, reject) => {
61
+ // Use `npx` so we hit whichever Playwright version this CLI depends on,
62
+ // regardless of whether the user has a global `playwright` binary.
63
+ const child = spawn('npx', ['--yes', 'playwright', 'install', 'chromium'], {
64
+ stdio: 'inherit',
65
+ });
66
+ child.on('error', (err) => {
67
+ reject(new Error(`Failed to spawn \`npx playwright install chromium\`: ${err.message}. ` +
68
+ 'Make sure Node.js + npm are installed and on PATH.'));
69
+ });
70
+ child.on('close', (code) => {
71
+ if (code === 0) {
72
+ resolve();
73
+ return;
74
+ }
75
+ reject(new Error(`\`npx playwright install chromium\` exited with code ${code}. ` +
76
+ 'Run it manually for full diagnostics.'));
77
+ });
78
+ });
79
+ }
80
+ //# sourceMappingURL=playwright-installer.js.map
@@ -34,7 +34,9 @@ export declare class WebPlaywrightLocal implements RuntimeAdapter {
34
34
  * Click an element using semantic target resolution.
35
35
  * Tries CSS selector first, falls back to Playwright semantic locators.
36
36
  */
37
- clickByTarget(opts: ResolveOptions): Promise<void>;
37
+ clickByTarget(opts: ResolveOptions & {
38
+ onClick?: (timestampMs: number) => void;
39
+ }): Promise<void>;
38
40
  /**
39
41
  * Type into an element using semantic target resolution.
40
42
  */
@@ -87,8 +89,12 @@ export declare class WebPlaywrightLocal implements RuntimeAdapter {
87
89
  value?: string;
88
90
  index?: number;
89
91
  }): Promise<void>;
90
- check(selector: string, checked: boolean): Promise<void>;
91
- doubleClick(selector: string): Promise<void>;
92
+ check(selector: string, checked: boolean, actionOpts?: {
93
+ onClick?: (timestampMs: number) => void;
94
+ }): Promise<void>;
95
+ doubleClick(selector: string, actionOpts?: {
96
+ onClick?: (timestampMs: number) => void;
97
+ }): Promise<void>;
92
98
  drag(opts: {
93
99
  selector?: string;
94
100
  target?: SemanticTarget;
@@ -77,9 +77,11 @@ export class WebPlaywrightLocal {
77
77
  const page = await this.browser.currentPage;
78
78
  const t0 = Date.now();
79
79
  logger.debug(`[click] start selector="${selector}"${options?.useKeyboard ? ' mode=keyboard' : ''}${options?.useJsDispatch ? ' mode=js_dispatch' : ''}${options?.coordinates ? ` mode=coords(${options.coordinates.x},${options.coordinates.y})` : ''}`);
80
+ const fireClickSfx = () => options?.onClick?.(Date.now());
80
81
  try {
81
82
  if (options?.coordinates) {
82
83
  await this.moveClipCursorToPoint(options.coordinates);
84
+ fireClickSfx();
83
85
  await this.browser.clickByCoordinates(options.coordinates.x, options.coordinates.y);
84
86
  logger.debug(`[click] done coords took ${Date.now() - t0}ms`);
85
87
  return;
@@ -88,11 +90,13 @@ export class WebPlaywrightLocal {
88
90
  const animatedTarget = await this.moveClipCursorToLocator(locator);
89
91
  if (options?.useKeyboard) {
90
92
  await locator.focus();
93
+ fireClickSfx();
91
94
  await page.keyboard.press('Enter');
92
95
  logger.debug(`[click] done keyboard took ${Date.now() - t0}ms`);
93
96
  return;
94
97
  }
95
98
  if (options?.useJsDispatch) {
99
+ fireClickSfx();
96
100
  await locator.dispatchEvent('click');
97
101
  logger.debug(`[click] done js_dispatch took ${Date.now() - t0}ms`);
98
102
  return;
@@ -103,6 +107,7 @@ export class WebPlaywrightLocal {
103
107
  ? await this.relativeClickPosition(locator, animatedTarget)
104
108
  : null;
105
109
  if (options?.button && options.button !== 'left') {
110
+ fireClickSfx();
106
111
  await locator.click({
107
112
  button: options.button,
108
113
  timeout: 5000,
@@ -113,6 +118,7 @@ export class WebPlaywrightLocal {
113
118
  return;
114
119
  }
115
120
  if (clickPosition) {
121
+ fireClickSfx();
116
122
  await locator.click({
117
123
  timeout: 5000,
118
124
  force: options?.force,
@@ -120,6 +126,7 @@ export class WebPlaywrightLocal {
120
126
  });
121
127
  }
122
128
  else {
129
+ fireClickSfx();
123
130
  await this.browser.clickBySelector(selector, { force: options?.force });
124
131
  }
125
132
  await this.emitClipClickPulse();
@@ -144,6 +151,7 @@ export class WebPlaywrightLocal {
144
151
  const position = target
145
152
  ? await this.relativeClickPosition(resolved.locator, target)
146
153
  : null;
154
+ opts.onClick?.(Date.now());
147
155
  await resolved.locator.click({
148
156
  timeout: 5000,
149
157
  ...(position ? { position } : {}),
@@ -633,7 +641,7 @@ export class WebPlaywrightLocal {
633
641
  optionIndex: option.index,
634
642
  });
635
643
  }
636
- async check(selector, checked) {
644
+ async check(selector, checked, actionOpts) {
637
645
  const page = await this.browser.currentPage;
638
646
  const locator = page.locator(selector).first();
639
647
  const target = await this.moveClipCursorToLocator(locator);
@@ -641,6 +649,7 @@ export class WebPlaywrightLocal {
641
649
  ? await this.relativeClickPosition(locator, target)
642
650
  : null;
643
651
  const opts = { timeout: 5000, ...(position ? { position } : {}) };
652
+ actionOpts?.onClick?.(Date.now());
644
653
  if (checked) {
645
654
  await locator.check(opts);
646
655
  }
@@ -648,13 +657,14 @@ export class WebPlaywrightLocal {
648
657
  await locator.uncheck(opts);
649
658
  }
650
659
  }
651
- async doubleClick(selector) {
660
+ async doubleClick(selector, actionOpts) {
652
661
  const page = await this.browser.currentPage;
653
662
  const locator = page.locator(selector).first();
654
663
  const target = await this.moveClipCursorToLocator(locator);
655
664
  const position = target
656
665
  ? await this.relativeClickPosition(locator, target)
657
666
  : null;
667
+ actionOpts?.onClick?.(Date.now());
658
668
  await locator.dblclick({
659
669
  timeout: 5000,
660
670
  ...(position ? { position } : {}),
@@ -895,7 +905,10 @@ export class WebPlaywrightLocal {
895
905
  }
896
906
  await page.waitForTimeout(70);
897
907
  await humanType(page, text, this.clipCursor
898
- ? { minDelayMs: 20, maxDelayMs: 45, onKeystroke }
908
+ // Demo-video cadence: ~80-180ms between keys (≈ 80-100 WPM with
909
+ // natural variation). Faster than that reads as robotic in the
910
+ // mixed video + keyboard SFX track.
911
+ ? { minDelayMs: 80, maxDelayMs: 180, onKeystroke }
899
912
  : { onKeystroke });
900
913
  }
901
914
  async seedClipCursor(position) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autokap",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "AI-powered CLI tool for capturing clean screenshots of websites",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",