autokap 1.0.8 → 1.1.0

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 (58) hide show
  1. package/assets/skill/OPCODE-REFERENCE.md +29 -1
  2. package/assets/skill/SKILL.md +2 -1
  3. package/dist/auth-capture.js +35 -2
  4. package/dist/billing-operation-logging.d.ts +4 -3
  5. package/dist/billing-operation-logging.js +3 -2
  6. package/dist/browser.d.ts +10 -10
  7. package/dist/browser.js +32 -28
  8. package/dist/capture-encryption.d.ts +3 -1
  9. package/dist/capture-encryption.js +21 -6
  10. package/dist/capture-strategy.js +3 -2
  11. package/dist/cli-config.d.ts +2 -1
  12. package/dist/cli-config.js +51 -2
  13. package/dist/cli-contract.d.ts +5 -1
  14. package/dist/cli-contract.js +7 -1
  15. package/dist/cli-runner-local.js +16 -3
  16. package/dist/cli-runner.js +165 -18
  17. package/dist/cli.js +25 -19
  18. package/dist/clip-begin-frame-recorder.d.ts +44 -0
  19. package/dist/clip-begin-frame-recorder.js +250 -0
  20. package/dist/clip-capture-backend.d.ts +25 -0
  21. package/dist/clip-capture-backend.js +189 -0
  22. package/dist/clip-capture-loop.d.ts +61 -0
  23. package/dist/clip-capture-loop.js +111 -0
  24. package/dist/clip-frame-recorder.d.ts +63 -0
  25. package/dist/clip-frame-recorder.js +305 -0
  26. package/dist/clip-postprocess.d.ts +31 -2
  27. package/dist/clip-postprocess.js +174 -57
  28. package/dist/clip-runtime.d.ts +18 -0
  29. package/dist/clip-runtime.js +67 -0
  30. package/dist/clip-scale.d.ts +10 -0
  31. package/dist/clip-scale.js +21 -0
  32. package/dist/clip-screencast-recorder.d.ts +42 -0
  33. package/dist/clip-screencast-recorder.js +242 -0
  34. package/dist/clip-sidecar.d.ts +54 -0
  35. package/dist/clip-sidecar.js +208 -0
  36. package/dist/cost-logging.d.ts +1 -1
  37. package/dist/env-validation.js +38 -4
  38. package/dist/execution-schema.d.ts +690 -360
  39. package/dist/execution-schema.js +98 -42
  40. package/dist/execution-types.d.ts +53 -3
  41. package/dist/execution-types.js +2 -1
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.js +1 -0
  44. package/dist/llm-healer.d.ts +2 -10
  45. package/dist/llm-healer.js +109 -62
  46. package/dist/llm-provider.js +3 -0
  47. package/dist/opcode-actions.js +13 -0
  48. package/dist/opcode-runner.js +21 -12
  49. package/dist/program-signing.d.ts +1094 -0
  50. package/dist/program-signing.js +140 -0
  51. package/dist/provider-config.d.ts +5 -0
  52. package/dist/provider-config.js +28 -1
  53. package/dist/recovery-chain.js +40 -16
  54. package/dist/server-credit-usage.d.ts +1 -1
  55. package/dist/types.d.ts +8 -2
  56. package/dist/web-playwright-local.d.ts +31 -1
  57. package/dist/web-playwright-local.js +207 -37
  58. package/package.json +12 -2
@@ -1,6 +1,6 @@
1
1
  # AutoKap Opcode Reference
2
2
 
3
- Detailed parameter documentation for all 24 opcodes. For workflow, rules, and examples, see [SKILL.md](SKILL.md).
3
+ Detailed parameter documentation for all 25 opcodes. For workflow, rules, and examples, see [SKILL.md](SKILL.md).
4
4
 
5
5
  ## Common Fields (all opcodes)
6
6
 
@@ -248,6 +248,34 @@ Double-click an element.
248
248
  { "kind": "DOUBLE_CLICK", "selector": "[data-ak=\"editable-title\"]", "postcondition": { "type": "element_visible", "selector": "[data-ak=\"title-editor\"]", "waitMs": 3000 } }
