UNPKG

donobu

Version:

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

274 lines 10.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MiscUtils = void 0; const crypto_1 = require("crypto"); const fs_1 = require("fs"); const os_1 = require("os"); const path_1 = require("path"); const v4_1 = __importDefault(require("zod/v4")); const envVars_1 = require("../envVars"); class MiscUtils { constructor() { } static errName(error) { if (error instanceof Error) { return error.constructor.name !== 'Error' ? error.constructor.name : error.name; } else { return typeof error; } } static capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); } /** * Force a true/false/undefined value for a given value. This is a stripped down * version of th 'yn' library. */ static yn(value) { value = String(value).trim(); if (/^(?:y|yes|true|1|on)$/i.test(value)) { return true; } else if (/^(?:n|no|false|0|off)$/i.test(value)) { return false; } else { return undefined; } } /** * 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(environ = envVars_1.env.pick('APPDATA', 'BASE_WORKING_DIR', 'XDG_CONFIG_HOME')) { const persistenceDirOverride = environ.data.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)(environ.data.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)(environ.data.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}`); } } static detectImageType(buffer) { // Check for PNG signature (89 50 4E 47 0D 0A 1A 0A) if (buffer.length >= 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47 && buffer[4] === 0x0d && buffer[5] === 0x0a && buffer[6] === 0x1a && buffer[7] === 0x0a) { return 'png'; } // Check for JPEG signature (FF D8 FF) if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) { return 'jpeg'; } // Default to PNG if unknown. return 'png'; } /** * Updates token counts in flow metadata based on GPT response */ static updateTokenCounts(gptResponse, flowMetadata) { flowMetadata.inputTokensUsed += gptResponse.promptTokensUsed; flowMetadata.completionTokensUsed += gptResponse.completionTokensUsed; } /** * Resolves the absolute path to a file in the assets directory. * Works from both `src/` (dev) and `dist/` (compiled). */ static getResourceFilePath(fileName) { const isProduction = __dirname.includes('dist'); return 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/ } /** * Loads a resource file from the assets directory as a string */ static getResourceFileAsString(fileName) { return (0, fs_1.readFileSync)(MiscUtils.getResourceFilePath(fileName), { encoding: 'utf8', }); } static getPackageVersion() { const candidatePaths = [ (0, path_1.join)(__dirname, '..', '..', 'package.json'), (0, path_1.join)(__dirname, '..', '..', '..', 'package.json'), ]; const packageJsonPath = candidatePaths.find((path) => (0, fs_1.existsSync)(path)); if (!packageJsonPath) { throw new Error(`Cannot locate package.json. Tried: ${candidatePaths.join(', ')}`); } return v4_1.default .object({ version: v4_1.default.string(), }) .parse(JSON.parse((0, fs_1.readFileSync)(packageJsonPath, { encoding: 'utf8' }))) .version; } /** * 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, crypto_1.randomUUID)().replace(/-/g, '')}`; } /** * Merges adjacent user messages (some LLMs like Gemini's or Anthropic's require this). */ static mergeAdjacentUserMessages(messages) { let updatedMessages = []; for (let i = 0; i < messages.length; ++i) { if (i === messages.length - 1) { updatedMessages.push(messages[i]); break; } const message = messages[i]; const adjacentMessage = messages[i + 1]; if (message.type === 'user' && adjacentMessage.type === 'user') { const mergedMessage = { type: 'user', items: [...message.items, ...adjacentMessage.items], }; updatedMessages.push(mergedMessage); // Skip the next message. ++i; } else { updatedMessages.push(messages[i]); } } return updatedMessages; } /** * Infers a MIME type from a file ID / filename extension. */ static inferMimeType(fileId) { const ext = fileId.split('.').pop()?.toLowerCase(); switch (ext) { case 'json': return 'application/json'; case 'png': return 'image/png'; case 'jpg': case 'jpeg': return 'image/jpeg'; case 'webm': return 'video/webm'; case 'mp4': return 'video/mp4'; case 'html': return 'text/html'; case 'txt': return 'text/plain'; default: return 'application/octet-stream'; } } } exports.MiscUtils = MiscUtils; MiscUtils.DONOBU_VERSION = MiscUtils.getPackageVersion(); //# sourceMappingURL=MiscUtils.js.map