autokap 1.0.5 → 1.0.7

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 (130) hide show
  1. package/assets/chrome/ios-statusbar-comparison-reference.jpg +0 -0
  2. package/assets/chrome/ios-statusbar-dark-reference.jpg +0 -0
  3. package/assets/chrome/ios-statusbar-light-reference.jpg +0 -0
  4. package/assets/devices/ipad-pro-11-m4.json +52 -0
  5. package/assets/devices/iphone-16-pro.json +53 -0
  6. package/assets/devices/macbook-air-13.json +45 -0
  7. package/assets/frames/MacBook Air 13.svg +242 -0
  8. package/assets/frames/Status bar - iPhone.png +0 -0
  9. Menu bar- iPad.png +0 -0
  10. package/assets/frames/iPad Pro M4 11_.png +0 -0
  11. package/assets/frames/iPhone 16 Pro.png +0 -0
  12. package/assets/icons/Cellular Connection.svg +3 -0
  13. package/assets/icons/Union.svg +6 -0
  14. package/assets/icons/Wifi.svg +3 -0
  15. package/assets/icons/battery.svg +5 -0
  16. package/assets/icons/battery_charging.svg +8 -0
  17. package/dist/abort.d.ts +5 -0
  18. package/dist/abort.js +44 -0
  19. package/dist/agent.d.ts +142 -0
  20. package/dist/agent.js +4511 -0
  21. package/dist/billing-operation-logging.d.ts +38 -0
  22. package/dist/billing-operation-logging.js +248 -0
  23. package/dist/browser-bar.d.ts +40 -0
  24. package/dist/browser-bar.js +147 -0
  25. package/dist/browser.d.ts +25 -0
  26. package/dist/browser.js +177 -9
  27. package/dist/capture-alt-text.d.ts +12 -0
  28. package/dist/capture-alt-text.js +51 -0
  29. package/dist/capture-encryption.d.ts +10 -0
  30. package/dist/capture-encryption.js +41 -0
  31. package/dist/capture-language-preflight.d.ts +41 -0
  32. package/dist/capture-language-preflight.js +286 -0
  33. package/dist/capture-llm-page-identity.d.ts +15 -0
  34. package/dist/capture-llm-page-identity.js +116 -0
  35. package/dist/capture-model-resolution.d.ts +9 -0
  36. package/dist/capture-model-resolution.js +21 -0
  37. package/dist/capture-page-identity.d.ts +9 -0
  38. package/dist/capture-page-identity.js +219 -0
  39. package/dist/capture-preset-credentials.d.ts +12 -0
  40. package/dist/capture-preset-credentials.js +57 -0
  41. package/dist/capture-request-plan.d.ts +58 -0
  42. package/dist/capture-request-plan.js +216 -0
  43. package/dist/capture-run-optimizer.d.ts +139 -0
  44. package/dist/capture-run-optimizer.js +848 -0
  45. package/dist/capture-selector-memory.d.ts +26 -0
  46. package/dist/capture-selector-memory.js +327 -0
  47. package/dist/capture-session-profile-encryption.d.ts +2 -0
  48. package/dist/capture-session-profile-encryption.js +22 -0
  49. package/dist/capture-step-timeout.d.ts +10 -0
  50. package/dist/capture-step-timeout.js +30 -0
  51. package/dist/capture-studio-sync.d.ts +22 -0
  52. package/dist/capture-studio-sync.js +166 -0
  53. package/dist/capture-variant-state.d.ts +54 -0
  54. package/dist/capture-variant-state.js +156 -0
  55. package/dist/cli.js +21 -0
  56. package/dist/clip-orchestrator.d.ts +148 -0
  57. package/dist/clip-orchestrator.js +950 -0
  58. package/dist/clip-postprocess.d.ts +42 -0
  59. package/dist/clip-postprocess.js +192 -0
  60. package/dist/cost-logging.d.ts +27 -0
  61. package/dist/cost-logging.js +128 -0
  62. package/dist/credential-templates.d.ts +5 -0
  63. package/dist/credential-templates.js +60 -0
  64. package/dist/element-capture.d.ts +53 -0
  65. package/dist/element-capture.js +766 -0
  66. package/dist/hybrid-navigator.d.ts +138 -0
  67. package/dist/hybrid-navigator.js +468 -0
  68. package/dist/index.d.ts +15 -0
  69. package/dist/index.js +11 -0
  70. package/dist/llm-usage.d.ts +17 -0
  71. package/dist/llm-usage.js +45 -0
  72. package/dist/mockup-html.d.ts +119 -0
  73. package/dist/mockup-html.js +253 -0
  74. package/dist/mockup.d.ts +94 -0
  75. package/dist/mockup.js +608 -0
  76. package/dist/mouse-animation.d.ts +46 -0
  77. package/dist/mouse-animation.js +100 -0
  78. package/dist/overlay-utils.d.ts +14 -0
  79. package/dist/overlay-utils.js +13 -0
  80. package/dist/posthog.d.ts +4 -0
  81. package/dist/posthog.js +26 -0
  82. package/dist/prompt-cache.d.ts +10 -0
  83. package/dist/prompt-cache.js +24 -0
  84. package/dist/prompts.d.ts +167 -0
  85. package/dist/prompts.js +1165 -0
  86. package/dist/remote-browser.d.ts +191 -0
  87. package/dist/remote-browser.js +305 -0
  88. package/dist/security.d.ts +20 -0
  89. package/dist/security.js +569 -0
  90. package/dist/server-capture-runtime.d.ts +123 -0
  91. package/dist/server-capture-runtime.js +638 -0
  92. package/dist/server-credit-usage.d.ts +12 -0
  93. package/dist/server-credit-usage.js +41 -0
  94. package/dist/server-posthog.d.ts +2 -0
  95. package/dist/server-posthog.js +16 -0
  96. package/dist/server-project-webhooks.d.ts +45 -0
  97. package/dist/server-project-webhooks.js +97 -0
  98. package/dist/server-screenshot-watermark.d.ts +7 -0
  99. package/dist/server-screenshot-watermark.js +38 -0
  100. package/dist/session-profile.d.ts +86 -0
  101. package/dist/session-profile.js +1373 -0
  102. package/dist/sf-pro-fonts.d.ts +4 -0
  103. package/dist/sf-pro-fonts.js +7 -0
  104. package/dist/status-bar-l10n.d.ts +14 -0
  105. package/dist/status-bar-l10n.js +177 -0
  106. package/dist/status-bar.d.ts +44 -0
  107. package/dist/status-bar.js +336 -0
  108. package/dist/tools.d.ts +4 -0
  109. package/dist/tools.js +578 -0
  110. package/dist/video-agent.d.ts +143 -0
  111. package/dist/video-agent.js +4783 -0
  112. package/dist/video-observation.d.ts +36 -0
  113. package/dist/video-observation.js +192 -0
  114. package/dist/video-planner.d.ts +12 -0
  115. package/dist/video-planner.js +500 -0
  116. package/dist/video-prompts.d.ts +37 -0
  117. package/dist/video-prompts.js +554 -0
  118. package/dist/video-tools.d.ts +3 -0
  119. package/dist/video-tools.js +59 -0
  120. package/dist/video-variant-state.d.ts +29 -0
  121. package/dist/video-variant-state.js +80 -0
  122. package/dist/vision-model.d.ts +17 -0
  123. package/dist/vision-model.js +74 -0
  124. package/dist/ws-auth.d.ts +20 -0
  125. package/dist/ws-auth.js +67 -0
  126. package/dist/ws-handler.d.ts +10 -0
  127. package/dist/ws-handler.js +1663 -0
  128. package/dist/ws-server.d.ts +9 -0
  129. package/dist/ws-server.js +52 -0
  130. package/package.json +93 -39
