autokap 1.0.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.
- package/assets/chrome/ios-statusbar-comparison-reference.jpg +0 -0
- package/assets/chrome/ios-statusbar-dark-reference.jpg +0 -0
- package/assets/chrome/ios-statusbar-light-reference.jpg +0 -0
- package/assets/devices/ipad-pro-11-m4.json +52 -0
- package/assets/devices/iphone-16-pro.json +53 -0
- package/assets/devices/macbook-air-13.json +45 -0
- package/assets/frames/MacBook Air 13.svg +242 -0
- package/assets/frames/Status bar - iPhone.png +0 -0
- Menu bar- iPad.png +0 -0
- package/assets/frames/iPad Pro M4 11_.png +0 -0
- package/assets/frames/iPhone 16 Pro.png +0 -0
- package/assets/icons/Cellular Connection.svg +3 -0
- package/assets/icons/Union.svg +6 -0
- package/assets/icons/Wifi.svg +3 -0
- package/assets/icons/battery.svg +5 -0
- package/assets/icons/battery_charging.svg +8 -0
- package/assets/skill/SKILL.md +575 -0
- package/dist/abort.d.ts +5 -0
- package/dist/abort.js +44 -0
- package/dist/agent.d.ts +142 -0
- package/dist/agent.js +4504 -0
- package/dist/browser-bar.d.ts +40 -0
- package/dist/browser-bar.js +147 -0
- package/dist/browser-pool.d.ts +34 -0
- package/dist/browser-pool.js +122 -0
- package/dist/browser.d.ts +279 -0
- package/dist/browser.js +2902 -0
- package/dist/cli-utils.d.ts +25 -0
- package/dist/cli-utils.js +80 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +365 -0
- package/dist/clip-orchestrator.d.ts +148 -0
- package/dist/clip-orchestrator.js +950 -0
- package/dist/clip-postprocess.d.ts +42 -0
- package/dist/clip-postprocess.js +192 -0
- package/dist/cookie-dismiss.d.ts +5 -0
- package/dist/cookie-dismiss.js +172 -0
- package/dist/credential-templates.d.ts +5 -0
- package/dist/credential-templates.js +60 -0
- package/dist/element-capture.d.ts +53 -0
- package/dist/element-capture.js +766 -0
- package/dist/hybrid-navigator.d.ts +138 -0
- package/dist/hybrid-navigator.js +468 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +11 -0
- package/dist/llm-usage.d.ts +17 -0
- package/dist/llm-usage.js +45 -0
- package/dist/logger.d.ts +46 -0
- package/dist/logger.js +79 -0
- package/dist/mockup-html.d.ts +119 -0
- package/dist/mockup-html.js +253 -0
- package/dist/mockup.d.ts +94 -0
- package/dist/mockup.js +604 -0
- package/dist/mouse-animation.d.ts +46 -0
- package/dist/mouse-animation.js +100 -0
- package/dist/overlay-utils.d.ts +14 -0
- package/dist/overlay-utils.js +13 -0
- package/dist/posthog.d.ts +4 -0
- package/dist/posthog.js +26 -0
- package/dist/prompt-cache.d.ts +10 -0
- package/dist/prompt-cache.js +24 -0
- package/dist/prompts.d.ts +167 -0
- package/dist/prompts.js +1165 -0
- package/dist/security.d.ts +20 -0
- package/dist/security.js +569 -0
- package/dist/session-profile.d.ts +86 -0
- package/dist/session-profile.js +1471 -0
- package/dist/sf-pro-fonts.d.ts +4 -0
- package/dist/sf-pro-fonts.js +7 -0
- package/dist/status-bar-l10n.d.ts +14 -0
- package/dist/status-bar-l10n.js +177 -0
- package/dist/status-bar.d.ts +44 -0
- package/dist/status-bar.js +336 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +578 -0
- package/dist/types.d.ts +796 -0
- package/dist/types.js +2 -0
- package/dist/video-agent.d.ts +143 -0
- package/dist/video-agent.js +4783 -0
- package/dist/video-observation.d.ts +36 -0
- package/dist/video-observation.js +192 -0
- package/dist/video-planner.d.ts +12 -0
- package/dist/video-planner.js +500 -0
- package/dist/video-prompts.d.ts +37 -0
- package/dist/video-prompts.js +554 -0
- package/dist/video-tools.d.ts +3 -0
- package/dist/video-tools.js +59 -0
- package/dist/video-variant-state.d.ts +29 -0
- package/dist/video-variant-state.js +80 -0
- package/dist/vision-model.d.ts +17 -0
- package/dist/vision-model.js +74 -0
- package/package.json +165 -0
- package/readme.md +61 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ClipOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the system ffmpeg binary path. Throws if not found.
|
|
4
|
+
*/
|
|
5
|
+
export declare function ensureFfmpegAvailable(): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Convert a WebM recording to an optimized GIF using a 2-pass palette approach.
|
|
8
|
+
* Pass 1: generate an optimal palette. Pass 2: encode with that palette.
|
|
9
|
+
*/
|
|
10
|
+
export declare function convertToGif(webmPath: string, outputPath: string, opts?: {
|
|
11
|
+
fps?: number;
|
|
12
|
+
maxWidth?: number;
|
|
13
|
+
loop?: boolean;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Convert a WebM recording to an MP4 with web-optimized settings.
|
|
17
|
+
*/
|
|
18
|
+
export declare function convertToMp4(webmPath: string, outputPath: string): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Extract the first frame of a WebM as a PNG thumbnail.
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractThumbnail(webmPath: string, outputPath: string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Trim a recording: skip dead frames at the start and cap duration.
|
|
25
|
+
*/
|
|
26
|
+
export declare function trimRecording(inputPath: string, outputPath: string, startSec?: number, maxDurationSec?: number): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Get the duration of a media file in milliseconds.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getMediaDurationMs(filePath: string): Promise<number>;
|
|
31
|
+
export interface ClipPostProcessResult {
|
|
32
|
+
gifPath?: string;
|
|
33
|
+
mp4Path?: string;
|
|
34
|
+
thumbnailPath?: string;
|
|
35
|
+
durationMs: number;
|
|
36
|
+
fileSizeBytes?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Full post-processing pipeline for a single clip recording:
|
|
40
|
+
* trim → GIF and/or MP4 → thumbnail → measure.
|
|
41
|
+
*/
|
|
42
|
+
export declare function postProcessClipRecording(webmPath: string, outputDir: string, clipId: string, options?: ClipOptions): Promise<ClipPostProcessResult>;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
// ── Default values ──────────────────────────────────────────────────
|
|
7
|
+
const DEFAULT_GIF_FPS = 15;
|
|
8
|
+
const DEFAULT_GIF_MAX_WIDTH = 800;
|
|
9
|
+
const DEFAULT_MAX_DURATION_SEC = 8;
|
|
10
|
+
const DEFAULT_TRIM_START_SEC = 0.3;
|
|
11
|
+
// ── ffmpeg detection ────────────────────────────────────────────────
|
|
12
|
+
let cachedFfmpegPath = null;
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the system ffmpeg binary path. Throws if not found.
|
|
15
|
+
*/
|
|
16
|
+
export async function ensureFfmpegAvailable() {
|
|
17
|
+
if (cachedFfmpegPath)
|
|
18
|
+
return cachedFfmpegPath;
|
|
19
|
+
try {
|
|
20
|
+
const { stdout } = await execFileAsync('which', ['ffmpeg']);
|
|
21
|
+
const ffmpegPath = stdout.trim();
|
|
22
|
+
if (!ffmpegPath)
|
|
23
|
+
throw new Error('ffmpeg not found');
|
|
24
|
+
cachedFfmpegPath = ffmpegPath;
|
|
25
|
+
return ffmpegPath;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error('ffmpeg is required for clip post-processing but was not found on your system. ' +
|
|
29
|
+
'Install it via: brew install ffmpeg (macOS) or apt install ffmpeg (Linux).');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ── Conversion functions ────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Convert a WebM recording to an optimized GIF using a 2-pass palette approach.
|
|
35
|
+
* Pass 1: generate an optimal palette. Pass 2: encode with that palette.
|
|
36
|
+
*/
|
|
37
|
+
export async function convertToGif(webmPath, outputPath, opts = {}) {
|
|
38
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
39
|
+
const fps = opts.fps ?? DEFAULT_GIF_FPS;
|
|
40
|
+
const maxWidth = opts.maxWidth ?? DEFAULT_GIF_MAX_WIDTH;
|
|
41
|
+
const loopFlag = (opts.loop ?? true) ? '0' : '-1';
|
|
42
|
+
const paletteDir = path.dirname(outputPath);
|
|
43
|
+
const palettePath = path.join(paletteDir, `_palette_${Date.now()}.png`);
|
|
44
|
+
const scaleFilter = `fps=${fps},scale=${maxWidth}:-1:flags=lanczos`;
|
|
45
|
+
// Pass 1: Generate palette
|
|
46
|
+
await execFileAsync(ffmpeg, [
|
|
47
|
+
'-i', webmPath,
|
|
48
|
+
'-vf', `${scaleFilter},palettegen=stats_mode=diff`,
|
|
49
|
+
'-y', palettePath,
|
|
50
|
+
]);
|
|
51
|
+
// Pass 2: Encode GIF with palette
|
|
52
|
+
try {
|
|
53
|
+
await execFileAsync(ffmpeg, [
|
|
54
|
+
'-i', webmPath,
|
|
55
|
+
'-i', palettePath,
|
|
56
|
+
'-lavfi', `${scaleFilter}[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=3`,
|
|
57
|
+
'-loop', loopFlag,
|
|
58
|
+
'-y', outputPath,
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
// Clean up palette file
|
|
63
|
+
try {
|
|
64
|
+
await stat(palettePath).then(() => execFileAsync('rm', [palettePath]));
|
|
65
|
+
}
|
|
66
|
+
catch { /* ignore */ }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Convert a WebM recording to an MP4 with web-optimized settings.
|
|
71
|
+
*/
|
|
72
|
+
export async function convertToMp4(webmPath, outputPath) {
|
|
73
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
74
|
+
await execFileAsync(ffmpeg, [
|
|
75
|
+
'-i', webmPath,
|
|
76
|
+
'-c:v', 'libx264',
|
|
77
|
+
'-preset', 'slow',
|
|
78
|
+
'-crf', '22',
|
|
79
|
+
'-pix_fmt', 'yuv420p',
|
|
80
|
+
'-movflags', '+faststart',
|
|
81
|
+
'-an',
|
|
82
|
+
'-y', outputPath,
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract the first frame of a WebM as a PNG thumbnail.
|
|
87
|
+
*/
|
|
88
|
+
export async function extractThumbnail(webmPath, outputPath) {
|
|
89
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
90
|
+
await execFileAsync(ffmpeg, [
|
|
91
|
+
'-i', webmPath,
|
|
92
|
+
'-frames:v', '1',
|
|
93
|
+
'-y', outputPath,
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Trim a recording: skip dead frames at the start and cap duration.
|
|
98
|
+
*/
|
|
99
|
+
export async function trimRecording(inputPath, outputPath, startSec = DEFAULT_TRIM_START_SEC, maxDurationSec = DEFAULT_MAX_DURATION_SEC) {
|
|
100
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
101
|
+
await execFileAsync(ffmpeg, [
|
|
102
|
+
'-ss', String(startSec),
|
|
103
|
+
'-i', inputPath,
|
|
104
|
+
'-t', String(maxDurationSec),
|
|
105
|
+
'-c', 'copy',
|
|
106
|
+
'-y', outputPath,
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get the duration of a media file in milliseconds.
|
|
111
|
+
*/
|
|
112
|
+
export async function getMediaDurationMs(filePath) {
|
|
113
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
114
|
+
const ffprobe = ffmpeg.replace(/ffmpeg$/, 'ffprobe');
|
|
115
|
+
const { stdout } = await execFileAsync(ffprobe, [
|
|
116
|
+
'-v', 'error',
|
|
117
|
+
'-show_entries', 'format=duration',
|
|
118
|
+
'-of', 'default=noprint_wrappers=1:nokey=1',
|
|
119
|
+
filePath,
|
|
120
|
+
]);
|
|
121
|
+
return Math.round(parseFloat(stdout.trim()) * 1000);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Freeze the last frame of a video for `durationSec` additional seconds.
|
|
125
|
+
* Uses ffmpeg's tpad filter to hold the final frame in place.
|
|
126
|
+
*/
|
|
127
|
+
async function freezeLastFrame(inputPath, outputPath, durationSec) {
|
|
128
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
129
|
+
const stopDurationMs = Math.round(durationSec * 1000);
|
|
130
|
+
await execFileAsync(ffmpeg, [
|
|
131
|
+
'-i', inputPath,
|
|
132
|
+
'-vf', `tpad=stop_mode=clone:stop_duration=${stopDurationMs}ms`,
|
|
133
|
+
'-c:v', 'libvpx-vp9',
|
|
134
|
+
'-crf', '30',
|
|
135
|
+
'-b:v', '0',
|
|
136
|
+
'-an',
|
|
137
|
+
'-y', outputPath,
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Full post-processing pipeline for a single clip recording:
|
|
142
|
+
* trim → GIF and/or MP4 → thumbnail → measure.
|
|
143
|
+
*/
|
|
144
|
+
export async function postProcessClipRecording(webmPath, outputDir, clipId, options = {}) {
|
|
145
|
+
const maxDuration = options.maxDurationSec ?? DEFAULT_MAX_DURATION_SEC;
|
|
146
|
+
// Step 1: Trim the recording — cut the setup phase (page load, overlay dismiss)
|
|
147
|
+
const trimStart = options.trimStartSec ?? DEFAULT_TRIM_START_SEC;
|
|
148
|
+
const trimmedPath = path.join(outputDir, `${clipId}_trimmed.webm`);
|
|
149
|
+
await trimRecording(webmPath, trimmedPath, trimStart, maxDuration);
|
|
150
|
+
// Step 1b: Optionally freeze the last frame for a pause before looping
|
|
151
|
+
const holdSec = Math.min(Math.max(options.holdLastFrameSec ?? 0, 0), 10);
|
|
152
|
+
let sourcePath = trimmedPath;
|
|
153
|
+
if (holdSec > 0) {
|
|
154
|
+
const paddedPath = path.join(outputDir, `${clipId}_padded.webm`);
|
|
155
|
+
await freezeLastFrame(trimmedPath, paddedPath, holdSec);
|
|
156
|
+
sourcePath = paddedPath;
|
|
157
|
+
}
|
|
158
|
+
const durationMs = await getMediaDurationMs(sourcePath);
|
|
159
|
+
const result = { durationMs };
|
|
160
|
+
// Step 2: Always persist both a GIF and an MP4.
|
|
161
|
+
// The GIF remains useful for delivery and embeds, while the MP4 is the
|
|
162
|
+
// high-fidelity source for Studio playback and downstream re-renders.
|
|
163
|
+
const gifPath = path.join(outputDir, `${clipId}.gif`);
|
|
164
|
+
await convertToGif(sourcePath, gifPath, {
|
|
165
|
+
fps: options.gifFps,
|
|
166
|
+
maxWidth: options.gifMaxWidth,
|
|
167
|
+
loop: options.loop,
|
|
168
|
+
});
|
|
169
|
+
result.gifPath = gifPath;
|
|
170
|
+
const gifStat = await stat(gifPath);
|
|
171
|
+
result.fileSizeBytes = gifStat.size;
|
|
172
|
+
const mp4Path = path.join(outputDir, `${clipId}.mp4`);
|
|
173
|
+
await convertToMp4(sourcePath, mp4Path);
|
|
174
|
+
result.mp4Path = mp4Path;
|
|
175
|
+
// Step 3: Extract thumbnail
|
|
176
|
+
const thumbnailPath = path.join(outputDir, `${clipId}_thumb.png`);
|
|
177
|
+
await extractThumbnail(sourcePath, thumbnailPath);
|
|
178
|
+
result.thumbnailPath = thumbnailPath;
|
|
179
|
+
// Clean up intermediate files
|
|
180
|
+
try {
|
|
181
|
+
await execFileAsync('rm', [trimmedPath]);
|
|
182
|
+
}
|
|
183
|
+
catch { /* ignore */ }
|
|
184
|
+
if (sourcePath !== trimmedPath) {
|
|
185
|
+
try {
|
|
186
|
+
await execFileAsync('rm', [sourcePath]);
|
|
187
|
+
}
|
|
188
|
+
catch { /* ignore */ }
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=clip-postprocess.js.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
// Known CMP (Consent Management Platform) accept buttons
|
|
3
|
+
const CMP_SELECTORS = [
|
|
4
|
+
// OneTrust
|
|
5
|
+
'#onetrust-accept-btn-handler',
|
|
6
|
+
'.onetrust-close-btn-handler',
|
|
7
|
+
// Cookiebot
|
|
8
|
+
'#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
|
|
9
|
+
'#CybotCookiebotDialogBodyButtonAccept',
|
|
10
|
+
// Quantcast
|
|
11
|
+
'.qc-cmp2-summary-buttons button[mode="primary"]',
|
|
12
|
+
// Didomi
|
|
13
|
+
'#didomi-notice-agree-button',
|
|
14
|
+
// Axeptio
|
|
15
|
+
'.axeptio_btn_acceptAll',
|
|
16
|
+
// Iubenda
|
|
17
|
+
'.iubenda-cs-accept-btn',
|
|
18
|
+
// HubSpot
|
|
19
|
+
'#hs-eu-confirmation-button',
|
|
20
|
+
// Cookie Notice plugin
|
|
21
|
+
'.cookie-notice-container .cn-set-cookie',
|
|
22
|
+
// Cookie Consent (Osano)
|
|
23
|
+
'.cc-compliance .cc-btn.cc-allow',
|
|
24
|
+
'.cc-btn.cc-dismiss',
|
|
25
|
+
// Klaro
|
|
26
|
+
'.klaro .cm-btn-accept',
|
|
27
|
+
// Complianz
|
|
28
|
+
'#cmplz-cookiebanner-container .cmplz-btn.cmplz-accept',
|
|
29
|
+
// Generic patterns
|
|
30
|
+
'[data-cookiefirst-action="accept"]',
|
|
31
|
+
'[data-testid="cookie-accept"]',
|
|
32
|
+
'[data-testid="accept-cookies"]',
|
|
33
|
+
];
|
|
34
|
+
// Multilingual accept button text patterns
|
|
35
|
+
const ACCEPT_PATTERNS = [
|
|
36
|
+
/^accept\s*(all|cookies)?$/i,
|
|
37
|
+
/^i\s*agree$/i,
|
|
38
|
+
/^agree(\s*(&|and)\s*close)?$/i,
|
|
39
|
+
/^allow\s*(all|cookies)?$/i,
|
|
40
|
+
/^got\s*it$/i,
|
|
41
|
+
/^ok$/i,
|
|
42
|
+
/^continue$/i,
|
|
43
|
+
// French
|
|
44
|
+
/^accepter\s*(tout|les cookies)?$/i,
|
|
45
|
+
/^tout\s*accepter$/i,
|
|
46
|
+
/^j.accepte$/i,
|
|
47
|
+
/^continuer$/i,
|
|
48
|
+
// German
|
|
49
|
+
/^(alle\s*)?akzeptieren$/i,
|
|
50
|
+
/^alle\s*zulassen$/i,
|
|
51
|
+
/^zustimmen$/i,
|
|
52
|
+
// Spanish
|
|
53
|
+
/^aceptar\s*(todo|cookies)?$/i,
|
|
54
|
+
// Portuguese
|
|
55
|
+
/^aceitar\s*(tudo|cookies)?$/i,
|
|
56
|
+
// Italian
|
|
57
|
+
/^accetta\s*(tutto|i cookie)?$/i,
|
|
58
|
+
// Dutch
|
|
59
|
+
/^(alles\s*)?accepteren$/i,
|
|
60
|
+
];
|
|
61
|
+
// Elements to always hide via CSS injection
|
|
62
|
+
const HIDE_SELECTORS = [
|
|
63
|
+
// Chat widgets
|
|
64
|
+
'#intercom-container', '.intercom-lightweight-app', '#intercom-frame',
|
|
65
|
+
'#drift-widget', '#drift-frame-controller',
|
|
66
|
+
'.crisp-client', '#crisp-chatbox',
|
|
67
|
+
'#launcher', '#webWidget', // Zendesk
|
|
68
|
+
'#hubspot-messages-iframe-container',
|
|
69
|
+
'#tidio-chat', '#tidio-chat-iframe',
|
|
70
|
+
'.fb-customerchat', '.fb_dialog', // Facebook Messenger
|
|
71
|
+
'#chat-widget-container',
|
|
72
|
+
// Feedback widgets
|
|
73
|
+
'#_hj_feedback_container', '._hj-widget-container', // Hotjar
|
|
74
|
+
'#usersnap-button', '#usersnap-panel',
|
|
75
|
+
// Generic
|
|
76
|
+
'[class*="chat-widget"]', '[class*="chatWidget"]',
|
|
77
|
+
'[id*="chat-widget"]', '[id*="chatWidget"]',
|
|
78
|
+
// Cookie banners (CSS fallback)
|
|
79
|
+
'[class*="cookie-banner"]', '[class*="cookie-consent"]',
|
|
80
|
+
'[id*="cookie-banner"]', '[id*="cookie-consent"]',
|
|
81
|
+
'[class*="cookieBanner"]', '[class*="cookieConsent"]',
|
|
82
|
+
'[id*="cookieBanner"]', '[id*="cookieConsent"]',
|
|
83
|
+
'[class*="gdpr"]', '[id*="gdpr"]',
|
|
84
|
+
];
|
|
85
|
+
export async function dismissCookiesAndWidgets(page) {
|
|
86
|
+
// Strategy 1: Click known CMP buttons
|
|
87
|
+
for (const selector of CMP_SELECTORS) {
|
|
88
|
+
try {
|
|
89
|
+
const button = page.locator(selector).first();
|
|
90
|
+
if (await button.isVisible({ timeout: 500 })) {
|
|
91
|
+
await button.click({ timeout: 2000 });
|
|
92
|
+
logger.info(`Cookie dismissed via CMP selector: ${selector}`);
|
|
93
|
+
await page.waitForTimeout(500);
|
|
94
|
+
await injectHideCSS(page);
|
|
95
|
+
return { dismissed: true, method: `cmp:${selector}` };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Selector not found or not clickable, try next
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Strategy 2: Find accept buttons by text in cookie-like containers
|
|
103
|
+
try {
|
|
104
|
+
const dismissed = await page.evaluate((patterns) => {
|
|
105
|
+
const regexPatterns = patterns.map(p => {
|
|
106
|
+
const match = p.match(/^\/(.*)\/([gimsuy]*)$/);
|
|
107
|
+
return match ? new RegExp(match[1], match[2]) : new RegExp(p, 'i');
|
|
108
|
+
});
|
|
109
|
+
// Find all visible buttons and links
|
|
110
|
+
const candidates = document.querySelectorAll('button, a, [role="button"], input[type="submit"]');
|
|
111
|
+
for (const el of candidates) {
|
|
112
|
+
const htmlEl = el;
|
|
113
|
+
const text = (htmlEl.textContent || htmlEl.getAttribute('value') || '').trim();
|
|
114
|
+
if (!text || text.length > 40)
|
|
115
|
+
continue;
|
|
116
|
+
// Check if text matches an accept pattern
|
|
117
|
+
const matches = regexPatterns.some(rx => rx.test(text));
|
|
118
|
+
if (!matches)
|
|
119
|
+
continue;
|
|
120
|
+
// Check if it's inside a cookie/consent container.
|
|
121
|
+
// Only match containers that explicitly identify as cookie/consent/GDPR
|
|
122
|
+
// via ID, class, role, or aria-label. Do NOT match on position:fixed/sticky
|
|
123
|
+
// alone — that's too broad and can match unrelated sticky CTAs.
|
|
124
|
+
let parent = htmlEl;
|
|
125
|
+
let isCookieContainer = false;
|
|
126
|
+
for (let depth = 0; depth < 8 && parent; depth++) {
|
|
127
|
+
const id = (parent.id || '').toLowerCase();
|
|
128
|
+
const className = (parent.className || '').toString().toLowerCase();
|
|
129
|
+
const role = (parent.getAttribute('role') || '').toLowerCase();
|
|
130
|
+
const ariaLabel = (parent.getAttribute('aria-label') || '').toLowerCase();
|
|
131
|
+
const combinedAttrs = `${id} ${className} ${ariaLabel}`;
|
|
132
|
+
if (combinedAttrs.includes('cookie') || combinedAttrs.includes('consent') ||
|
|
133
|
+
combinedAttrs.includes('gdpr') || combinedAttrs.includes('privacy') ||
|
|
134
|
+
combinedAttrs.includes('banner') ||
|
|
135
|
+
((role === 'dialog' || role === 'alertdialog') && combinedAttrs.includes('co'))) {
|
|
136
|
+
isCookieContainer = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
parent = parent.parentElement;
|
|
140
|
+
}
|
|
141
|
+
if (isCookieContainer) {
|
|
142
|
+
htmlEl.click();
|
|
143
|
+
return text;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}, ACCEPT_PATTERNS.map(rx => rx.toString()));
|
|
148
|
+
if (dismissed) {
|
|
149
|
+
logger.info(`Cookie dismissed via text match: "${dismissed}"`);
|
|
150
|
+
await page.waitForTimeout(500);
|
|
151
|
+
await injectHideCSS(page);
|
|
152
|
+
return { dismissed: true, method: `text:${dismissed}` };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Text-based search failed
|
|
157
|
+
}
|
|
158
|
+
// Strategy 3: Always inject CSS to hide known overlays and widgets
|
|
159
|
+
await injectHideCSS(page);
|
|
160
|
+
return { dismissed: false, method: null };
|
|
161
|
+
}
|
|
162
|
+
async function injectHideCSS(page) {
|
|
163
|
+
try {
|
|
164
|
+
await page.addStyleTag({
|
|
165
|
+
content: HIDE_SELECTORS.map(s => `${s} { display: none !important; visibility: hidden !important; pointer-events: none !important; }`).join('\n'),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Page might have navigated away
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=cookie-dismiss.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LoginCredentials } from "./types.js";
|
|
2
|
+
export declare function resolveCredentialTemplates(value: string | undefined, credentials?: LoginCredentials): string | undefined;
|
|
3
|
+
export declare function ensureNoCredentialTemplate(field: string, value: string | undefined): void;
|
|
4
|
+
export declare function sanitizeCredentialParams(params: Record<string, unknown>, credentials?: LoginCredentials): Record<string, unknown>;
|
|
5
|
+
export declare function resolveActionCredentialArgs(action: string, args: Record<string, unknown>, credentials?: LoginCredentials): Record<string, unknown>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const TEMPLATE_RE = /\{\{credential\.(loginUrl|email|password)\}\}/g;
|
|
2
|
+
export function resolveCredentialTemplates(value, credentials) {
|
|
3
|
+
if (!value)
|
|
4
|
+
return value;
|
|
5
|
+
const replacements = {
|
|
6
|
+
loginUrl: credentials?.loginUrl,
|
|
7
|
+
email: credentials?.email,
|
|
8
|
+
password: credentials?.password,
|
|
9
|
+
};
|
|
10
|
+
return value.replace(TEMPLATE_RE, (match, key) => {
|
|
11
|
+
const replacement = replacements[key];
|
|
12
|
+
return typeof replacement === "string" ? replacement : match;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function ensureNoCredentialTemplate(field, value) {
|
|
16
|
+
if (value && TEMPLATE_RE.test(value)) {
|
|
17
|
+
TEMPLATE_RE.lastIndex = 0;
|
|
18
|
+
throw new Error(`Missing credential value for ${field}`);
|
|
19
|
+
}
|
|
20
|
+
TEMPLATE_RE.lastIndex = 0;
|
|
21
|
+
}
|
|
22
|
+
function sanitizeCredentialString(value, credentials) {
|
|
23
|
+
if (!credentials)
|
|
24
|
+
return value;
|
|
25
|
+
if (credentials.password && value === credentials.password) {
|
|
26
|
+
return "{{credential.password}}";
|
|
27
|
+
}
|
|
28
|
+
if (credentials.email && value === credentials.email) {
|
|
29
|
+
return "{{credential.email}}";
|
|
30
|
+
}
|
|
31
|
+
if (credentials.loginUrl && value === credentials.loginUrl) {
|
|
32
|
+
return "{{credential.loginUrl}}";
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export function sanitizeCredentialParams(params, credentials) {
|
|
37
|
+
const sanitized = {};
|
|
38
|
+
for (const [key, value] of Object.entries(params)) {
|
|
39
|
+
sanitized[key] =
|
|
40
|
+
typeof value === "string"
|
|
41
|
+
? sanitizeCredentialString(value, credentials)
|
|
42
|
+
: value;
|
|
43
|
+
}
|
|
44
|
+
return sanitized;
|
|
45
|
+
}
|
|
46
|
+
export function resolveActionCredentialArgs(action, args, credentials) {
|
|
47
|
+
if (!credentials)
|
|
48
|
+
return args;
|
|
49
|
+
const resolved = { ...args };
|
|
50
|
+
if (action === "type_text" && typeof resolved.text === "string") {
|
|
51
|
+
resolved.text = resolveCredentialTemplates(resolved.text, credentials);
|
|
52
|
+
ensureNoCredentialTemplate("type_text.text", resolved.text);
|
|
53
|
+
}
|
|
54
|
+
if (action === "navigate_to" && typeof resolved.url === "string") {
|
|
55
|
+
resolved.url = resolveCredentialTemplates(resolved.url, credentials);
|
|
56
|
+
ensureNoCredentialTemplate("navigate_to.url", resolved.url);
|
|
57
|
+
}
|
|
58
|
+
return resolved;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=credential-templates.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Browser } from './browser.js';
|
|
2
|
+
import type { IsolatedElement, ElementCaptureResult, OutscaleConfig, SelectorValidationResult } from './types.js';
|
|
3
|
+
interface SearchQueryHistoryEntry {
|
|
4
|
+
candidateLines: string[];
|
|
5
|
+
domSignature: string;
|
|
6
|
+
selectors: string[];
|
|
7
|
+
hasTransientSelectors: boolean;
|
|
8
|
+
/** Best candidate bounding box for coordinate-based fallback capture. */
|
|
9
|
+
bestRegion?: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
} | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function isLooseElementCaptureRejectionReason(reason: string | null | undefined): boolean;
|
|
17
|
+
export declare function isTagOnlyStructuralSelector(selector: string): boolean;
|
|
18
|
+
export declare function shouldBlockUngroundedStructuralSelector(params: {
|
|
19
|
+
selector: string;
|
|
20
|
+
groundedSelectors: Iterable<string>;
|
|
21
|
+
verifierRejectedAsTooLoose: boolean;
|
|
22
|
+
}): boolean;
|
|
23
|
+
export declare function outscaleAddsPadding(outscale: OutscaleConfig | undefined): boolean;
|
|
24
|
+
export declare function buildVerificationOutscale(outscale: OutscaleConfig | undefined): OutscaleConfig;
|
|
25
|
+
export declare function shouldAcceptDomCorroboratedSelector(params: {
|
|
26
|
+
looseFailureCount: number;
|
|
27
|
+
verifierRejectedAsTooLoose: boolean;
|
|
28
|
+
validation: SelectorValidationResult;
|
|
29
|
+
viewport: {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
} | null;
|
|
33
|
+
observedAsInteractive: boolean;
|
|
34
|
+
directQueryCount: number;
|
|
35
|
+
containerQueryCount: number;
|
|
36
|
+
}): boolean;
|
|
37
|
+
export declare function computeElementCaptureDomSignature(params: {
|
|
38
|
+
currentUrl: string;
|
|
39
|
+
interactiveElements: Array<Pick<Awaited<ReturnType<Browser['getInteractiveElements']>>[number], 'index' | 'tag' | 'role' | 'text' | 'selector' | 'visibilityState'>>;
|
|
40
|
+
}): string;
|
|
41
|
+
export declare function shouldAllowSearchRefresh(params: {
|
|
42
|
+
cached: SearchQueryHistoryEntry | undefined;
|
|
43
|
+
domSignature: string;
|
|
44
|
+
lastFailedTransientSelector: string | null;
|
|
45
|
+
}): boolean;
|
|
46
|
+
interface ElementCaptureOptions {
|
|
47
|
+
abortSignal?: AbortSignal;
|
|
48
|
+
distinctId?: string;
|
|
49
|
+
fallbackModel?: string;
|
|
50
|
+
uploadImage?: (buffer: Buffer, mimeType: 'image/jpeg' | 'image/png') => Promise<string>;
|
|
51
|
+
}
|
|
52
|
+
export declare function captureIsolatedElement(browser: Browser, element: IsolatedElement, apiKey: string, model: string, options?: ElementCaptureOptions): Promise<ElementCaptureResult>;
|
|
53
|
+
export {};
|