donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
220 lines • 9.59 kB
JavaScript
;
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