package/dist/browser.js CHANGED
@@ -366,15 +366,57 @@ export class Browser {
366
366
  headless: !this.options.headed,
367
367
  args: CHROMIUM_ARGS,
368
368
  });
369
- this.context = await this.browser.newContext({
370
- viewport: this.options.viewport,
371
- deviceScaleFactor: normalizeDeviceScaleFactor(this.options.deviceScaleFactor),
372
- locale: langToLocale(this.options.lang ?? 'en'),
373
- colorScheme: this.options.colorScheme ?? 'light',
374
- storageState: this.options.storageState,
375
- });
369
+ this.context = await this.browser.newContext(this.buildContextOptions());
376
370
  this.page = await this.context.newPage();
377
371
  }
372
+ async recreateContext(options = {}) {
373
+ if (this.poolContext) {
374
+ throw new Error('Cannot recreate the context for a pooled browser instance');
375
+ }
376
+ this.options = {
377
+ ...this.options,
378
+ ...options,
379
+ viewport: options.viewport ?? this.options.viewport,
380
+ deviceScaleFactor: options.deviceScaleFactor ?? this.options.deviceScaleFactor,
381
+ lang: options.lang ?? this.options.lang,
382
+ colorScheme: options.colorScheme ?? this.options.colorScheme,
383
+ storageState: options.storageState ?? this.options.storageState,
384
+ };
385
+ if (!this.browser) {
386
+ await this.launch();
387
+ return;
388
+ }
389
+ if (this.page) {
390
+ try {
391
+ await this.page.close();
392
+ }
393
+ catch { /* ignore */ }
394
+ this.page = null;
395
+ }
396
+ if (this.context) {
397
+ try {
398
+ await this.context.close();
399
+ }
400
+ catch { /* ignore */ }
401
+ this.context = null;
402
+ }
403
+ this.context = await this.browser.newContext(this.buildContextOptions());
404
+ this.page = await this.context.newPage();
405
+ this.elementMap.clear();
406
+ }
407
+ async setDeviceScaleFactor(deviceScaleFactor) {
408
+ const normalizedScale = normalizeDeviceScaleFactor(deviceScaleFactor);
409
+ if (normalizeDeviceScaleFactor(this.options.deviceScaleFactor) === normalizedScale) {
410
+ return;
411
+ }
412
+ const sessionStorage = this.page
413
+ ? await this.exportSessionStorage().catch(() => undefined)
414
+ : undefined;
415
+ await this.recreateContext({ deviceScaleFactor: normalizedScale });
416
+ if (sessionStorage && Object.keys(sessionStorage).length > 0) {
417
+ await this.prepareSessionStorage(sessionStorage, { replace: false });
418
+ }
419
+ }
378
420
  async addCookies(cookies) {
379
421
  const context = this.ensureContext();
380
422
  await context.addCookies(cookies.map(c => ({
@@ -987,6 +1029,116 @@ export class Browser {
987
1029
  [snapshot.origin]: snapshot.entries,
988
1030
  };
989
1031
  }
1032
+ async reloadCurrentPage(options = {}) {
1033
+ const page = this.ensurePage();
1034
+ await page.reload({
1035
+ waitUntil: options.waitUntil ?? 'domcontentloaded',
1036
+ timeout: options.timeout ?? 15_000,
1037
+ }).catch(async () => {
1038
+ await this.navigateTo(page.url());
1039
+ });
1040
+ }
1041
+ async writeStorageHintCandidate(params) {
1042
+ const page = this.ensurePage();
1043
+ return page.evaluate(({ storageName, key, candidate, kind }) => {
1044
+ const storage = storageName === 'localStorage' ? window.localStorage : window.sessionStorage;
1045
+ const current = storage.getItem(key);
1046
+ if (current == null)
1047
+ return false;
1048
+ if (current === candidate)
1049
+ return true;
1050
+ const LOCALE_KEY_WHITELIST = ['lang', 'locale', 'language', 'i18n', 'intl', 'i18n-locale', 'next-i18next', 'NEXT_LOCALE', 'nuxt-i18n-locale'];
1051
+ const THEME_KEY_WHITELIST = ['theme', 'color-scheme', 'colorScheme', 'dark-mode', 'darkMode', 'appearance'];
1052
+ const whitelist = kind === 'locale' ? LOCALE_KEY_WHITELIST : THEME_KEY_WHITELIST;
1053
+ const isAllowedKey = (inputKey) => {
1054
+ const lower = inputKey.toLowerCase();
1055
+ return whitelist.some((entry) => {
1056
+ const normalized = entry.toLowerCase();
1057
+ if (lower === normalized)
1058
+ return true;
1059
+ const re = new RegExp(`(?:^|[^a-zA-Z0-9])${normalized.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}(?:$|[^a-zA-Z0-9])`, 'i');
1060
+ return re.test(lower);
1061
+ });
1062
+ };
1063
+ const looksLikeTargetValue = (value) => {
1064
+ if (kind === 'locale') {
1065
+ return /^[a-z]{2,3}(-[a-zA-Z]{2,4})?$/.test(value.trim());
1066
+ }
1067
+ return /^(light|dark|auto|system|dim|high-contrast)$/i.test(value.trim());
1068
+ };
1069
+ const rewrite = (input, parentKeyAllowed = false) => {
1070
+ if (typeof input === 'string') {
1071
+ if (!looksLikeTargetValue(input))
1072
+ return { changed: false, value: input };
1073
+ return { changed: input !== candidate, value: candidate };
1074
+ }
1075
+ if (Array.isArray(input)) {
1076
+ if (!parentKeyAllowed)
1077
+ return { changed: false, value: input };
1078
+ let changed = false;
1079
+ const next = input.map((entry) => {
1080
+ const rewritten = rewrite(entry, true);
1081
+ changed = changed || rewritten.changed;
1082
+ return rewritten.value;
1083
+ });
1084
+ return { changed, value: next };
1085
+ }
1086
+ if (input && typeof input === 'object') {
1087
+ let changed = false;
1088
+ const next = {};
1089
+ for (const [entryKey, entryValue] of Object.entries(input)) {
1090
+ const keyAllowed = isAllowedKey(entryKey);
1091
+ if (keyAllowed && typeof entryValue === 'string' && looksLikeTargetValue(entryValue)) {
1092
+ if (entryValue !== candidate)
1093
+ changed = true;
1094
+ next[entryKey] = candidate;
1095
+ continue;
1096
+ }
1097
+ const rewritten = rewrite(entryValue, keyAllowed);
1098
+ changed = changed || rewritten.changed;
1099
+ next[entryKey] = rewritten.value;
1100
+ }
1101
+ return { changed, value: next };
1102
+ }
1103
+ return { changed: false, value: input };
1104
+ };
1105
+ let nextValue = candidate;
1106
+ try {
1107
+ const parsed = JSON.parse(current);
1108
+ const rewritten = rewrite(parsed);
1109
+ if (rewritten.changed) {
1110
+ nextValue = JSON.stringify(rewritten.value);
1111
+ }
1112
+ }
1113
+ catch {
1114
+ nextValue = candidate;
1115
+ }
1116
+ storage.setItem(key, nextValue);
1117
+ return true;
1118
+ }, params);
1119
+ }
1120
+ async probeSelector(selector) {
1121
+ const page = this.ensurePage();
1122
+ try {
1123
+ return await page.locator(selector).first().evaluate((node) => ({
1124
+ tag: node.tagName.toLowerCase(),
1125
+ role: node.getAttribute('role') || '',
1126
+ href: node instanceof HTMLAnchorElement ? node.href : node.getAttribute('href'),
1127
+ label: (node.getAttribute('aria-label')
1128
+ || node.getAttribute('title')
1129
+ || node.innerText
1130
+ || node.textContent
1131
+ || '').replace(/\s+/g, ' ').trim().slice(0, 120),
1132
+ inputType: node instanceof HTMLInputElement ? node.type : node.getAttribute('type'),
1133
+ ariaExpanded: node.getAttribute('aria-expanded'),
1134
+ ariaControls: node.getAttribute('aria-controls'),
1135
+ ariaHasPopup: node.getAttribute('aria-haspopup'),
1136
+ }));
1137
+ }
1138
+ catch {
1139
+ return null;
1140
+ }
1141
+ }
990
1142
  async prepareSessionStorage(bundle, options = {}) {
991
1143
  if (!bundle || Object.keys(bundle).length === 0)
992
1144
  return;
@@ -2797,9 +2949,16 @@ export class Browser {
2797
2949
  }
2798
2950
  async resizeViewport(width, height) {
2799
2951
  const page = this.ensurePage();
2952
+ this.options = {
2953
+ ...this.options,
2954
+ viewport: {
2955
+ width: normalizeViewportDimension(width),
2956
+ height: normalizeViewportDimension(height),
2957
+ },
2958
+ };
2800
2959
  await page.setViewportSize({
2801
- width: normalizeViewportDimension(width),
2802
- height: normalizeViewportDimension(height),
2960
+ width: this.options.viewport.width,
2961
+ height: this.options.viewport.height,
2803
2962
  });
2804
2963
  }
2805
2964
  get currentPage() {
@@ -2898,5 +3057,14 @@ export class Browser {
2898
3057
  throw new Error('Browser not launched. Call launch() first.');
2899
3058
  return this.context;
2900
3059
  }
3060
+ buildContextOptions() {
3061
+ return {
3062
+ viewport: this.options.viewport,
3063
+ deviceScaleFactor: normalizeDeviceScaleFactor(this.options.deviceScaleFactor),
3064
+ locale: langToLocale(this.options.lang ?? 'en'),
3065
+ colorScheme: this.options.colorScheme ?? 'light',
3066
+ storageState: this.options.storageState,
3067
+ };
3068
+ }
2901
3069
  }
2902
3070
  //# sourceMappingURL=browser.js.map
@@ -0,0 +1,12 @@
1
+ interface GenerateAltTextParams {
2
+ url: string;
3
+ prompt: string;
4
+ lang: string;
5
+ theme: 'light' | 'dark';
6
+ targetLabel?: string;
7
+ elementName?: string;
8
+ model: string;
9
+ apiKey: string;
10
+ }
11
+ export declare function generateAltText(params: GenerateAltTextParams): Promise<string | null>;
12
+ export {};
@@ -0,0 +1,51 @@
1
+ export async function generateAltText(params) {
2
+ const { url, prompt, lang, theme, targetLabel, elementName, model, apiKey } = params;
3
+ const contextParts = [
4
+ `URL: ${url}`,
5
+ `Capture intent: "${prompt}"`,
6
+ `Theme: ${theme}`,
7
+ ];
8
+ if (targetLabel)
9
+ contextParts.push(`Device: ${targetLabel}`);
10
+ if (elementName)
11
+ contextParts.push(`Isolated element: ${elementName}`);
12
+ const systemPrompt = `You generate concise alt text for web page screenshots, for accessibility purposes.
13
+ Write in ${lang}. Output ONLY the alt text, nothing else. Keep it under 150 characters.
14
+ Describe what the screenshot represents based on the context provided.`;
15
+ try {
16
+ const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
17
+ method: 'POST',
18
+ headers: {
19
+ Authorization: `Bearer ${apiKey}`,
20
+ 'Content-Type': 'application/json',
21
+ 'HTTP-Referer': 'https://autokap.app',
22
+ 'X-Title': 'Screenshot Agent',
23
+ },
24
+ body: JSON.stringify({
25
+ model,
26
+ max_tokens: 100,
27
+ stream: false,
28
+ messages: [
29
+ { role: 'system', content: systemPrompt },
30
+ { role: 'user', content: contextParts.join('\n') },
31
+ ],
32
+ provider: { zdr: true },
33
+ }),
34
+ signal: AbortSignal.timeout(5_000),
35
+ });
36
+ if (!res.ok) {
37
+ console.warn(`[generate-alt-text] LLM call failed: HTTP ${res.status}`);
38
+ return null;
39
+ }
40
+ const json = (await res.json());
41
+ const content = json.choices?.[0]?.message?.content?.trim();
42
+ if (!content)
43
+ return null;
44
+ return content.replace(/^["']|["']$/g, '');
45
+ }
46
+ catch {
47
+ console.warn('[generate-alt-text] ALT text generation failed');
48
+ return null;
49
+ }
50
+ }
51
+ //# sourceMappingURL=capture-alt-text.js.map
@@ -0,0 +1,10 @@
1
+ export interface EncryptedEnvelope {
2
+ __encrypted: true;
3
+ version: 1;
4
+ iv: string;
5
+ tag: string;
6
+ ciphertext: string;
7
+ }
8
+ export declare function encrypt(plaintext: string, secret: string): EncryptedEnvelope;
9
+ export declare function decrypt(envelope: EncryptedEnvelope, secret: string): string;
10
+ export declare function isEncryptedEnvelope(value: unknown): value is EncryptedEnvelope;
@@ -0,0 +1,41 @@
1
+ import crypto from 'node:crypto';
2
+ function deriveKey(secret) {
3
+ return crypto.createHash('sha256').update(secret).digest();
4
+ }
5
+ export function encrypt(plaintext, secret) {
6
+ const key = deriveKey(secret);
7
+ const iv = crypto.randomBytes(12);
8
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
9
+ const ciphertext = Buffer.concat([
10
+ cipher.update(plaintext, 'utf8'),
11
+ cipher.final(),
12
+ ]);
13
+ const tag = cipher.getAuthTag();
14
+ return {
15
+ __encrypted: true,
16
+ version: 1,
17
+ iv: iv.toString('base64'),
18
+ tag: tag.toString('base64'),
19
+ ciphertext: ciphertext.toString('base64'),
20
+ };
21
+ }
22
+ export function decrypt(envelope, secret) {
23
+ const key = deriveKey(secret);
24
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(envelope.iv, 'base64'));
25
+ decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'));
26
+ return Buffer.concat([
27
+ decipher.update(Buffer.from(envelope.ciphertext, 'base64')),
28
+ decipher.final(),
29
+ ]).toString('utf8');
30
+ }
31
+ export function isEncryptedEnvelope(value) {
32
+ if (!value || typeof value !== 'object')
33
+ return false;
34
+ const candidate = value;
35
+ return (candidate.__encrypted === true
36
+ && candidate.version === 1
37
+ && typeof candidate.iv === 'string'
38
+ && typeof candidate.tag === 'string'
39
+ && typeof candidate.ciphertext === 'string');
40
+ }
41
+ //# sourceMappingURL=capture-encryption.js.map
@@ -0,0 +1,41 @@
1
+ import type { Browser } from "./browser.js";
2
+ import { type RequestedLanguageState, type RequestedThemeState } from "./session-profile.js";
3
+ import type { AgentResult, LoginCredentials, StepUsage, ValidatedSessionProfile, VideoPageSignals } from "./types.js";
4
+ import { type ScreenshotSelectorMemoryUpdate } from "./capture-selector-memory.js";
5
+ interface LanguageObservation {
6
+ currentUrl: string;
7
+ signals: VideoPageSignals | null;
8
+ languageState: RequestedLanguageState | null;
9
+ themeState: RequestedThemeState | null;
10
+ }
11
+ export interface ScreenshotLanguagePreflightResult {
12
+ ok: boolean;
13
+ resolvedBy: "already_active" | "deterministic_repair" | "language_preflight_agent" | "blocked";
14
+ observation: LanguageObservation;
15
+ usage: StepUsage[];
16
+ selectorUpdates: ScreenshotSelectorMemoryUpdate[];
17
+ rebasedToStartUrl: boolean;
18
+ reason?: string;
19
+ }
20
+ export declare function ensureScreenshotVariantLanguage(params: {
21
+ browser: Browser;
22
+ requestedLang: string;
23
+ requestedTheme: "light" | "dark";
24
+ startUrl?: string;
25
+ profile?: ValidatedSessionProfile;
26
+ credentials?: LoginCredentials;
27
+ selectorMemory?: Record<string, string[]>;
28
+ langInstructions?: string;
29
+ themeInstructions?: string;
30
+ onLog?: (message: string) => void;
31
+ performRepair?: () => Promise<{
32
+ repaired: boolean;
33
+ updates: ScreenshotSelectorMemoryUpdate[];
34
+ } | null>;
35
+ rebaseToStartUrl?: (currentUrl: string, startUrl: string) => Promise<void>;
36
+ runLanguageSwitchAgent?: (state: {
37
+ languageState: RequestedLanguageState | null;
38
+ themeState: RequestedThemeState | null;
39
+ }) => Promise<AgentResult | null>;
40
+ }): Promise<ScreenshotLanguagePreflightResult>;
41
+ export {};
@@ -0,0 +1,286 @@
1
+ import { evaluateRequestedLanguageState, evaluateRequestedThemeState, performDeterministicSessionRepair, } from "./session-profile.js";
2
+ import { extractSelectorUpdates, } from "./capture-selector-memory.js";
3
+ import { isCaptureStepTimeoutError, withCaptureStepTimeout, } from "./capture-step-timeout.js";
4
+ import { urlMatchesCaptureTarget } from "./capture-run-optimizer.js";
5
+ const LANGUAGE_OBSERVATION_TIMEOUT_MS = 12000;
6
+ const LANGUAGE_REPAIR_TIMEOUT_MS = 15000;
7
+ const LANGUAGE_STABILITY_DELAY_MS = 900;
8
+ function urlsRoughlyMatch(expectedUrl, currentUrl) {
9
+ if (!expectedUrl || !currentUrl)
10
+ return false;
11
+ return urlMatchesCaptureTarget(currentUrl, expectedUrl) || urlMatchesCaptureTarget(expectedUrl, currentUrl);
12
+ }
13
+ async function waitForVariantStability(browser, delayMs) {
14
+ if (typeof browser.wait === "function") {
15
+ await browser.wait(delayMs);
16
+ return;
17
+ }
18
+ const pageWait = browser.currentPage.waitForTimeout;
19
+ if (typeof pageWait === "function") {
20
+ await pageWait.call(browser.currentPage, delayMs);
21
+ return;
22
+ }
23
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
24
+ }
25
+ function summarizeRequestedLanguageState(state) {
26
+ if (!state)
27
+ return "language_state=unavailable";
28
+ return [
29
+ state.requested ? `requested=${state.requested}` : null,
30
+ state.detected ? `detected=${state.detected}` : "detected=unknown",
31
+ `ambiguous=${state.ambiguous}`,
32
+ ...state.reasons.slice(0, 3),
33
+ ]
34
+ .filter(Boolean)
35
+ .join("; ");
36
+ }
37
+ function summarizeRequestedThemeState(state) {
38
+ if (!state)
39
+ return "theme_state=unavailable";
40
+ return [
41
+ state.requested ? `requested=${state.requested}` : null,
42
+ state.detected ? `detected=${state.detected}` : "detected=unknown",
43
+ `ambiguous=${state.ambiguous}`,
44
+ ...state.reasons.slice(0, 3),
45
+ ]
46
+ .filter(Boolean)
47
+ .join("; ");
48
+ }
49
+ function summarizeVariantObservation(observation) {
50
+ return [
51
+ summarizeRequestedLanguageState(observation.languageState),
52
+ summarizeRequestedThemeState(observation.themeState),
53
+ ].join(" | ");
54
+ }
55
+ function isObservationReady(observation) {
56
+ const languageReady = !!(observation.languageState?.active
57
+ && !observation.languageState.ambiguous);
58
+ const themeReady = !!(observation.themeState?.active
59
+ && !observation.themeState.ambiguous);
60
+ return languageReady && themeReady;
61
+ }
62
+ function buildFailureReason(requestedLang, requestedTheme, observation) {
63
+ const failures = [];
64
+ if (!observation.languageState) {
65
+ failures.push(`unable to inspect the current UI language for "${requestedLang}"`);
66
+ }
67
+ else if (!observation.languageState.active || observation.languageState.ambiguous) {
68
+ failures.push(`requested "${requestedLang}", detected "${observation.languageState.detected ?? "unknown"}" (${observation.languageState.reasons.join("; ")})`);
69
+ }
70
+ if (!observation.themeState) {
71
+ failures.push(`unable to inspect the current UI theme for "${requestedTheme}"`);
72
+ }
73
+ else if (!observation.themeState.active || observation.themeState.ambiguous) {
74
+ failures.push(`theme requested "${requestedTheme}", detected "${observation.themeState.detected ?? "unknown"}" (${observation.themeState.reasons.join("; ")})`);
75
+ }
76
+ if (failures.length === 0) {
77
+ return "";
78
+ }
79
+ return `Language preflight failed: ${failures.join(" | ")}.`;
80
+ }
81
+ async function observeLanguageState(browser, requestedLang, params) {
82
+ const currentUrl = browser.currentPage.url();
83
+ const phase = params?.phase ?? "inspection";
84
+ params?.onLog?.(`Language guard: inspecting fixed UI chrome during ${phase}.`);
85
+ const signals = await withCaptureStepTimeout(() => browser.capturePageSignals(), {
86
+ stepLabel: `inspecting fixed UI chrome during ${phase}`,
87
+ timeoutMs: LANGUAGE_OBSERVATION_TIMEOUT_MS,
88
+ }).catch((error) => {
89
+ if (isCaptureStepTimeoutError(error)) {
90
+ params?.onLog?.(`Language guard: fixed UI inspection timed out after ${error.timeoutMs}ms during ${phase}.`);
91
+ return null;
92
+ }
93
+ params?.onLog?.(`Language guard: fixed UI inspection failed during ${phase} (${error instanceof Error ? error.message : String(error)}).`);
94
+ return null;
95
+ });
96
+ return {
97
+ currentUrl,
98
+ signals,
99
+ languageState: signals
100
+ ? evaluateRequestedLanguageState({
101
+ currentUrl,
102
+ requestedLang,
103
+ signals,
104
+ })
105
+ : null,
106
+ themeState: signals
107
+ ? evaluateRequestedThemeState({
108
+ requestedTheme: params?.requestedTheme,
109
+ signals,
110
+ })
111
+ : null,
112
+ };
113
+ }
114
+ async function confirmStableLanguageState(params) {
115
+ if (!isObservationReady(params.observation)) {
116
+ return params.observation;
117
+ }
118
+ params.onLog?.(`Language guard: waiting ${LANGUAGE_STABILITY_DELAY_MS}ms to confirm the variant remains stable during ${params.phase}.`);
119
+ await waitForVariantStability(params.browser, LANGUAGE_STABILITY_DELAY_MS);
120
+ return observeLanguageState(params.browser, params.requestedLang, {
121
+ onLog: params.onLog,
122
+ phase: `${params.phase} stability verification`,
123
+ requestedTheme: params.requestedTheme,
124
+ });
125
+ }
126
+ export async function ensureScreenshotVariantLanguage(params) {
127
+ const usage = [];
128
+ const selectorUpdates = [];
129
+ const requestedLang = params.requestedLang.trim().toLowerCase();
130
+ let rebasedToStartUrl = false;
131
+ const rebaseIfNeeded = async (observation) => {
132
+ if (!params.startUrl
133
+ || !params.rebaseToStartUrl
134
+ || urlsRoughlyMatch(params.startUrl, observation.currentUrl)) {
135
+ return observation;
136
+ }
137
+ params.onLog?.(`Language guard: rebasing back to ${params.startUrl} after language switch from ${observation.currentUrl}.`);
138
+ await params.rebaseToStartUrl(observation.currentUrl, params.startUrl);
139
+ rebasedToStartUrl = true;
140
+ return observeLanguageState(params.browser, requestedLang, {
141
+ onLog: params.onLog,
142
+ phase: "post-rebase verification",
143
+ requestedTheme: params.requestedTheme,
144
+ });
145
+ };
146
+ let observation = await observeLanguageState(params.browser, requestedLang, {
147
+ onLog: params.onLog,
148
+ phase: "initial preflight",
149
+ requestedTheme: params.requestedTheme,
150
+ });
151
+ if (isObservationReady(observation)) {
152
+ return {
153
+ ok: true,
154
+ resolvedBy: "already_active",
155
+ observation,
156
+ usage,
157
+ selectorUpdates,
158
+ rebasedToStartUrl,
159
+ };
160
+ }
161
+ params.onLog?.(`Language guard: requested variant "${requestedLang}/${params.requestedTheme}" is not active yet; ${summarizeVariantObservation(observation)}.`);
162
+ params.onLog?.("Language guard: running deterministic variant repair before the main workflow.");
163
+ const repair = await withCaptureStepTimeout(async () => (params.performRepair
164
+ ?? (async () => {
165
+ const result = await performDeterministicSessionRepair(params.browser, {
166
+ startUrl: params.startUrl,
167
+ requestedLang,
168
+ requestedTheme: params.requestedTheme,
169
+ credentials: params.credentials,
170
+ profile: params.profile,
171
+ selectorMemory: params.selectorMemory,
172
+ }).catch(() => null);
173
+ return result
174
+ ? {
175
+ repaired: result.repaired,
176
+ updates: result.updates.map((update) => ({
177
+ ...update,
178
+ source: "deterministic",
179
+ })),
180
+ }
181
+ : null;
182
+ }))(), {
183
+ stepLabel: "running deterministic variant repair",
184
+ timeoutMs: LANGUAGE_REPAIR_TIMEOUT_MS,
185
+ }).catch((error) => {
186
+ if (isCaptureStepTimeoutError(error)) {
187
+ params.onLog?.(`Language guard: deterministic repair timed out after ${error.timeoutMs}ms.`);
188
+ return null;
189
+ }
190
+ params.onLog?.(`Language guard: deterministic repair failed (${error instanceof Error ? error.message : String(error)}).`);
191
+ return null;
192
+ });
193
+ if (repair?.updates.length) {
194
+ selectorUpdates.push(...repair.updates);
195
+ }
196
+ observation = await observeLanguageState(params.browser, requestedLang, {
197
+ onLog: params.onLog,
198
+ phase: "post-repair verification",
199
+ requestedTheme: params.requestedTheme,
200
+ });
201
+ if (isObservationReady(observation)) {
202
+ observation = await rebaseIfNeeded(observation);
203
+ observation = await confirmStableLanguageState({
204
+ browser: params.browser,
205
+ requestedLang,
206
+ requestedTheme: params.requestedTheme,
207
+ observation,
208
+ onLog: params.onLog,
209
+ phase: "post-deterministic-repair",
210
+ });
211
+ }
212
+ if (isObservationReady(observation)) {
213
+ params.onLog?.(`Language guard: fixed UI variant switched successfully after deterministic repair; ${summarizeVariantObservation(observation)}.`);
214
+ return {
215
+ ok: true,
216
+ resolvedBy: "deterministic_repair",
217
+ observation,
218
+ usage,
219
+ selectorUpdates,
220
+ rebasedToStartUrl,
221
+ };
222
+ }
223
+ if (!params.runLanguageSwitchAgent) {
224
+ const reason = buildFailureReason(requestedLang, params.requestedTheme, observation);
225
+ params.onLog?.(`Language guard: blocking variant before main workflow. ${reason}`);
226
+ return {
227
+ ok: false,
228
+ resolvedBy: "blocked",
229
+ observation,
230
+ usage,
231
+ selectorUpdates,
232
+ rebasedToStartUrl,
233
+ reason,
234
+ };
235
+ }
236
+ params.onLog?.(`Language guard: deterministic repair was insufficient; launching dedicated variant preflight. ${summarizeVariantObservation(observation)}.`);
237
+ const preflightAgentResult = await params.runLanguageSwitchAgent({
238
+ languageState: observation.languageState,
239
+ themeState: observation.themeState,
240
+ }).catch(() => null);
241
+ if (preflightAgentResult?.usage?.length) {
242
+ usage.push(...preflightAgentResult.usage);
243
+ }
244
+ if (preflightAgentResult?.actions?.length) {
245
+ selectorUpdates.push(...extractSelectorUpdates(preflightAgentResult.actions));
246
+ }
247
+ observation = await observeLanguageState(params.browser, requestedLang, {
248
+ onLog: params.onLog,
249
+ phase: "post-variant-switch verification",
250
+ requestedTheme: params.requestedTheme,
251
+ });
252
+ if (isObservationReady(observation)) {
253
+ observation = await rebaseIfNeeded(observation);
254
+ observation = await confirmStableLanguageState({
255
+ browser: params.browser,
256
+ requestedLang,
257
+ requestedTheme: params.requestedTheme,
258
+ observation,
259
+ onLog: params.onLog,
260
+ phase: "post-variant-switch",
261
+ });
262
+ }
263
+ if (isObservationReady(observation)) {
264
+ params.onLog?.(`Language guard: fixed UI variant switched successfully after dedicated variant preflight. ${summarizeVariantObservation(observation)}.`);
265
+ return {
266
+ ok: true,
267
+ resolvedBy: "language_preflight_agent",
268
+ observation,
269
+ usage,
270
+ selectorUpdates,
271
+ rebasedToStartUrl,
272
+ };
273
+ }
274
+ const reason = buildFailureReason(requestedLang, params.requestedTheme, observation);
275
+ params.onLog?.(`Language guard: blocking variant before main workflow. ${reason}`);
276
+ return {
277
+ ok: false,
278
+ resolvedBy: "blocked",
279
+ observation,
280
+ usage,
281
+ selectorUpdates,
282
+ rebasedToStartUrl,
283
+ reason,
284
+ };
285
+ }
286
+ //# sourceMappingURL=capture-language-preflight.js.map
@@ -0,0 +1,15 @@
1
+ import type { CapturePageIdentity, StepUsage } from "./types.js";
2
+ import type { ScreenshotPageRunInput } from "./capture-run-optimizer.js";
3
+ export interface PageIdentityInferenceResult {
4
+ identities: Record<string, CapturePageIdentity>;
5
+ usage: StepUsage | null;
6
+ }
7
+ /**
8
+ * Use a cheap LLM call to classify all pages in a capture run.
9
+ * Falls back to regex heuristics if the LLM call fails.
10
+ */
11
+ export declare function inferPageIdentitiesWithLLM(pageRuns: ScreenshotPageRunInput[], model: string, apiKey: string, providerPrefs?: Record<string, {
12
+ order?: string[];
13
+ require?: string[];
14
+ disallow?: string[];
15
+ }>): Promise<PageIdentityInferenceResult>;