autokap 1.0.6 → 1.0.8
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/cursors/macos.svg +4 -0
- package/assets/cursors/windows.svg +15 -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/OPCODE-REFERENCE.md +607 -0
- package/assets/skill/README.md +39 -0
- package/assets/skill/SKILL.md +453 -468
- package/assets/skill/STUDIO-SKILL.md +476 -0
- package/assets/skill/references/examples.md +104 -0
- package/assets/skill/references/interactive-demo.md +225 -0
- package/assets/skill/references/mock-data.md +178 -0
- package/dist/abort.d.ts +5 -0
- package/dist/abort.js +44 -0
- package/dist/action-verifier.d.ts +29 -0
- package/dist/action-verifier.js +133 -0
- package/dist/agent-action-recovery.d.ts +45 -0
- package/dist/agent-action-recovery.js +370 -0
- package/dist/agent-message-utils.d.ts +21 -0
- package/dist/agent-message-utils.js +77 -0
- package/dist/agent-url-utils.d.ts +30 -0
- package/dist/agent-url-utils.js +138 -0
- package/dist/agent.d.ts +226 -0
- package/dist/agent.js +6666 -0
- package/dist/ak-tree.d.ts +39 -0
- package/dist/ak-tree.js +368 -0
- package/dist/alt-text.d.ts +26 -0
- package/dist/alt-text.js +55 -0
- package/dist/auth-capture.d.ts +17 -0
- package/dist/auth-capture.js +164 -0
- package/dist/benchmark.d.ts +59 -0
- package/dist/benchmark.js +135 -0
- package/dist/billing-operation-logging.d.ts +38 -0
- package/dist/billing-operation-logging.js +248 -0
- package/dist/browser-bar.d.ts +48 -0
- package/dist/browser-bar.js +284 -0
- package/dist/browser-pool.d.ts +7 -0
- package/dist/browser-pool.js +15 -5
- package/dist/browser-utils.d.ts +31 -0
- package/dist/browser-utils.js +97 -0
- package/dist/browser.d.ts +76 -1
- package/dist/browser.js +1657 -39
- package/dist/capture-alt-text.d.ts +12 -0
- package/dist/capture-alt-text.js +52 -0
- package/dist/capture-encryption.d.ts +10 -0
- package/dist/capture-encryption.js +41 -0
- package/dist/capture-language-preflight.d.ts +41 -0
- package/dist/capture-language-preflight.js +300 -0
- package/dist/capture-llm-page-identity.d.ts +15 -0
- package/dist/capture-llm-page-identity.js +128 -0
- package/dist/capture-model-resolution.d.ts +9 -0
- package/dist/capture-model-resolution.js +21 -0
- package/dist/capture-page-identity.d.ts +7 -0
- package/dist/capture-page-identity.js +352 -0
- package/dist/capture-preset-credentials.d.ts +62 -0
- package/dist/capture-preset-credentials.js +184 -0
- package/dist/capture-request-plan.d.ts +58 -0
- package/dist/capture-request-plan.js +264 -0
- package/dist/capture-run-optimizer.d.ts +139 -0
- package/dist/capture-run-optimizer.js +863 -0
- package/dist/capture-selector-memory.d.ts +31 -0
- package/dist/capture-selector-memory.js +345 -0
- package/dist/capture-session-profile-encryption.d.ts +2 -0
- package/dist/capture-session-profile-encryption.js +22 -0
- package/dist/capture-step-timeout.d.ts +10 -0
- package/dist/capture-step-timeout.js +30 -0
- package/dist/capture-strategy.d.ts +36 -0
- package/dist/capture-strategy.js +95 -0
- package/dist/capture-studio-sync.d.ts +23 -0
- package/dist/capture-studio-sync.js +172 -0
- package/dist/capture-surface-contract.d.ts +36 -0
- package/dist/capture-surface-contract.js +299 -0
- package/dist/capture-transition-engine.d.ts +28 -0
- package/dist/capture-transition-engine.js +292 -0
- package/dist/capture-variant-state.d.ts +56 -0
- package/dist/capture-variant-state.js +182 -0
- package/dist/capture-verification.d.ts +35 -0
- package/dist/capture-verification.js +95 -0
- package/dist/capture-viewport-lock.d.ts +48 -0
- package/dist/capture-viewport-lock.js +74 -0
- package/dist/circuit-breaker.d.ts +42 -0
- package/dist/circuit-breaker.js +119 -0
- package/dist/cli-config.d.ts +8 -1
- package/dist/cli-config.js +62 -6
- package/dist/cli-contract.d.ts +15 -0
- package/dist/cli-contract.js +167 -0
- package/dist/cli-runner-local.d.ts +12 -0
- package/dist/cli-runner-local.js +102 -0
- package/dist/cli-runner.d.ts +34 -0
- package/dist/cli-runner.js +433 -0
- package/dist/cli-utils.d.ts +0 -1
- package/dist/cli-utils.js +2 -5
- package/dist/cli.js +1005 -252
- package/dist/clip-orchestrator.d.ts +148 -0
- package/dist/clip-orchestrator.js +957 -0
- package/dist/clip-postprocess.d.ts +42 -0
- package/dist/clip-postprocess.js +201 -0
- package/dist/cookie-dismiss.d.ts +2 -0
- package/dist/cookie-dismiss.js +48 -13
- package/dist/cost-logging.d.ts +35 -0
- package/dist/cost-logging.js +242 -0
- package/dist/cost-resolution-monitor.d.ts +16 -0
- package/dist/cost-resolution-monitor.js +34 -0
- package/dist/credential-templates.d.ts +5 -0
- package/dist/credential-templates.js +60 -0
- package/dist/cursor-overlay-script.d.ts +6 -0
- package/dist/cursor-overlay-script.js +169 -0
- package/dist/dom-css-purger.d.ts +65 -0
- package/dist/dom-css-purger.js +333 -0
- package/dist/dom-font-inliner.d.ts +45 -0
- package/dist/dom-font-inliner.js +148 -0
- package/dist/dom-patch-resolver.d.ts +52 -0
- package/dist/dom-patch-resolver.js +242 -0
- package/dist/dom-serializer.d.ts +82 -0
- package/dist/dom-serializer.js +378 -0
- package/dist/element-capture.d.ts +13 -0
- package/dist/element-capture.js +522 -0
- package/dist/env-validation.d.ts +5 -0
- package/dist/env-validation.js +29 -0
- package/dist/execution-schema.d.ts +4423 -0
- package/dist/execution-schema.js +507 -0
- package/dist/execution-types.d.ts +886 -0
- package/dist/execution-types.js +65 -0
- package/dist/fonts-loader.d.ts +14 -0
- package/dist/fonts-loader.js +55 -0
- package/dist/hybrid-navigator.d.ts +138 -0
- package/dist/hybrid-navigator.js +468 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +17 -0
- package/dist/legacy/agent-action-recovery.d.ts +45 -0
- package/dist/legacy/agent-action-recovery.js +370 -0
- package/dist/legacy/agent-message-utils.d.ts +21 -0
- package/dist/legacy/agent-message-utils.js +77 -0
- package/dist/legacy/agent-url-utils.d.ts +30 -0
- package/dist/legacy/agent-url-utils.js +138 -0
- package/dist/legacy/agent.d.ts +226 -0
- package/dist/legacy/agent.js +6666 -0
- package/dist/legacy/clip-orchestrator.d.ts +148 -0
- package/dist/legacy/clip-orchestrator.js +957 -0
- package/dist/legacy/credential-templates.d.ts +5 -0
- package/dist/legacy/credential-templates.js +60 -0
- package/dist/legacy/hybrid-navigator.d.ts +138 -0
- package/dist/legacy/hybrid-navigator.js +468 -0
- package/dist/legacy/llm-usage.d.ts +17 -0
- package/dist/legacy/llm-usage.js +45 -0
- package/dist/legacy/prompt-cache.d.ts +10 -0
- package/dist/legacy/prompt-cache.js +24 -0
- package/dist/legacy/prompts.d.ts +175 -0
- package/dist/legacy/prompts.js +1038 -0
- package/dist/legacy/tools.d.ts +4 -0
- package/dist/legacy/tools.js +216 -0
- package/dist/legacy/video-agent.d.ts +143 -0
- package/dist/legacy/video-agent.js +4788 -0
- package/dist/legacy/video-observation.d.ts +36 -0
- package/dist/legacy/video-observation.js +192 -0
- package/dist/legacy/video-planner.d.ts +12 -0
- package/dist/legacy/video-planner.js +501 -0
- package/dist/legacy/video-prompts.d.ts +37 -0
- package/dist/legacy/video-prompts.js +569 -0
- package/dist/legacy/video-tools.d.ts +3 -0
- package/dist/legacy/video-tools.js +59 -0
- package/dist/legacy/video-variant-state.d.ts +29 -0
- package/dist/legacy/video-variant-state.js +80 -0
- package/dist/legacy/vision-model.d.ts +17 -0
- package/dist/legacy/vision-model.js +74 -0
- package/dist/llm-healer.d.ts +63 -0
- package/dist/llm-healer.js +166 -0
- package/dist/llm-provider.d.ts +29 -0
- package/dist/llm-provider.js +80 -0
- package/dist/llm-usage.d.ts +17 -0
- package/dist/llm-usage.js +45 -0
- package/dist/logger.d.ts +6 -2
- package/dist/logger.js +15 -1
- package/dist/mockup-html.d.ts +119 -0
- package/dist/mockup-html.js +263 -0
- package/dist/mockup.d.ts +187 -0
- package/dist/mockup.js +869 -0
- package/dist/mouse-animation.d.ts +46 -0
- package/dist/mouse-animation.js +114 -0
- package/dist/opcode-actions.d.ts +42 -0
- package/dist/opcode-actions.js +511 -0
- package/dist/opcode-runner.d.ts +51 -0
- package/dist/opcode-runner.js +770 -0
- package/dist/openrouter-client.d.ts +40 -0
- package/dist/openrouter-client.js +16 -0
- package/dist/overlay-engine.d.ts +24 -0
- package/dist/overlay-engine.js +176 -0
- package/dist/overlay-utils.d.ts +14 -0
- package/dist/overlay-utils.js +13 -0
- package/dist/postcondition.d.ts +16 -0
- package/dist/postcondition.js +269 -0
- package/dist/posthog.d.ts +4 -0
- package/dist/posthog.js +26 -0
- package/dist/program-patcher.d.ts +25 -0
- package/dist/program-patcher.js +44 -0
- package/dist/prompt-cache.d.ts +10 -0
- package/dist/prompt-cache.js +24 -0
- package/dist/prompts.d.ts +175 -0
- package/dist/prompts.js +1038 -0
- package/dist/provider-config.d.ts +12 -0
- package/dist/provider-config.js +15 -0
- package/dist/recovery-chain.d.ts +37 -0
- package/dist/recovery-chain.js +350 -0
- package/dist/remote-browser.d.ts +215 -0
- package/dist/remote-browser.js +360 -0
- package/dist/safari-browser-bar.d.ts +15 -0
- package/dist/safari-browser-bar.js +95 -0
- package/dist/safari-toolbar-asset.d.ts +15 -0
- package/dist/safari-toolbar-asset.js +12 -0
- package/dist/security.d.ts +21 -0
- package/dist/security.js +608 -0
- package/dist/selector-resolver.d.ts +34 -0
- package/dist/selector-resolver.js +181 -0
- package/dist/semantic-resolver.d.ts +35 -0
- package/dist/semantic-resolver.js +161 -0
- package/dist/server-capture-runtime.d.ts +125 -0
- package/dist/server-capture-runtime.js +585 -0
- package/dist/server-credit-usage.d.ts +12 -0
- package/dist/server-credit-usage.js +41 -0
- package/dist/server-posthog.d.ts +2 -0
- package/dist/server-posthog.js +16 -0
- package/dist/server-project-webhooks.d.ts +59 -0
- package/dist/server-project-webhooks.js +123 -0
- package/dist/server-screenshot-watermark.d.ts +7 -0
- package/dist/server-screenshot-watermark.js +60 -0
- package/dist/session-profile.d.ts +86 -0
- package/dist/session-profile.js +1536 -0
- package/dist/sf-pro-fonts.d.ts +4 -0
- package/dist/sf-pro-fonts.js +7 -0
- package/dist/sf-pro-symbols.d.ts +1 -0
- package/dist/sf-pro-symbols.js +55 -0
- package/dist/skill-packaging.d.ts +28 -0
- package/dist/skill-packaging.js +169 -0
- package/dist/smart-wait.d.ts +27 -0
- package/dist/smart-wait.js +81 -0
- package/dist/status-bar-l10n.d.ts +14 -0
- package/dist/status-bar-l10n.js +177 -0
- package/dist/status-bar-render.d.ts +20 -0
- package/dist/status-bar-render.js +410 -0
- package/dist/status-bar.d.ts +53 -0
- package/dist/status-bar.js +620 -0
- package/dist/svg-browser-bar.d.ts +33 -0
- package/dist/svg-browser-bar.js +206 -0
- package/dist/svg-status-bar.d.ts +36 -0
- package/dist/svg-status-bar.js +597 -0
- package/dist/svg-text.d.ts +61 -0
- package/dist/svg-text.js +118 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +216 -0
- package/dist/types.d.ts +240 -5
- package/dist/types.js +23 -1
- package/dist/v2/action-verifier.d.ts +29 -0
- package/dist/v2/action-verifier.js +133 -0
- package/dist/v2/alt-text.d.ts +26 -0
- package/dist/v2/alt-text.js +55 -0
- package/dist/v2/benchmark.d.ts +59 -0
- package/dist/v2/benchmark.js +135 -0
- package/dist/v2/capture-strategy.d.ts +30 -0
- package/dist/v2/capture-strategy.js +67 -0
- package/dist/v2/capture-verification.d.ts +35 -0
- package/dist/v2/capture-verification.js +95 -0
- package/dist/v2/circuit-breaker.d.ts +42 -0
- package/dist/v2/circuit-breaker.js +119 -0
- package/dist/v2/cli-runner-local.d.ts +11 -0
- package/dist/v2/cli-runner-local.js +91 -0
- package/dist/v2/cli-runner.d.ts +34 -0
- package/dist/v2/cli-runner.js +300 -0
- package/dist/v2/compiler-prompts.d.ts +27 -0
- package/dist/v2/compiler-prompts.js +123 -0
- package/dist/v2/compiler.d.ts +37 -0
- package/dist/v2/compiler.js +147 -0
- package/dist/v2/explorer.d.ts +41 -0
- package/dist/v2/explorer.js +56 -0
- package/dist/v2/index.d.ts +37 -0
- package/dist/v2/index.js +31 -0
- package/dist/v2/llm-healer.d.ts +62 -0
- package/dist/v2/llm-healer.js +166 -0
- package/dist/v2/llm-provider.d.ts +29 -0
- package/dist/v2/llm-provider.js +80 -0
- package/dist/v2/opcode-runner.d.ts +47 -0
- package/dist/v2/opcode-runner.js +634 -0
- package/dist/v2/overlay-engine.d.ts +24 -0
- package/dist/v2/overlay-engine.js +150 -0
- package/dist/v2/postcondition.d.ts +16 -0
- package/dist/v2/postcondition.js +249 -0
- package/dist/v2/program-patcher.d.ts +25 -0
- package/dist/v2/program-patcher.js +44 -0
- package/dist/v2/recovery-chain.d.ts +30 -0
- package/dist/v2/recovery-chain.js +368 -0
- package/dist/v2/schema.d.ts +2580 -0
- package/dist/v2/schema.js +295 -0
- package/dist/v2/selector-resolver.d.ts +34 -0
- package/dist/v2/selector-resolver.js +181 -0
- package/dist/v2/semantic-resolver.d.ts +35 -0
- package/dist/v2/semantic-resolver.js +161 -0
- package/dist/v2/smart-wait.d.ts +27 -0
- package/dist/v2/smart-wait.js +81 -0
- package/dist/v2/types.d.ts +444 -0
- package/dist/v2/types.js +19 -0
- package/dist/v2/web-playwright-local.d.ts +69 -0
- package/dist/v2/web-playwright-local.js +392 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/dist/video-agent.d.ts +143 -0
- package/dist/video-agent.js +4788 -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 +501 -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/dist/web-playwright-local.d.ts +126 -0
- package/dist/web-playwright-local.js +819 -0
- package/dist/ws-auth.d.ts +20 -0
- package/dist/ws-auth.js +70 -0
- package/dist/ws-broadcast.d.ts +34 -0
- package/dist/ws-broadcast.js +85 -0
- package/dist/ws-connection-limits.d.ts +12 -0
- package/dist/ws-connection-limits.js +44 -0
- package/dist/ws-handler-utils.d.ts +32 -0
- package/dist/ws-handler-utils.js +139 -0
- package/dist/ws-handler.d.ts +10 -0
- package/dist/ws-handler.js +1793 -0
- package/dist/ws-metrics-server.d.ts +9 -0
- package/dist/ws-metrics-server.js +31 -0
- package/dist/ws-server.d.ts +9 -0
- package/dist/ws-server.js +92 -0
- package/package.json +142 -71
|
@@ -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,201 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { rm, 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
|
+
const cachedBinaryPaths = {};
|
|
13
|
+
async function ensureBinaryAvailable(binaryName) {
|
|
14
|
+
const cached = cachedBinaryPaths[binaryName];
|
|
15
|
+
if (cached)
|
|
16
|
+
return cached;
|
|
17
|
+
const resolver = process.platform === 'win32' ? 'where' : 'which';
|
|
18
|
+
const { stdout } = await execFileAsync(resolver, [binaryName]);
|
|
19
|
+
const resolvedPath = stdout
|
|
20
|
+
.split(/\r?\n/)
|
|
21
|
+
.map(line => line.trim())
|
|
22
|
+
.find(Boolean);
|
|
23
|
+
if (!resolvedPath) {
|
|
24
|
+
throw new Error(`${binaryName} not found`);
|
|
25
|
+
}
|
|
26
|
+
cachedBinaryPaths[binaryName] = resolvedPath;
|
|
27
|
+
return resolvedPath;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the system ffmpeg binary path. Throws if not found.
|
|
31
|
+
*/
|
|
32
|
+
export async function ensureFfmpegAvailable() {
|
|
33
|
+
try {
|
|
34
|
+
return await ensureBinaryAvailable('ffmpeg');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error('ffmpeg is required for clip post-processing but was not found on your system. ' +
|
|
38
|
+
'Install it via: brew install ffmpeg (macOS), apt install ffmpeg (Linux), or choco install ffmpeg (Windows).');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// ── Conversion functions ────────────────────────────────────────────
|
|
42
|
+
/**
|
|
43
|
+
* Convert a WebM recording to an optimized GIF using a 2-pass palette approach.
|
|
44
|
+
* Pass 1: generate an optimal palette. Pass 2: encode with that palette.
|
|
45
|
+
*/
|
|
46
|
+
export async function convertToGif(webmPath, outputPath, opts = {}) {
|
|
47
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
48
|
+
const fps = opts.fps ?? DEFAULT_GIF_FPS;
|
|
49
|
+
const maxWidth = opts.maxWidth ?? DEFAULT_GIF_MAX_WIDTH;
|
|
50
|
+
const loopFlag = (opts.loop ?? true) ? '0' : '-1';
|
|
51
|
+
const paletteDir = path.dirname(outputPath);
|
|
52
|
+
const palettePath = path.join(paletteDir, `_palette_${Date.now()}.png`);
|
|
53
|
+
const scaleFilter = `fps=${fps},scale=${maxWidth}:-1:flags=lanczos`;
|
|
54
|
+
// Pass 1: Generate palette
|
|
55
|
+
await execFileAsync(ffmpeg, [
|
|
56
|
+
'-i', webmPath,
|
|
57
|
+
'-vf', `${scaleFilter},palettegen=stats_mode=diff`,
|
|
58
|
+
'-y', palettePath,
|
|
59
|
+
]);
|
|
60
|
+
// Pass 2: Encode GIF with palette
|
|
61
|
+
try {
|
|
62
|
+
await execFileAsync(ffmpeg, [
|
|
63
|
+
'-i', webmPath,
|
|
64
|
+
'-i', palettePath,
|
|
65
|
+
'-lavfi', `${scaleFilter}[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=3`,
|
|
66
|
+
'-loop', loopFlag,
|
|
67
|
+
'-y', outputPath,
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
// Clean up palette file
|
|
72
|
+
try {
|
|
73
|
+
await stat(palettePath);
|
|
74
|
+
await rm(palettePath, { force: true });
|
|
75
|
+
}
|
|
76
|
+
catch { /* ignore */ }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Convert a WebM recording to an MP4 with web-optimized settings.
|
|
81
|
+
*/
|
|
82
|
+
export async function convertToMp4(webmPath, outputPath) {
|
|
83
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
84
|
+
await execFileAsync(ffmpeg, [
|
|
85
|
+
'-i', webmPath,
|
|
86
|
+
'-c:v', 'libx264',
|
|
87
|
+
'-preset', 'slow',
|
|
88
|
+
'-crf', '22',
|
|
89
|
+
'-pix_fmt', 'yuv420p',
|
|
90
|
+
'-movflags', '+faststart',
|
|
91
|
+
'-an',
|
|
92
|
+
'-y', outputPath,
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Extract the first frame of a WebM as a PNG thumbnail.
|
|
97
|
+
*/
|
|
98
|
+
export async function extractThumbnail(webmPath, outputPath) {
|
|
99
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
100
|
+
await execFileAsync(ffmpeg, [
|
|
101
|
+
'-i', webmPath,
|
|
102
|
+
'-frames:v', '1',
|
|
103
|
+
'-y', outputPath,
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Trim a recording: skip dead frames at the start and cap duration.
|
|
108
|
+
*/
|
|
109
|
+
export async function trimRecording(inputPath, outputPath, startSec = DEFAULT_TRIM_START_SEC, maxDurationSec = DEFAULT_MAX_DURATION_SEC) {
|
|
110
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
111
|
+
await execFileAsync(ffmpeg, [
|
|
112
|
+
'-ss', String(startSec),
|
|
113
|
+
'-i', inputPath,
|
|
114
|
+
'-t', String(maxDurationSec),
|
|
115
|
+
'-c', 'copy',
|
|
116
|
+
'-y', outputPath,
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get the duration of a media file in milliseconds.
|
|
121
|
+
*/
|
|
122
|
+
export async function getMediaDurationMs(filePath) {
|
|
123
|
+
const ffprobe = await ensureBinaryAvailable('ffprobe');
|
|
124
|
+
const { stdout } = await execFileAsync(ffprobe, [
|
|
125
|
+
'-v', 'error',
|
|
126
|
+
'-show_entries', 'format=duration',
|
|
127
|
+
'-of', 'default=noprint_wrappers=1:nokey=1',
|
|
128
|
+
filePath,
|
|
129
|
+
]);
|
|
130
|
+
return Math.round(parseFloat(stdout.trim()) * 1000);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Freeze the last frame of a video for `durationSec` additional seconds.
|
|
134
|
+
* Uses ffmpeg's tpad filter to hold the final frame in place.
|
|
135
|
+
*/
|
|
136
|
+
async function freezeLastFrame(inputPath, outputPath, durationSec) {
|
|
137
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
138
|
+
const stopDurationMs = Math.round(durationSec * 1000);
|
|
139
|
+
await execFileAsync(ffmpeg, [
|
|
140
|
+
'-i', inputPath,
|
|
141
|
+
'-vf', `tpad=stop_mode=clone:stop_duration=${stopDurationMs}ms`,
|
|
142
|
+
'-c:v', 'libvpx-vp9',
|
|
143
|
+
'-crf', '30',
|
|
144
|
+
'-b:v', '0',
|
|
145
|
+
'-an',
|
|
146
|
+
'-y', outputPath,
|
|
147
|
+
]);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Full post-processing pipeline for a single clip recording:
|
|
151
|
+
* trim → GIF and/or MP4 → thumbnail → measure.
|
|
152
|
+
*/
|
|
153
|
+
export async function postProcessClipRecording(webmPath, outputDir, clipId, options = {}) {
|
|
154
|
+
const maxDuration = options.maxDurationSec ?? DEFAULT_MAX_DURATION_SEC;
|
|
155
|
+
// Step 1: Trim the recording — cut the setup phase (page load, overlay dismiss)
|
|
156
|
+
const trimStart = options.trimStartSec ?? DEFAULT_TRIM_START_SEC;
|
|
157
|
+
const trimmedPath = path.join(outputDir, `${clipId}_trimmed.webm`);
|
|
158
|
+
await trimRecording(webmPath, trimmedPath, trimStart, maxDuration);
|
|
159
|
+
// Step 1b: Optionally freeze the last frame for a pause before looping
|
|
160
|
+
const holdSec = Math.min(Math.max(options.holdLastFrameSec ?? 0, 0), 10);
|
|
161
|
+
let sourcePath = trimmedPath;
|
|
162
|
+
if (holdSec > 0) {
|
|
163
|
+
const paddedPath = path.join(outputDir, `${clipId}_padded.webm`);
|
|
164
|
+
await freezeLastFrame(trimmedPath, paddedPath, holdSec);
|
|
165
|
+
sourcePath = paddedPath;
|
|
166
|
+
}
|
|
167
|
+
const durationMs = await getMediaDurationMs(sourcePath);
|
|
168
|
+
const result = { durationMs };
|
|
169
|
+
// Step 2: Always persist both a GIF and an MP4.
|
|
170
|
+
// The GIF remains useful for delivery and embeds, while the MP4 is the
|
|
171
|
+
// high-fidelity source for Studio playback and downstream re-renders.
|
|
172
|
+
const gifPath = path.join(outputDir, `${clipId}.gif`);
|
|
173
|
+
await convertToGif(sourcePath, gifPath, {
|
|
174
|
+
fps: options.gifFps,
|
|
175
|
+
maxWidth: options.gifMaxWidth,
|
|
176
|
+
loop: options.loop,
|
|
177
|
+
});
|
|
178
|
+
result.gifPath = gifPath;
|
|
179
|
+
const gifStat = await stat(gifPath);
|
|
180
|
+
result.fileSizeBytes = gifStat.size;
|
|
181
|
+
const mp4Path = path.join(outputDir, `${clipId}.mp4`);
|
|
182
|
+
await convertToMp4(sourcePath, mp4Path);
|
|
183
|
+
result.mp4Path = mp4Path;
|
|
184
|
+
// Step 3: Extract thumbnail
|
|
185
|
+
const thumbnailPath = path.join(outputDir, `${clipId}_thumb.png`);
|
|
186
|
+
await extractThumbnail(sourcePath, thumbnailPath);
|
|
187
|
+
result.thumbnailPath = thumbnailPath;
|
|
188
|
+
// Clean up intermediate files
|
|
189
|
+
try {
|
|
190
|
+
await rm(trimmedPath, { force: true });
|
|
191
|
+
}
|
|
192
|
+
catch { /* ignore */ }
|
|
193
|
+
if (sourcePath !== trimmedPath) {
|
|
194
|
+
try {
|
|
195
|
+
await rm(sourcePath, { force: true });
|
|
196
|
+
}
|
|
197
|
+
catch { /* ignore */ }
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=clip-postprocess.js.map
|
package/dist/cookie-dismiss.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Page } from 'playwright';
|
|
2
|
+
export declare function getCaptureHideCSS(): string;
|
|
3
|
+
export declare function ensureCaptureHideStyles(page: Page): Promise<void>;
|
|
2
4
|
export declare function dismissCookiesAndWidgets(page: Page): Promise<{
|
|
3
5
|
dismissed: boolean;
|
|
4
6
|
method: string | null;
|
package/dist/cookie-dismiss.js
CHANGED
|
@@ -82,6 +82,51 @@ const HIDE_SELECTORS = [
|
|
|
82
82
|
'[id*="cookieBanner"]', '[id*="cookieConsent"]',
|
|
83
83
|
'[class*="gdpr"]', '[id*="gdpr"]',
|
|
84
84
|
];
|
|
85
|
+
// Dev-only chrome that should never appear in captures
|
|
86
|
+
const DEV_TOOL_SELECTORS = [
|
|
87
|
+
// Next.js dev indicator / devtools
|
|
88
|
+
'nextjs-portal',
|
|
89
|
+
'#__next-build-watcher',
|
|
90
|
+
'[data-next-badge-root]',
|
|
91
|
+
// Astro dev toolbar
|
|
92
|
+
'astro-dev-toolbar',
|
|
93
|
+
// Vite overlays
|
|
94
|
+
'vite-error-overlay',
|
|
95
|
+
'vite-plugin-checker-error-overlay',
|
|
96
|
+
// Nuxt devtools
|
|
97
|
+
'nuxt-devtools',
|
|
98
|
+
'nuxt-devtools-frame',
|
|
99
|
+
'#nuxt-devtools-anchor',
|
|
100
|
+
'#nuxt-devtools-container',
|
|
101
|
+
];
|
|
102
|
+
const CAPTURE_HIDE_STYLE_ID = 'autokap-capture-hide-style';
|
|
103
|
+
export function getCaptureHideCSS() {
|
|
104
|
+
return [...HIDE_SELECTORS, ...DEV_TOOL_SELECTORS].map(selector => `${selector} { display: none !important; visibility: hidden !important; pointer-events: none !important; }`).join('\n');
|
|
105
|
+
}
|
|
106
|
+
export async function ensureCaptureHideStyles(page) {
|
|
107
|
+
try {
|
|
108
|
+
await page.evaluate(({ styleId, css }) => {
|
|
109
|
+
const parent = document.head ?? document.documentElement;
|
|
110
|
+
if (!parent)
|
|
111
|
+
return;
|
|
112
|
+
let style = document.getElementById(styleId);
|
|
113
|
+
if (!style) {
|
|
114
|
+
style = document.createElement('style');
|
|
115
|
+
style.id = styleId;
|
|
116
|
+
parent.appendChild(style);
|
|
117
|
+
}
|
|
118
|
+
if (style.textContent !== css) {
|
|
119
|
+
style.textContent = css;
|
|
120
|
+
}
|
|
121
|
+
}, {
|
|
122
|
+
styleId: CAPTURE_HIDE_STYLE_ID,
|
|
123
|
+
css: getCaptureHideCSS(),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Page might have navigated away
|
|
128
|
+
}
|
|
129
|
+
}
|
|
85
130
|
export async function dismissCookiesAndWidgets(page) {
|
|
86
131
|
// Strategy 1: Click known CMP buttons
|
|
87
132
|
for (const selector of CMP_SELECTORS) {
|
|
@@ -91,7 +136,7 @@ export async function dismissCookiesAndWidgets(page) {
|
|
|
91
136
|
await button.click({ timeout: 2000 });
|
|
92
137
|
logger.info(`Cookie dismissed via CMP selector: ${selector}`);
|
|
93
138
|
await page.waitForTimeout(500);
|
|
94
|
-
await
|
|
139
|
+
await ensureCaptureHideStyles(page);
|
|
95
140
|
return { dismissed: true, method: `cmp:${selector}` };
|
|
96
141
|
}
|
|
97
142
|
}
|
|
@@ -148,7 +193,7 @@ export async function dismissCookiesAndWidgets(page) {
|
|
|
148
193
|
if (dismissed) {
|
|
149
194
|
logger.info(`Cookie dismissed via text match: "${dismissed}"`);
|
|
150
195
|
await page.waitForTimeout(500);
|
|
151
|
-
await
|
|
196
|
+
await ensureCaptureHideStyles(page);
|
|
152
197
|
return { dismissed: true, method: `text:${dismissed}` };
|
|
153
198
|
}
|
|
154
199
|
}
|
|
@@ -156,17 +201,7 @@ export async function dismissCookiesAndWidgets(page) {
|
|
|
156
201
|
// Text-based search failed
|
|
157
202
|
}
|
|
158
203
|
// Strategy 3: Always inject CSS to hide known overlays and widgets
|
|
159
|
-
await
|
|
204
|
+
await ensureCaptureHideStyles(page);
|
|
160
205
|
return { dismissed: false, method: null };
|
|
161
206
|
}
|
|
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
207
|
//# sourceMappingURL=cookie-dismiss.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import type { StepUsage } from './types.js';
|
|
3
|
+
export interface CostLogContext {
|
|
4
|
+
runId: string;
|
|
5
|
+
userId: string;
|
|
6
|
+
projectId?: string;
|
|
7
|
+
presetId?: string;
|
|
8
|
+
lang?: string;
|
|
9
|
+
theme?: string;
|
|
10
|
+
captureType?: 'fullpage' | 'element' | 'video';
|
|
11
|
+
elementName?: string;
|
|
12
|
+
targetId?: string;
|
|
13
|
+
captureId?: string | null;
|
|
14
|
+
viewportWidth?: number | null;
|
|
15
|
+
viewportHeight?: number | null;
|
|
16
|
+
}
|
|
17
|
+
export declare function insertCostLogs(supabase: SupabaseClient, ctx: CostLogContext, steps: StepUsage[]): Promise<string[]>;
|
|
18
|
+
export declare function updateCostLogCaptureContext(supabase: SupabaseClient, costLogIds: string[], params: {
|
|
19
|
+
captureId?: string | null;
|
|
20
|
+
targetId?: string | null;
|
|
21
|
+
viewportWidth?: number | null;
|
|
22
|
+
viewportHeight?: number | null;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Retry resolution for previously failed cost logs.
|
|
26
|
+
* Designed to be called by a reconciliation cron job.
|
|
27
|
+
*/
|
|
28
|
+
export declare function reconcileUnresolvedCosts(supabase: SupabaseClient, apiKey: string): Promise<{
|
|
29
|
+
retried: number;
|
|
30
|
+
resolved: number;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function resolveAllCosts(supabase: SupabaseClient, entries: Array<{
|
|
33
|
+
costLogId: string;
|
|
34
|
+
generationId: string | null;
|
|
35
|
+
}>, apiKey: string, serviceClient?: SupabaseClient): Promise<void>;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
function isFiniteInteger(value) {
|
|
2
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
3
|
+
}
|
|
4
|
+
function normalizeUsageForCostLog(step, index) {
|
|
5
|
+
const normalizedStepNumber = isFiniteInteger(step.stepNumber) && step.stepNumber >= 0
|
|
6
|
+
? step.stepNumber
|
|
7
|
+
: index + 1;
|
|
8
|
+
const normalizedStepType = typeof step.stepType === 'string' && step.stepType.trim().length > 0
|
|
9
|
+
? step.stepType
|
|
10
|
+
: 'agent_iteration';
|
|
11
|
+
const normalizedModelRequested = typeof step.modelRequested === 'string' && step.modelRequested.trim().length > 0
|
|
12
|
+
? step.modelRequested
|
|
13
|
+
: step.modelUsed ?? 'unknown';
|
|
14
|
+
const normalizedImagesInPrompt = typeof step.imagesInPrompt === 'number' && Number.isFinite(step.imagesInPrompt)
|
|
15
|
+
? step.imagesInPrompt
|
|
16
|
+
: 0;
|
|
17
|
+
if (normalizedStepNumber !== step.stepNumber
|
|
18
|
+
|| normalizedStepType !== step.stepType
|
|
19
|
+
|| normalizedModelRequested !== step.modelRequested
|
|
20
|
+
|| normalizedImagesInPrompt !== step.imagesInPrompt) {
|
|
21
|
+
console.warn('Normalizing invalid cost log usage entry:', JSON.stringify({
|
|
22
|
+
stepNumber: step.stepNumber ?? null,
|
|
23
|
+
stepType: step.stepType ?? null,
|
|
24
|
+
modelRequested: step.modelRequested ?? null,
|
|
25
|
+
modelUsed: step.modelUsed ?? null,
|
|
26
|
+
fallbackStepNumber: normalizedStepNumber,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
...step,
|
|
31
|
+
stepNumber: normalizedStepNumber,
|
|
32
|
+
stepType: normalizedStepType,
|
|
33
|
+
modelRequested: normalizedModelRequested,
|
|
34
|
+
imagesInPrompt: normalizedImagesInPrompt,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function insertCostLogs(supabase, ctx, steps) {
|
|
38
|
+
if (steps.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
const rows = steps.map((rawStep, index) => {
|
|
41
|
+
const step = normalizeUsageForCostLog(rawStep, index);
|
|
42
|
+
return {
|
|
43
|
+
id: crypto.randomUUID(),
|
|
44
|
+
run_id: ctx.runId,
|
|
45
|
+
user_id: ctx.userId,
|
|
46
|
+
project_id: ctx.projectId ?? null,
|
|
47
|
+
preset_id: ctx.presetId ?? null,
|
|
48
|
+
capture_id: ctx.captureId ?? null,
|
|
49
|
+
step_number: step.stepNumber,
|
|
50
|
+
step_type: step.stepType,
|
|
51
|
+
model_requested: step.modelRequested,
|
|
52
|
+
model_used: step.modelUsed,
|
|
53
|
+
generation_id: step.generationId,
|
|
54
|
+
prompt_tokens: step.promptTokens,
|
|
55
|
+
completion_tokens: step.completionTokens,
|
|
56
|
+
total_tokens: step.totalTokens,
|
|
57
|
+
cost_resolved: false,
|
|
58
|
+
images_in_prompt: step.imagesInPrompt,
|
|
59
|
+
lang: ctx.lang ?? null,
|
|
60
|
+
theme: ctx.theme ?? null,
|
|
61
|
+
capture_type: ctx.captureType ?? null,
|
|
62
|
+
element_name: ctx.elementName ?? null,
|
|
63
|
+
target_id: ctx.targetId ?? null,
|
|
64
|
+
viewport_width: ctx.viewportWidth ?? null,
|
|
65
|
+
viewport_height: ctx.viewportHeight ?? null,
|
|
66
|
+
system_prompt_chars: step.systemPromptChars ?? null,
|
|
67
|
+
tool_schema_chars: step.toolSchemaChars ?? null,
|
|
68
|
+
user_payload_chars: step.userPayloadChars ?? null,
|
|
69
|
+
accessibility_chars: step.accessibilityChars ?? null,
|
|
70
|
+
interactive_element_count: step.interactiveElementCount ?? null,
|
|
71
|
+
action_history_count: step.actionHistoryCount ?? null,
|
|
72
|
+
elements_chars: step.elementsChars ?? null,
|
|
73
|
+
session_summary_chars: step.sessionSummaryChars ?? null,
|
|
74
|
+
selector_memory_chars: step.selectorMemoryChars ?? null,
|
|
75
|
+
agent_context_chars: step.agentContextChars ?? null,
|
|
76
|
+
profile_validation_status: step.profileValidationStatus ?? null,
|
|
77
|
+
session_profile_source: step.sessionProfileSource ?? null,
|
|
78
|
+
repair_path_used: step.repairPathUsed ?? null,
|
|
79
|
+
evaluator_used: step.evaluatorUsed ?? false,
|
|
80
|
+
cache_read_tokens: step.cacheReadTokens ?? null,
|
|
81
|
+
cache_write_tokens: step.cacheWriteTokens ?? null,
|
|
82
|
+
session_profile_reused: step.sessionProfileReused ?? false,
|
|
83
|
+
action_replay_used: step.actionReplayUsed ?? false,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const { error } = await supabase.from('cost_logs').insert(rows);
|
|
88
|
+
if (error) {
|
|
89
|
+
console.error('Failed to insert cost logs:', error.message);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
return rows.map((row) => row.id);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error('Failed to insert cost logs:', error.message);
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export async function updateCostLogCaptureContext(supabase, costLogIds, params) {
|
|
100
|
+
if (costLogIds.length === 0)
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
const { error } = await supabase
|
|
104
|
+
.from('cost_logs')
|
|
105
|
+
.update({
|
|
106
|
+
capture_id: params.captureId ?? null,
|
|
107
|
+
target_id: params.targetId ?? null,
|
|
108
|
+
viewport_width: params.viewportWidth ?? null,
|
|
109
|
+
viewport_height: params.viewportHeight ?? null,
|
|
110
|
+
})
|
|
111
|
+
.in('id', costLogIds);
|
|
112
|
+
if (error) {
|
|
113
|
+
console.error('Failed to update cost log capture context:', error.message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('Failed to update cost log capture context:', error.message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function resolveCost(updateClient, generationId, costLogId, apiKey) {
|
|
121
|
+
const delays = [10_000, 20_000, 40_000, 80_000, 160_000];
|
|
122
|
+
for (let attempt = 0; attempt < delays.length; attempt += 1) {
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
|
|
124
|
+
// Track resolution attempt (best-effort)
|
|
125
|
+
try {
|
|
126
|
+
await updateClient
|
|
127
|
+
.from('cost_logs')
|
|
128
|
+
.update({
|
|
129
|
+
resolution_attempts: attempt + 1,
|
|
130
|
+
last_resolution_attempt_at: new Date().toISOString(),
|
|
131
|
+
cost_resolution_status: 'pending',
|
|
132
|
+
})
|
|
133
|
+
.eq('id', costLogId);
|
|
134
|
+
}
|
|
135
|
+
catch { /* best-effort tracking */ }
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch(`https://openrouter.ai/api/v1/generation?id=${generationId}`, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
138
|
+
if (response.status === 404 && attempt < delays.length - 1) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
console.error(`OpenRouter generation lookup failed (${generationId}): HTTP ${response.status}`);
|
|
143
|
+
if (attempt === delays.length - 1) {
|
|
144
|
+
await markCostResolutionFailed(updateClient, costLogId, `HTTP ${response.status}`);
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const generation = await response.json();
|
|
149
|
+
const totalCost = generation.data?.total_cost ?? null;
|
|
150
|
+
if (totalCost == null) {
|
|
151
|
+
console.warn(`OpenRouter returned no total_cost for generation ${generationId}`);
|
|
152
|
+
if (attempt === delays.length - 1) {
|
|
153
|
+
await markCostResolutionFailed(updateClient, costLogId, 'no total_cost in response');
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const { error } = await updateClient
|
|
158
|
+
.from('cost_logs')
|
|
159
|
+
.update({
|
|
160
|
+
cost_usd: totalCost,
|
|
161
|
+
cost_resolved: true,
|
|
162
|
+
resolved_at: new Date().toISOString(),
|
|
163
|
+
cost_resolution_status: 'resolved',
|
|
164
|
+
})
|
|
165
|
+
.eq('id', costLogId);
|
|
166
|
+
if (error) {
|
|
167
|
+
console.error(`cost_logs update failed for row ${costLogId}:`, error.message);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (attempt === delays.length - 1) {
|
|
173
|
+
console.error(`Cost resolution failed for ${generationId}:`, error.message);
|
|
174
|
+
await markCostResolutionFailed(updateClient, costLogId, error.message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function markCostResolutionFailed(updateClient, costLogId, reason) {
|
|
180
|
+
try {
|
|
181
|
+
await updateClient
|
|
182
|
+
.from('cost_logs')
|
|
183
|
+
.update({ cost_resolution_status: 'failed' })
|
|
184
|
+
.eq('id', costLogId);
|
|
185
|
+
console.error(`Cost resolution marked as failed for ${costLogId}: ${reason}`);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// best-effort — don't fail the caller
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Retry resolution for previously failed cost logs.
|
|
193
|
+
* Designed to be called by a reconciliation cron job.
|
|
194
|
+
*/
|
|
195
|
+
export async function reconcileUnresolvedCosts(supabase, apiKey) {
|
|
196
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
197
|
+
const { data: failedLogs, error } = await supabase
|
|
198
|
+
.from('cost_logs')
|
|
199
|
+
.select('id, generation_id')
|
|
200
|
+
.eq('cost_resolution_status', 'failed')
|
|
201
|
+
.gt('created_at', sevenDaysAgo)
|
|
202
|
+
.not('generation_id', 'is', null)
|
|
203
|
+
.limit(100);
|
|
204
|
+
if (error || !failedLogs?.length) {
|
|
205
|
+
return { retried: 0, resolved: 0 };
|
|
206
|
+
}
|
|
207
|
+
let resolved = 0;
|
|
208
|
+
for (const log of failedLogs) {
|
|
209
|
+
try {
|
|
210
|
+
const response = await fetch(`https://openrouter.ai/api/v1/generation?id=${log.generation_id}`, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
211
|
+
if (!response.ok)
|
|
212
|
+
continue;
|
|
213
|
+
const generation = await response.json();
|
|
214
|
+
const totalCost = generation.data?.total_cost ?? null;
|
|
215
|
+
if (totalCost == null)
|
|
216
|
+
continue;
|
|
217
|
+
await supabase
|
|
218
|
+
.from('cost_logs')
|
|
219
|
+
.update({
|
|
220
|
+
cost_usd: totalCost,
|
|
221
|
+
cost_resolved: true,
|
|
222
|
+
resolved_at: new Date().toISOString(),
|
|
223
|
+
cost_resolution_status: 'resolved',
|
|
224
|
+
})
|
|
225
|
+
.eq('id', log.id);
|
|
226
|
+
resolved += 1;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// skip this entry, will be retried next run
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { retried: failedLogs.length, resolved };
|
|
233
|
+
}
|
|
234
|
+
export function resolveAllCosts(supabase, entries, apiKey, serviceClient) {
|
|
235
|
+
const toResolve = entries.filter((entry) => entry.generationId != null);
|
|
236
|
+
if (toResolve.length === 0) {
|
|
237
|
+
return Promise.resolve();
|
|
238
|
+
}
|
|
239
|
+
const updateClient = serviceClient ?? supabase;
|
|
240
|
+
return Promise.allSettled(toResolve.map((entry) => resolveCost(updateClient, entry.generationId, entry.costLogId, apiKey))).then(() => undefined);
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=cost-logging.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost resolution monitoring — exposes metrics for alerting
|
|
3
|
+
* when the failure rate of OpenRouter cost lookups exceeds thresholds.
|
|
4
|
+
*/
|
|
5
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
6
|
+
export interface CostResolutionStats {
|
|
7
|
+
/** Number of cost logs with status 'failed' in the last hour. */
|
|
8
|
+
failedLastHour: number;
|
|
9
|
+
/** Total cost logs created in the last hour. */
|
|
10
|
+
totalLastHour: number;
|
|
11
|
+
/** Failure rate as a percentage (0-100). */
|
|
12
|
+
failureRate: number;
|
|
13
|
+
/** Number of cost logs still pending (older than 5 minutes). */
|
|
14
|
+
stalePending: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function getCostResolutionStats(supabase: SupabaseClient): Promise<CostResolutionStats>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost resolution monitoring — exposes metrics for alerting
|
|
3
|
+
* when the failure rate of OpenRouter cost lookups exceeds thresholds.
|
|
4
|
+
*/
|
|
5
|
+
export async function getCostResolutionStats(supabase) {
|
|
6
|
+
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
|
7
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
|
8
|
+
const [failedResult, totalResult, staleResult] = await Promise.all([
|
|
9
|
+
supabase
|
|
10
|
+
.from('cost_logs')
|
|
11
|
+
.select('*', { count: 'exact', head: true })
|
|
12
|
+
.eq('cost_resolution_status', 'failed')
|
|
13
|
+
.gt('created_at', oneHourAgo),
|
|
14
|
+
supabase
|
|
15
|
+
.from('cost_logs')
|
|
16
|
+
.select('*', { count: 'exact', head: true })
|
|
17
|
+
.gt('created_at', oneHourAgo)
|
|
18
|
+
.not('generation_id', 'is', null),
|
|
19
|
+
supabase
|
|
20
|
+
.from('cost_logs')
|
|
21
|
+
.select('*', { count: 'exact', head: true })
|
|
22
|
+
.eq('cost_resolution_status', 'pending')
|
|
23
|
+
.lt('created_at', fiveMinutesAgo)
|
|
24
|
+
.not('generation_id', 'is', null),
|
|
25
|
+
]);
|
|
26
|
+
const failedLastHour = failedResult.count ?? 0;
|
|
27
|
+
const totalLastHour = totalResult.count ?? 0;
|
|
28
|
+
const stalePending = staleResult.count ?? 0;
|
|
29
|
+
const failureRate = totalLastHour > 0
|
|
30
|
+
? Math.round((failedLastHour / totalLastHour) * 10000) / 100
|
|
31
|
+
: 0;
|
|
32
|
+
return { failedLastHour, totalLastHour, failureRate, stalePending };
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=cost-resolution-monitor.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>;
|