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