249
249
  ```
250
250
 
251
+ ## DRAG
252
+
253
+ Drag the source element from point A to point B with an animated cursor. In clip recordings the cursor overlay glides along a Bezier curve, shows a pressed state during the drag, and emits a drop pulse at the destination.
254
+
255
+ | Param | Type | Required | Description |
256
+ |-------|------|----------|-------------|
257
+ | `selector` | string | yes | CSS selector of the source element (the one being dragged) |
258
+ | `target` | SemanticTarget | no | Semantic fallback for the source |
259
+ | `fingerprint` | string | no | AKTree fingerprint for the source |
260
+ | `selectorAlternates` | string[] | no | Alternative source selectors |
261
+ | `toSelector` | string | no* | CSS selector of the drop target element |
262
+ | `toTarget` | SemanticTarget | no* | Semantic fallback for the drop target |
263
+ | `toSelectorAlternates` | string[] | no | Alternative destination selectors |
264
+ | `offset` | `{ dx: number, dy: number }` | no* | Pixel offset from the source center (use for sliders, canvas drawing, or any drop that isn't a DOM node) |
265
+
266
+ *Provide EITHER `toSelector` / `toTarget` (element drag) OR `offset` (relative drag). Not both.
267
+
268
+ **Can:** Element-to-element drag (Kanban columns, sortable lists), slider adjustments, canvas drawing, any interaction driven by mousedown + mousemove + mouseup. Works with native HTML5 drag, dnd-kit, react-dnd, Radix DnD primitives — `page.mouse.*` fires both mouse and pointer events.
269
+ **Cannot:** Multi-touch gestures, drag between different browser windows, file drops (use a file input instead).
270
+
271
+ ```json
272
+ { "kind": "DRAG", "selector": "[data-ak=\"task-card-todo-1\"]", "toSelector": "[data-ak=\"column-in-progress\"]", "postcondition": { "type": "element_visible", "selector": "[data-ak=\"column-in-progress\"] [data-ak=\"task-card-todo-1\"]", "waitMs": 3000 } }
273
+ ```
274
+
275
+ ```json
276
+ { "kind": "DRAG", "selector": "[data-ak=\"volume-slider-thumb\"]", "offset": { "dx": 120, "dy": 0 }, "postcondition": { "type": "any_change" } }
277
+ ```
278
+
251
279
  ## SET_LOCALE
252
280
 
253
281
  Set the application's locale/language for the current variant.
@@ -134,7 +134,7 @@ interface VariantSpec {
134
134
 
135
135
  ## Opcode Quick Reference
136
136
 
137
- 24 opcodes available. For full parameter documentation, see [OPCODE-REFERENCE.md](OPCODE-REFERENCE.md).
137
+ 25 opcodes available. For full parameter documentation, see [OPCODE-REFERENCE.md](OPCODE-REFERENCE.md).
138
138
 
139
139
  | Kind | Selector? | Key Params | Typical Postcondition | Notes |
140
140
  |------|-----------|-----------|----------------------|-------|
@@ -149,6 +149,7 @@ interface VariantSpec {
149
149
  | `SELECT_OPTION` | yes | `optionLabel` / `optionValue` / `optionIndex` | `text_contains` | **Native `<select>` only.** Custom dropdowns: use CLICK sequence |
150
150
  | `CHECK` | yes | `checked` | `always` | Idempotent. Safer than CLICK for checkboxes |
151
151
  | `DOUBLE_CLICK` | yes | — | `element_visible` / `any_change` | Inline editing, text selection |
152
+ | `DRAG` | yes | `toSelector?` / `toTarget?` / `offset?` | `element_visible` / `any_change` | Animated cursor A→B. Use `toSelector` for Kanban-style drops, `offset` for sliders / canvas |
152
153
  | `SET_LOCALE` | no | `locale`, `method`, `storageHints?` | `always` | Use `"$variant"`. Prefer `method: "storage"` |
153
154
  | `SET_THEME` | no | `theme`, `method`, `storageHints?` | `always` | Use `"$variant"`. Prefer `method: "storage"` |
154
155
  | `ASSERT_ROUTE` | no | `urlPattern` | `route_matches` | Validation checkpoint |
@@ -95,7 +95,7 @@ export async function captureAuthSession(options) {
95
95
  if (!context || !browser.isConnected())
96
96
  return;
97
97
  try {
98
- const snapshot = (await context.storageState());
98
+ const snapshot = sanitizeStorageStateForStartUrl((await context.storageState()), startUrl);
99
99
  const hasAny = (snapshot.cookies?.length ?? 0) > 0 || (snapshot.origins?.length ?? 0) > 0;
100
100
  if (hasAny)
101
101
  lastGoodState = snapshot;
@@ -119,7 +119,7 @@ export async function captureAuthSession(options) {
119
119
  // snapshot (most up-to-date) before tearing down.
120
120
  if (userConfirmed && browser.isConnected()) {
121
121
  try {
122
- lastGoodState = (await context.storageState());
122
+ lastGoodState = sanitizeStorageStateForStartUrl((await context.storageState()), startUrl);
123
123
  }
124
124
  catch {
125
125
  // fall back to whatever the poll captured
@@ -161,4 +161,37 @@ export async function captureAuthSession(options) {
161
161
  }
162
162
  }
163
163
  }
164
+ function sanitizeStorageStateForStartUrl(state, startUrl) {
165
+ try {
166
+ const parsed = new URL(startUrl);
167
+ const allowedSuffix = buildAllowedCookieSuffix(parsed.hostname);
168
+ const cookies = (state.cookies ?? []).filter((cookie) => {
169
+ const domain = String(cookie.domain ?? '').trim().replace(/^\./, '').toLowerCase();
170
+ return domain === parsed.hostname || domain.endsWith(`.${allowedSuffix}`) || domain === allowedSuffix;
171
+ });
172
+ const origins = (state.origins ?? []).filter((originEntry) => {
173
+ try {
174
+ const origin = new URL(String(originEntry.origin ?? ''));
175
+ return origin.hostname === parsed.hostname
176
+ || origin.hostname === allowedSuffix
177
+ || origin.hostname.endsWith(`.${allowedSuffix}`);
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ });
183
+ return { cookies, origins };
184
+ }
185
+ catch {
186
+ return state;
187
+ }
188
+ }
189
+ function buildAllowedCookieSuffix(hostname) {
190
+ const normalized = hostname.trim().toLowerCase().replace(/^\.+/, '');
191
+ if (!normalized || normalized === 'localhost' || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(normalized)) {
192
+ return normalized;
193
+ }
194
+ const parts = normalized.split('.');
195
+ return parts.length >= 2 ? parts.slice(-2).join('.') : normalized;
196
+ }
164
197
  //# sourceMappingURL=auth-capture.js.map
@@ -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' | 'video';
3
+ export type BillingOperationType = 'screenshot' | 'clip' | 'interactive_demo';
4
4
  type BillingOperationOutcome = 'succeeded' | 'failed' | 'cancelled';
5
5
  interface BillingOperationContext {
6
6
  runId: string;
@@ -10,8 +10,9 @@ interface BillingOperationContext {
10
10
  captureId?: string | null;
11
11
  videoId?: string | null;
12
12
  clipRecordId?: string | null;
13
+ interactiveDemoStateId?: string | null;
13
14
  operationType: BillingOperationType;
14
- captureType?: 'fullpage' | 'element' | 'video' | null;
15
+ captureType?: 'fullpage' | 'element' | null;
15
16
  lang?: string | null;
16
17
  theme?: 'light' | 'dark' | string | null;
17
18
  elementName?: string;
@@ -32,7 +33,7 @@ interface BillingOperationParams {
32
33
  export declare function insertBillingOperationLog(supabase: SupabaseClient, ctx: BillingOperationContext, params: BillingOperationParams): Promise<string | null>;
33
34
  export declare function insertScreenshotOperationLog(supabase: SupabaseClient, ctx: Omit<BillingOperationContext, 'operationType'>, params: BillingOperationParams): Promise<string | null>;
34
35
  export declare function insertClipOperationLog(supabase: SupabaseClient, ctx: Omit<BillingOperationContext, 'operationType'>, params: BillingOperationParams): Promise<string | null>;
35
- export declare function insertVideoOperationLog(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>;
36
37
  export declare function cancelScreenshotOperationLogsForCapture(supabase: SupabaseClient, captureId: string, reason: string): Promise<void>;
37
38
  export declare function reconcilePendingBillingOperationCosts(supabase: SupabaseClient, operationIds?: string[]): Promise<void>;
38
39
  export {};
@@ -131,6 +131,7 @@ 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,
134
135
  operation_type: ctx.operationType,
135
136
  operation_outcome: params.outcome,
136
137
  outcome_reason: params.outcomeReason ?? null,
@@ -177,8 +178,8 @@ export async function insertScreenshotOperationLog(supabase, ctx, params) {
177
178
  export async function insertClipOperationLog(supabase, ctx, params) {
178
179
  return insertBillingOperationLog(supabase, { ...ctx, operationType: 'clip' }, params);
179
180
  }
180
- export async function insertVideoOperationLog(supabase, ctx, params) {
181
- return insertBillingOperationLog(supabase, { ...ctx, operationType: 'video' }, params);
181
+ export async function insertInteractiveDemoOperationLog(supabase, ctx, params) {
182
+ return insertBillingOperationLog(supabase, { ...ctx, operationType: 'interactive_demo' }, params);
182
183
  }
183
184
  export async function cancelScreenshotOperationLogsForCapture(supabase, captureId, reason) {
184
185
  try {
package/dist/browser.d.ts CHANGED
@@ -107,20 +107,20 @@ export declare class Browser {
107
107
  */
108
108
  static fromPool(options: BrowserOptions): Promise<Browser>;
109
109
  /**
110
- * Create a Browser with a dedicated Chromium process and video recording enabled.
111
- * The context is configured with `recordVideo` so Playwright records the session.
110
+ * Create a Browser dedicated to clip capture. Frames are pulled via CDP
111
+ * `Page.captureScreenshot` in a tight loop by `ClipCaptureLoop` NOT via
112
+ * Playwright's built-in `recordVideo` (which plateaus at 27 FPS with ~12%
113
+ * duplicates due to the CDP screencast throttler).
112
114
  *
113
- * IMPORTANT: After calling `browser.close()`, retrieve the video file path via
114
- * `browser.currentPage.video()?.path()` BEFORE closing (save ref beforehand).
115
- * Or call `browser.saveVideo(outputPath)` before closing.
116
- *
117
- * @param videoDir - Directory where Playwright will write the WebM file.
115
+ * Preserves the HiDPI rendering path (`--force-device-scale-factor` +
116
+ * `--window-size`) so the captured frames match viewport × DSF pixels, and
117
+ * the cursor overlay script so clicks/hover moments are visible.
118
118
  */
119
- static forVideoRecording(options: BrowserOptions, videoDir: string, cursorScript: string): Promise<Browser>;
119
+ static forClipCapture(options: BrowserOptions, cursorScript: string): Promise<Browser>;
120
120
  /**
121
121
  * Close only the browser context (not the browser process).
122
- * Use this for video recording: closing the context finalizes the WebM on disk
123
- * while keeping the browser process alive so that saveAs() can still use the IPC channel.
122
+ * Used by clip capture to release the context promptly after the CDP loop
123
+ * has stopped, while keeping the browser process alive for any pending teardown.
124
124
  * Call browser.close() afterwards to shut down the browser process.
125
125
  */
126
126
  closeContext(): Promise<void>;
package/dist/browser.js CHANGED
@@ -141,16 +141,6 @@ function normalizeDeviceScaleFactor(value) {
141
141
  return 2;
142
142
  return Math.max(0.5, Math.min(4, Number(value)));
143
143
  }
144
- function resolveRecordedVideoSize(viewport) {
145
- // Playwright's video recorder expects a canvas size close to the CSS viewport.
146
- // Using viewport × deviceScaleFactor can produce a larger recording surface
147
- // with the page rendered only in the top-left corner, leaving the remainder
148
- // as empty matte. Keep the recorded frame size aligned to the viewport and
149
- // let the browser's deviceScaleFactor handle HiDPI rendering internally.
150
- const width = Math.max(2, Math.round(viewport.width)) & ~1;
151
- const height = Math.max(2, Math.round(viewport.height)) & ~1;
152
- return { width, height };
153
- }
154
144
  function escapeCssAttributeValue(value) {
155
145
  return value
156
146
  .replace(/\\/g, '\\\\')
@@ -327,24 +317,42 @@ export class Browser {
327
317
  return instance;
328
318
  }
329
319
  /**
330
- * Create a Browser with a dedicated Chromium process and video recording enabled.
331
- * The context is configured with `recordVideo` so Playwright records the session.
332
- *
333
- * IMPORTANT: After calling `browser.close()`, retrieve the video file path via
334
- * `browser.currentPage.video()?.path()` BEFORE closing (save ref beforehand).
335
- * Or call `browser.saveVideo(outputPath)` before closing.
320
+ * Create a Browser dedicated to clip capture. Frames are pulled via CDP
321
+ * `Page.captureScreenshot` in a tight loop by `ClipCaptureLoop` NOT via
322
+ * Playwright's built-in `recordVideo` (which plateaus at 27 FPS with ~12%
323
+ * duplicates due to the CDP screencast throttler).
336
324
  *
337
- * @param videoDir - Directory where Playwright will write the WebM file.
325
+ * Preserves the HiDPI rendering path (`--force-device-scale-factor` +
326
+ * `--window-size`) so the captured frames match viewport × DSF pixels, and
327
+ * the cursor overlay script so clicks/hover moments are visible.
338
328
  */
339
- static async forVideoRecording(options, videoDir, cursorScript) {
329
+ static async forClipCapture(options, cursorScript) {
340
330
  const instance = new Browser(options);
341
331
  const deviceScaleFactor = normalizeDeviceScaleFactor(options.deviceScaleFactor);
342
- const recordedVideoSize = resolveRecordedVideoSize(options.viewport);
343
- // Dedicated browser process for video cannot use the pool because
344
- // `recordVideo` must be set at context creation time.
332
+ // Enable GPU compositor on non-Linux platforms so Chrome can render
333
+ // 2880×1800 without saturating the CPU. Linux (Docker/CI) keeps
334
+ // `--disable-gpu` from CHROMIUM_ARGS because GPU is rarely available there.
335
+ const baseArgs = process.platform === 'linux'
336
+ ? CHROMIUM_ARGS
337
+ : CHROMIUM_ARGS.filter(arg => arg !== '--disable-gpu' && arg !== '--disable-gpu-sandbox');
338
+ // Pin ANGLE to the platform's native graphics API. Chrome's default
339
+ // backend is OpenGL on macOS, which is far slower than Metal for the
340
+ // compositor (measured 4 FPS vs 32 FPS at 2880×1800 on a heavy React UI).
341
+ // Same story on Windows where D3D11 is the native fast path.
342
+ const angleArg = process.platform === 'darwin' ? '--use-angle=metal'
343
+ : process.platform === 'win32' ? '--use-angle=d3d11'
344
+ : null; // Linux: skip — GPU is rarely present in CI anyway
345
+ const clipArgs = [
346
+ ...baseArgs,
347
+ `--force-device-scale-factor=${deviceScaleFactor}`,
348
+ `--window-size=${Math.round(options.viewport.width)},${Math.round(options.viewport.height)}`,
349
+ ...(angleArg ? [angleArg] : []),
350
+ ];
351
+ // Dedicated browser process for clip capture. Not pooled because clip
352
+ // capture installs context-level init scripts (cursor overlay).
345
353
  instance.browser = await chromium.launch({
346
354
  headless: !options.headed,
347
- args: CHROMIUM_ARGS,
355
+ args: clipArgs,
348
356
  });
349
357
  instance.context = await instance.browser.newContext({
350
358
  viewport: options.viewport,
@@ -352,10 +360,6 @@ export class Browser {
352
360
  locale: langToLocale(options.lang ?? 'en'),
353
361
  colorScheme: options.colorScheme ?? 'light',
354
362
  storageState: options.storageState,
355
- recordVideo: {
356
- dir: videoDir,
357
- size: recordedVideoSize,
358
- },
359
363
  });
360
364
  // Inject cursor overlay at context level — survives all navigations in this session
361
365
  await instance.context.addInitScript(cursorScript);
@@ -364,8 +368,8 @@ export class Browser {
364
368
  }
365
369
  /**
366
370
  * Close only the browser context (not the browser process).
367
- * Use this for video recording: closing the context finalizes the WebM on disk
368
- * while keeping the browser process alive so that saveAs() can still use the IPC channel.
371
+ * Used by clip capture to release the context promptly after the CDP loop
372
+ * has stopped, while keeping the browser process alive for any pending teardown.
369
373
  * Call browser.close() afterwards to shut down the browser process.
370
374
  */
371
375
  async closeContext() {
@@ -1,9 +1,11 @@
1
1
  export interface EncryptedEnvelope {
2
2
  __encrypted: true;
3
- version: 1;
3
+ version: 1 | 2;
4
+ keyVersion?: 2;
4
5
  iv: string;
5
6
  tag: string;
6
7
  ciphertext: string;
8
+ salt?: string;
7
9
  }
8
10
  export declare function encrypt(plaintext: string, secret: string): EncryptedEnvelope;
9
11
  export declare function decrypt(envelope: EncryptedEnvelope, secret: string): string;
@@ -1,9 +1,18 @@
1
1
  import crypto from 'node:crypto';
2
- function deriveKey(secret) {
2
+ function deriveLegacyKey(secret) {
3
3
  return crypto.createHash('sha256').update(secret).digest();
4
4
  }
5
+ function deriveKey(secret, salt) {
6
+ return crypto.scryptSync(secret, salt, 32, {
7
+ N: 1 << 15,
8
+ r: 8,
9
+ p: 1,
10
+ maxmem: 128 * 1024 * 1024,
11
+ });
12
+ }
5
13
  export function encrypt(plaintext, secret) {
6
- const key = deriveKey(secret);
14
+ const salt = crypto.randomBytes(16);
15
+ const key = deriveKey(secret, salt);
7
16
  const iv = crypto.randomBytes(12);
8
17
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
9
18
  const ciphertext = Buffer.concat([
@@ -13,14 +22,18 @@ export function encrypt(plaintext, secret) {
13
22
  const tag = cipher.getAuthTag();
14
23
  return {
15
24
  __encrypted: true,
16
- version: 1,
25
+ version: 2,
26
+ keyVersion: 2,
17
27
  iv: iv.toString('base64'),
18
28
  tag: tag.toString('base64'),
19
29
  ciphertext: ciphertext.toString('base64'),
30
+ salt: salt.toString('base64'),
20
31
  };
21
32
  }
22
33
  export function decrypt(envelope, secret) {
23
- const key = deriveKey(secret);
34
+ const key = envelope.version === 1
35
+ ? deriveLegacyKey(secret)
36
+ : deriveKey(secret, Buffer.from(envelope.salt ?? '', 'base64'));
24
37
  const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(envelope.iv, 'base64'));
25
38
  decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'));
26
39
  return Buffer.concat([
@@ -33,9 +46,11 @@ export function isEncryptedEnvelope(value) {
33
46
  return false;
34
47
  const candidate = value;
35
48
  return (candidate.__encrypted === true
36
- && candidate.version === 1
49
+ && (candidate.version === 1 || candidate.version === 2)
37
50
  && typeof candidate.iv === 'string'
38
51
  && typeof candidate.tag === 'string'
39
- && typeof candidate.ciphertext === 'string');
52
+ && typeof candidate.ciphertext === 'string'
53
+ && (candidate.version === 1
54
+ || (candidate.keyVersion === 2 && typeof candidate.salt === 'string')));
40
55
  }
41
56
  //# sourceMappingURL=capture-encryption.js.map
@@ -34,8 +34,9 @@ export class ClipStrategy {
34
34
  mediaMode = 'clip';
35
35
  async prepare(adapter, _spec) {
36
36
  // Clip recording preparation:
37
- // - The browser context is created with video recording enabled
38
- // - Cursor overlay is injected by Browser.forVideoRecording()
37
+ // - Browser is launched via Browser.forClipCapture() with HiDPI flags
38
+ // - Cursor overlay script is injected at context level
39
+ // - Frame capture starts in adapter.beginRecording() via ClipCaptureLoop
39
40
  }
40
41
  async capture(adapter, _spec) {
41
42
  // Clip capture is bounded by BEGIN_CLIP / END_CLIP opcodes.
@@ -9,6 +9,7 @@ declare const LOCAL_API_BASE_URL = "http://localhost:3000";
9
9
  declare const LOCAL_WS_URL = "ws://localhost:3000/ws";
10
10
  declare const API_BASE_URL_ENV_VAR = "AUTOKAP_API_BASE_URL";
11
11
  declare const WS_URL_ENV_VAR = "AUTOKAP_WS_URL";
12
+ declare const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = "AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN";
12
13
  export declare function getConfigDir(): string;
13
14
  export declare function getConfigPath(): string;
14
15
  export declare function getDefaultApiBaseUrl(): string;
@@ -17,4 +18,4 @@ export declare function readConfig(): Promise<AutokapConfig | null>;
17
18
  export declare function writeConfig(config: AutokapConfig): Promise<void>;
18
19
  export declare function deleteConfig(): Promise<void>;
19
20
  export declare function requireConfig(): Promise<AutokapConfig>;
20
- export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, };
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, };
@@ -8,6 +8,7 @@ const LOCAL_API_BASE_URL = 'http://localhost:3000';
8
8
  const LOCAL_WS_URL = 'ws://localhost:3000/ws';
9
9
  const API_BASE_URL_ENV_VAR = 'AUTOKAP_API_BASE_URL';
10
10
  const WS_URL_ENV_VAR = 'AUTOKAP_WS_URL';
11
+ const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = 'AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN';
11
12
  export function getConfigDir() {
12
13
  return path.join(os.homedir(), '.autokap');
13
14
  }
@@ -32,6 +33,10 @@ export async function readConfig() {
32
33
  const apiBaseUrl = envApiBaseUrl ?? storedApiBaseUrl;
33
34
  const wsUrl = envWsUrl
34
35
  ?? (envApiBaseUrl ? deriveWsUrl(apiBaseUrl) : normalizeUrl(parsed.wsUrl) ?? deriveWsUrl(apiBaseUrl));
36
+ assertAllowedApiOrigin(apiBaseUrl, storedApiBaseUrl, envApiBaseUrl ? API_BASE_URL_ENV_VAR : undefined);
37
+ if (envWsUrl) {
38
+ assertAllowedApiOrigin(wsUrl.replace(/^ws/, 'http'), deriveWsUrl(storedApiBaseUrl).replace(/^ws/, 'http'), WS_URL_ENV_VAR);
39
+ }
35
40
  return {
36
41
  apiKey: parsed.apiKey,
37
42
  apiBaseUrl,
@@ -67,7 +72,14 @@ export async function deleteConfig() {
67
72
  }
68
73
  }
69
74
  export async function requireConfig() {
70
- const config = await readConfig();
75
+ let config = null;
76
+ try {
77
+ config = await readConfig();
78
+ }
79
+ catch (error) {
80
+ logger.error(error instanceof Error ? error.message : String(error));
81
+ process.exit(1);
82
+ }
71
83
  if (!config) {
72
84
  logger.error('Not authenticated. Run `npx autokap@latest init --cli-key <key>` first.');
73
85
  process.exit(1);
@@ -94,5 +106,42 @@ function deriveWsUrl(apiBaseUrl) {
94
106
  return DEFAULT_WS_URL;
95
107
  }
96
108
  }
97
- export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, };
109
+ function normalizeOrigin(value) {
110
+ const normalized = normalizeUrl(value);
111
+ if (!normalized)
112
+ return null;
113
+ try {
114
+ const parsed = new URL(normalized);
115
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
116
+ return null;
117
+ return parsed.origin;
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ function isUnsafeOverrideEnabled() {
124
+ const raw = process.env[ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR]?.trim().toLowerCase();
125
+ return raw === '1' || raw === 'true';
126
+ }
127
+ function isTrustedOrigin(origin) {
128
+ return origin === normalizeOrigin(DEFAULT_API_BASE_URL)
129
+ || origin === normalizeOrigin(LOCAL_API_BASE_URL);
130
+ }
131
+ function assertAllowedApiOrigin(candidateUrl, baselineUrl, envVar) {
132
+ const candidateOrigin = normalizeOrigin(candidateUrl);
133
+ const baselineOrigin = normalizeOrigin(baselineUrl);
134
+ if (!candidateOrigin || !baselineOrigin) {
135
+ throw new Error('AutoKap CLI config contains an invalid server origin');
136
+ }
137
+ if (candidateOrigin === baselineOrigin || isTrustedOrigin(candidateOrigin)) {
138
+ return;
139
+ }
140
+ if (isUnsafeOverrideEnabled()) {
141
+ logger.warn(`[config] Allowing unsafe server override from ${baselineOrigin} to ${candidateOrigin} because ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1`);
142
+ return;
143
+ }
144
+ throw new Error(`Refusing unsafe server override to ${candidateOrigin}. Set ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1 to allow ${envVar ?? 'this override'} explicitly.`);
145
+ }
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, };
98
147
  //# sourceMappingURL=cli-config.js.map
@@ -5,11 +5,15 @@ export interface CliPublicCommandDescriptor {
5
5
  docsDescriptionKey?: string;
6
6
  }
7
7
  export declare const CLI_KEY_TERM = "CLI key";
8
+ export declare const CLI_VERSION_HEADER = "x-autokap-cli-version";
9
+ export declare const MIN_CLI_VERSION_FOR_SIGNED_PROGRAMS = "1.0.9";
8
10
  export declare const CLI_DEFAULT_INSTALL_COMMAND = "npm install -g autokap";
11
+ export declare const CLI_DEFAULT_INSTALLED_SETUP_COMMAND = "autokap init --cli-key <your-cli-key>";
9
12
  export declare const CLI_DEFAULT_SETUP_COMMAND = "npx autokap@latest init --cli-key <your-cli-key>";
10
- export declare const CLI_ADVANCED_SKILL_COMMAND = "npx autokap@latest skill --agent <agent>";
13
+ export declare const CLI_ADVANCED_SKILL_COMMAND = "autokap skill --agent <agent>";
11
14
  export declare const CLI_FALLBACK_PROGRAM_COMMAND = "autokap run <preset-id> --program <file>";
12
15
  export declare function buildCliRunCommand(presetId: string, options?: {
13
16
  local?: boolean;
14
17
  }): string;
18
+ export declare function buildCliInstalledSetupCommand(cliKey: string): string;
15
19
  export declare const CLI_PUBLIC_COMMANDS: CliPublicCommandDescriptor[];
@@ -1,11 +1,17 @@
1
1
  export const CLI_KEY_TERM = "CLI key";
2
+ export const CLI_VERSION_HEADER = "x-autokap-cli-version";
3
+ export const MIN_CLI_VERSION_FOR_SIGNED_PROGRAMS = "1.0.9";
2
4
  export const CLI_DEFAULT_INSTALL_COMMAND = "npm install -g autokap";
5
+ export const CLI_DEFAULT_INSTALLED_SETUP_COMMAND = "autokap init --cli-key <your-cli-key>";
3
6
  export const CLI_DEFAULT_SETUP_COMMAND = "npx autokap@latest init --cli-key <your-cli-key>";
4
- export const CLI_ADVANCED_SKILL_COMMAND = "npx autokap@latest skill --agent <agent>";
7
+ export const CLI_ADVANCED_SKILL_COMMAND = "autokap skill --agent <agent>";
5
8
  export const CLI_FALLBACK_PROGRAM_COMMAND = "autokap run <preset-id> --program <file>";
6
9
  export function buildCliRunCommand(presetId, options = {}) {
7
10
  return `autokap run${options.local ? " --local" : ""} ${presetId}`;
8
11
  }
12
+ export function buildCliInstalledSetupCommand(cliKey) {
13
+ return `autokap init --cli-key ${cliKey}`;
14
+ }
9
15
  export const CLI_PUBLIC_COMMANDS = [
10
16
  {
11
17
  id: "init",
@@ -87,16 +87,29 @@ async function persistArtifactsLocally(presetId, outputDirOption, variants) {
87
87
  let suffix = '';
88
88
  if (artifact.mediaMode === 'dom') {
89
89
  if (artifact.fragmentName && artifact.parentStateName) {
90
- suffix = `-${artifact.parentStateName}-fragment-${artifact.fragmentName}`;
90
+ suffix = `-${sanitizePathToken(artifact.parentStateName)}-fragment-${sanitizePathToken(artifact.fragmentName)}`;
91
91
  }
92
92
  else if (artifact.stateName) {
93
- suffix = `-${artifact.stateName}`;
93
+ suffix = `-${sanitizePathToken(artifact.stateName)}`;
94
94
  }
95
95
  }
96
- const filePath = path.join(outputDir, `capture-${presetId.slice(0, 8)}-${variant.variantId}${suffix}-${index}.${ext}`);
96
+ const fileName = `capture-${sanitizePathToken(presetId.slice(0, 8))}-${sanitizePathToken(variant.variantId)}${suffix}-${index}.${ext}`;
97
+ const filePath = resolveContainedPath(outputDir, fileName);
97
98
  await fs.writeFile(filePath, artifact.buffer);
98
99
  logger.info(`[capture] Artifact saved locally: ${filePath}`);
99
100
  }
100
101
  }
101
102
  }
103
+ function sanitizePathToken(value) {
104
+ const cleaned = value.trim().replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/-+/g, '-');
105
+ return cleaned.length > 0 ? cleaned.slice(0, 80) : 'artifact';
106
+ }
107
+ function resolveContainedPath(outputDir, fileName) {
108
+ const resolved = path.resolve(outputDir, fileName);
109
+ const relative = path.relative(outputDir, resolved);
110
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
111
+ throw new Error(`Refusing to write outside output directory: ${fileName}`);
112
+ }
113
+ return resolved;
114
+ }
102
115
  //# sourceMappingURL=cli-runner-local.js.map