UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

220 lines 9.59 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MiscUtils = void 0; const fs_1 = require("fs"); const uuid_1 = require("uuid"); const path_1 = require("path"); const os_1 = require("os"); const jimp_1 = require("jimp"); const envVars_1 = require("../envVars"); class MiscUtils { constructor() { } /** * This is the base working directory for the entire application. Care is made * to ensure we do not access data outside of this directory to maintain * proper permissions across platforms. * * Returns platform-specific application data directories: * - macOS: ~/Library/Application Support/Donobu * - Windows: %APPDATA%/Donobu * - Linux: ~/.config/Donobu * * If the BASE_WORKING_DIR environment variable is set, its value is returned. */ static baseWorkingDirectory() { const persistenceDirOverride = process.env[envVars_1.ENV_VAR_NAMES.BASE_WORKING_DIR]; if (persistenceDirOverride) { return persistenceDirOverride; } const appName = 'Donobu Studio'; switch ((0, os_1.platform)()) { case 'win32': // Windows - typically use %APPDATA% (Roaming). return (0, path_1.join)(process.env.APPDATA || (0, path_1.join)((0, os_1.homedir)(), 'AppData', 'Roaming'), appName); case 'darwin': // macOS return (0, path_1.join)((0, os_1.homedir)(), 'Library', 'Application Support', appName); case 'linux': // Linux - following XDG Base Directory Specification. return (0, path_1.join)(process.env.XDG_CONFIG_HOME || (0, path_1.join)((0, os_1.homedir)(), '.config'), appName); default: // Default fallback to home directory for other platforms. return (0, path_1.join)((0, os_1.homedir)(), `.${appName}`); } } /** * Updates token counts in flow metadata based on GPT response */ static updateTokenCounts(gptResponse, flowMetadata) { flowMetadata.inputTokensUsed += gptResponse.promptTokensUsed; flowMetadata.completionTokensUsed += gptResponse.completionTokensUsed; } /** * Loads a resource file from the assets directory as a string */ static getResourceFileAsString(fileName) { const bytes = this.getResourceFileAsBytes(fileName); return bytes.toString('utf8'); } /** * Loads a resource file from the assets directory as a Buffer */ static getResourceFileAsBytes(fileName) { // Check if we're running from compiled code. const isProduction = __dirname.includes('dist'); const assetPath = isProduction ? (0, path_1.join)(__dirname, '..', 'assets', fileName) // Go up one level from dist/ : (0, path_1.join)(__dirname, '..', '..', 'assets', fileName); // Go up two levels from src/ return (0, fs_1.readFileSync)(assetPath); } /** * Reduces the resolution of a PNG image by half. * * @param inputImageBytes - The input PNG image as a Buffer. * @returns A Promise that resolves to the resized PNG image as a Buffer. * @throws Error if image processing fails */ static async resizePngByHalf(inputImageBytes) { const image = await jimp_1.Jimp.read(inputImageBytes); image.resize({ w: Math.floor(image.bitmap.width / 2), h: Math.floor(image.bitmap.height / 2), }); return image.getBuffer('image/png'); } /** * Resizes a PNG image until its file size is below a specified maximum size. * * @param inputImageBytes - The input PNG image as a Buffer. * @param maxSizeInBytes - The maximum allowed file size in bytes. * @param minDimension - Minimum dimension (width or height) to prevent excessive resizing, default is 50px. * @returns A Promise that resolves to the resized PNG image as a Buffer. * @throws Error if the image cannot be resized to meet the size requirements or if processing fails. */ static async resizePngToMaxFileSize(inputImageBytes, maxSizeInBytes) { // Validate input if (maxSizeInBytes <= 0) { throw new Error('Maximum file size must be positive'); } // If input is already smaller than max size, return it as is if (inputImageBytes.length <= maxSizeInBytes) { return inputImageBytes; } // Load the image const image = await jimp_1.Jimp.read(inputImageBytes); let currentBuffer = await image.getBuffer('image/png'); let currentSize = currentBuffer.length; // Initial width and height let width = image.bitmap.width; let height = image.bitmap.height; // Initialize scaling factor (start with 90% of original size) let scaleFactor = 0.9; // Keep resizing until we're under the max size or hit minimum dimensions while (currentSize > maxSizeInBytes) { // Calculate new dimensions const newWidth = Math.floor(width * scaleFactor); const newHeight = Math.floor(height * scaleFactor); image.resize({ w: newWidth, h: newHeight }); currentBuffer = await image.getBuffer('image/png'); currentSize = currentBuffer.length; // Adjust scaling factor based on how close we are to target if (currentSize > maxSizeInBytes * 1.5) { // Still way too big, scale down faster scaleFactor = 0.7; } else if (currentSize > maxSizeInBytes * 1.2) { // Getting closer, scale down moderately scaleFactor = 0.8; } else { // Close to target, fine-tune scaleFactor = 0.9; } // Update dimensions for next iteration width = image.bitmap.width; height = image.bitmap.height; } return currentBuffer; } /** * Returns a duration (in milliseconds) that represents a statistically * plausible human mouse-click press duration. */ static generateHumanLikeClickDurationInMs() { // Statistical constants for human-like click-press durations. const CLICK_MEDIAN = 85; // ms const Q1 = 75; // ms const Q3 = 95; // ms const LOWER_FENCE = 55; // ms const UPPER_FENCE = 135; // ms // Box-Muller transform to generate normally distributed values. const u1 = Math.random(); const u2 = Math.random(); const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2); // Scale to our desired range (using IQR as a reference for standard deviation). const standardDeviation = (Q3 - Q1) / 1.34896; // Approximate relationship between IQR and SD. const value = CLICK_MEDIAN + z * standardDeviation; // Clamp the value between our fences. return Math.round(Math.min(Math.max(value, LOWER_FENCE), UPPER_FENCE)); } /** * Returns a duration (in milliseconds) that represents a statistically * plausible human key press duration. */ static generateHumanLikeKeyPressDurationInMs(key) { // Statistical constants for human-like typing. const REGULAR_KEY_MEDIAN = 100; // ms const REGULAR_KEY_Q1 = 80; // ms const REGULAR_KEY_Q3 = 120; // ms const REGULAR_KEY_LOWER_FENCE = 60; // ms const REGULAR_KEY_UPPER_FENCE = 160; // ms // Modifier and special keys are typically held longer const SPECIAL_KEYS = new Set([ 'Shift', 'Control', 'Alt', 'Meta', 'Enter', 'Tab', 'CapsLock', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', ]); const isSpecialKey = SPECIAL_KEYS.has(key); // Generate base duration using Box-Muller transform. const u1 = Math.random(); const u2 = Math.random(); const z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2); const standardDeviation = (REGULAR_KEY_Q3 - REGULAR_KEY_Q1) / 1.34896; let value = REGULAR_KEY_MEDIAN + z * standardDeviation; // Clamp to reasonable bounds. value = Math.min(Math.max(value, REGULAR_KEY_LOWER_FENCE), REGULAR_KEY_UPPER_FENCE); // Special keys are held longer. if (isSpecialKey) { value *= 1.5; // 50% longer for special keys } // Add small random variation const jitter = Math.random() * 10 - 5; // +/- 5ms random jitter. return Math.round(value + jitter); } /** * Creates a new random tool call ID. This is useful for registering ProposedToolCall and * ToolCalls that were proposed and/or invoked by a user rather than by a GPT, or by GPT * APIs that do not natively use tool call IDs (i.e. Google Gemini). Note OpenAI constrains the * length of tool call IDs to no longer than 40 characters. * * @returns A string representing a unique tool call ID. */ static createAdHocToolCallId() { return `adhoc_${(0, uuid_1.v4)().replace(/-/g, '')}`; } } exports.MiscUtils = MiscUtils; MiscUtils.DONOBU_VERSION = MiscUtils.getResourceFileAsString((0, path_1.join)('generated', 'version')); //# sourceMappingURL=MiscUtils.js.map