@sashbot/uibridge
Version:
🤖 AI-friendly live session automation with REAL screenshot backgrounds (no transparency issues!) - control your EXISTING browser with visual debug panel. Perfect for AI agents!
1,502 lines (1,481 loc) • 66.4 kB
JavaScript
var UIBridge = (() => {
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name2 in all)
__defProp(target, name2, { get: all[name2], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.js
var src_exports = {};
__export(src_exports, {
CDIGenerator: () => CDIGenerator,
CommandRegistry: () => CommandRegistry,
SelectorEngine: () => SelectorEngine,
UIBridge: () => UIBridge,
clickCommand: () => clickCommand,
createUIBridge: () => createUIBridge,
default: () => src_default,
initUIBridge: () => initUIBridge,
name: () => name,
screenshotCommand: () => screenshotCommand,
version: () => version
});
// src/core/CommandRegistry.js
var CommandRegistry = class {
constructor() {
this.commands = /* @__PURE__ */ new Map();
}
/**
* Register a new command
* @param {string} name - Command name
* @param {Object} command - Command implementation
*/
register(name2, command) {
if (!name2 || typeof name2 !== "string") {
throw new Error("Command name must be a non-empty string");
}
if (!command || typeof command.execute !== "function") {
throw new Error("Command must have an execute function");
}
const requiredFields = ["name", "description", "parameters"];
for (const field of requiredFields) {
if (!command[field]) {
throw new Error(`Command must have a ${field} property`);
}
}
this.commands.set(name2, {
...command,
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
});
}
/**
* Get a command by name
* @param {string} name - Command name
* @returns {Object|null} Command or null if not found
*/
get(name2) {
return this.commands.get(name2) || null;
}
/**
* Get all registered commands
* @returns {Array} Array of all commands
*/
getAll() {
return Array.from(this.commands.values());
}
/**
* Check if a command exists
* @param {string} name - Command name
* @returns {boolean} True if command exists
*/
has(name2) {
return this.commands.has(name2);
}
/**
* Unregister a command
* @param {string} name - Command name
* @returns {boolean} True if command was removed
*/
unregister(name2) {
return this.commands.delete(name2);
}
/**
* Get command names
* @returns {Array<string>} Array of command names
*/
getNames() {
return Array.from(this.commands.keys());
}
/**
* Clear all commands
*/
clear() {
this.commands.clear();
}
/**
* Get commands count
* @returns {number} Number of registered commands
*/
size() {
return this.commands.size;
}
};
// src/core/SelectorEngine.js
var SelectorEngine = class {
constructor() {
this.strategies = /* @__PURE__ */ new Map();
this._setupDefaultStrategies();
}
/**
* Setup default selector strategies
* @private
*/
_setupDefaultStrategies() {
this.strategies.set("css", (selector) => {
return document.querySelector(selector);
});
this.strategies.set("cssAll", (selector) => {
return Array.from(document.querySelectorAll(selector));
});
this.strategies.set("xpath", (xpath) => {
const result = document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
);
return result.singleNodeValue;
});
this.strategies.set("xpathAll", (xpath) => {
const result = document.evaluate(
xpath,
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
const nodes = [];
for (let i = 0; i < result.snapshotLength; i++) {
nodes.push(result.snapshotItem(i));
}
return nodes;
});
this.strategies.set("text", (text) => {
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
if (node.textContent.trim() === text) {
return node.parentElement;
}
}
return null;
});
this.strategies.set("partialText", (text) => {
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
if (node.textContent.trim().includes(text)) {
return node.parentElement;
}
}
return null;
});
this.strategies.set("testId", (id) => {
return document.querySelector(`[data-testid="${id}"]`);
});
this.strategies.set("dataTest", (id) => {
return document.querySelector(`[data-test="${id}"]`);
});
this.strategies.set("label", (labelText) => {
const labels = document.querySelectorAll("label");
for (const label of labels) {
if (label.textContent.trim() === labelText) {
const forAttr = label.getAttribute("for");
if (forAttr) {
return document.getElementById(forAttr);
}
const input = label.querySelector("input, select, textarea");
if (input) {
return input;
}
}
}
return null;
});
this.strategies.set("placeholder", (placeholder) => {
return document.querySelector(`[placeholder="${placeholder}"]`);
});
this.strategies.set("ariaLabel", (label) => {
return document.querySelector(`[aria-label="${label}"]`);
});
this.strategies.set("role", (role) => {
return document.querySelector(`[role="${role}"]`);
});
}
/**
* Find a single element
* @param {string|Object} selector - Selector configuration
* @returns {Element|null} Found element or null
*/
find(selector) {
if (typeof selector === "string") {
return this.strategies.get("css")(selector);
}
if (typeof selector === "object" && selector !== null) {
const strategies = [
"xpath",
"text",
"partialText",
"testId",
"dataTest",
"label",
"placeholder",
"ariaLabel",
"role",
"css"
];
for (const strategy of strategies) {
if (selector[strategy]) {
const strategyFn = this.strategies.get(strategy);
if (strategyFn) {
const element = strategyFn(selector[strategy]);
if (element) {
return element;
}
}
}
}
}
throw new Error(`Invalid selector: ${JSON.stringify(selector)}`);
}
/**
* Find multiple elements
* @param {string|Object} selector - Selector configuration
* @returns {Array<Element>} Found elements
*/
findAll(selector) {
if (typeof selector === "string") {
return this.strategies.get("cssAll")(selector);
}
if (typeof selector === "object" && selector !== null) {
if (selector.xpath) {
return this.strategies.get("xpathAll")(selector.xpath);
}
if (selector.css) {
return this.strategies.get("cssAll")(selector.css);
}
const element = this.find(selector);
return element ? [element] : [];
}
throw new Error(`Invalid selector: ${JSON.stringify(selector)}`);
}
/**
* Register a custom selector strategy
* @param {string} name - Strategy name
* @param {Function} strategy - Strategy function
*/
registerStrategy(name2, strategy) {
if (typeof strategy !== "function") {
throw new Error("Strategy must be a function");
}
this.strategies.set(name2, strategy);
}
/**
* Check if element is visible
* @param {Element} element - Element to check
* @returns {boolean} True if element is visible
*/
isVisible(element) {
if (!element)
return false;
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" && parseFloat(style.opacity) > 0 && rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
}
/**
* Get element information
* @param {Element} element - Element to analyze
* @returns {Object} Element information
*/
getElementInfo(element) {
if (!element)
return null;
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return {
tag: element.tagName.toLowerCase(),
id: element.id || null,
classes: Array.from(element.classList),
text: element.textContent?.trim().substring(0, 100) || "",
attributes: this._getElementAttributes(element),
position: {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height
},
visible: this.isVisible(element),
focusable: this._isFocusable(element)
};
}
/**
* Get element attributes
* @param {Element} element - Element to analyze
* @returns {Object} Element attributes
* @private
*/
_getElementAttributes(element) {
const attrs = {};
for (const attr of element.attributes) {
attrs[attr.name] = attr.value;
}
return attrs;
}
/**
* Check if element is focusable
* @param {Element} element - Element to check
* @returns {boolean} True if element is focusable
* @private
*/
_isFocusable(element) {
const focusableTags = ["input", "select", "textarea", "button", "a"];
return focusableTags.includes(element.tagName.toLowerCase()) || element.hasAttribute("tabindex") || element.hasAttribute("contenteditable");
}
};
// src/discovery/CDIGenerator.js
var CDIGenerator = class {
constructor(registry) {
this.registry = registry;
this.version = "1.0.0";
}
/**
* Generate markdown documentation
* @returns {string} Markdown documentation
*/
generateMarkdown() {
const commands = this.registry.getAll();
const date = (/* @__PURE__ */ new Date()).toISOString();
let markdown = `# UIBridge Commands Documentation
`;
markdown += `**Generated:** ${date}
`;
markdown += `**Version:** ${this.version}
`;
markdown += `**Total Commands:** ${commands.length}
`;
markdown += `## Command Summary
`;
markdown += `| Command | Description | Parameters |
`;
markdown += `|---------|-------------|------------|
`;
commands.forEach((cmd) => {
const params = cmd.parameters.map(
(p) => `${p.name}${p.required ? "" : "?"}`
).join(", ");
markdown += `| **${cmd.name}** | ${cmd.description} | ${params || "None"} |
`;
});
markdown += `
## Command Details
`;
commands.forEach((cmd) => {
markdown += `### ${cmd.name}
`;
markdown += `${cmd.description}
`;
if (cmd.parameters.length > 0) {
markdown += `**Parameters:**
`;
cmd.parameters.forEach((param) => {
const required = param.required ? "**required**" : "*optional*";
markdown += `- \`${param.name}\` (${param.type}) - ${required}
`;
markdown += ` ${param.description}
`;
});
markdown += "\n";
}
if (cmd.examples && cmd.examples.length > 0) {
markdown += `**Examples:**
`;
cmd.examples.forEach((example) => {
markdown += `\`\`\`javascript
${example}
\`\`\`
`;
});
}
markdown += `---
`;
});
return markdown;
}
/**
* Generate JSON schema for commands
* @returns {Object} JSON schema
*/
generateJSON() {
const commands = this.registry.getAll();
return {
version: this.version,
generated: (/* @__PURE__ */ new Date()).toISOString(),
commands: commands.map((cmd) => ({
name: cmd.name,
description: cmd.description,
parameters: cmd.parameters,
examples: cmd.examples || []
}))
};
}
/**
* Save documentation to file
* @param {string} format - Format (markdown, json)
*/
async saveToFile(format = "markdown") {
const content = format === "json" ? JSON.stringify(this.generateJSON(), null, 2) : this.generateMarkdown();
const blob = new Blob([content], {
type: format === "json" ? "application/json" : "text/markdown"
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `uibridge-commands.${format === "json" ? "json" : "md"}`;
a.click();
URL.revokeObjectURL(url);
}
/**
* Generate TypeScript definitions
* @returns {string} TypeScript definitions
*/
generateTypeScript() {
const commands = this.registry.getAll();
let ts = `// UIBridge Command Definitions
`;
ts += `// Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
`;
commands.forEach((cmd) => {
const optionalParams = cmd.parameters.filter((p) => !p.required);
if (optionalParams.length > 0) {
ts += `interface ${this._capitalize(cmd.name)}Options {
`;
optionalParams.forEach((param) => {
ts += ` ${param.name}?: ${this._mapTypeToTS(param.type)}; // ${param.description}
`;
});
ts += `}
`;
}
});
ts += `interface UIBridge {
`;
commands.forEach((cmd) => {
const requiredParams = cmd.parameters.filter((p) => p.required);
const optionalParams = cmd.parameters.filter((p) => !p.required);
let signature = ` ${cmd.name}(`;
requiredParams.forEach((param, index) => {
if (index > 0)
signature += ", ";
signature += `${param.name}: ${this._mapTypeToTS(param.type)}`;
});
if (optionalParams.length > 0) {
if (requiredParams.length > 0)
signature += ", ";
signature += `options?: ${this._capitalize(cmd.name)}Options`;
}
signature += `): Promise<any>; // ${cmd.description}
`;
ts += signature;
});
ts += `}
`;
ts += `export { UIBridge };
`;
commands.forEach((cmd) => {
const optionalParams = cmd.parameters.filter((p) => !p.required);
if (optionalParams.length > 0) {
ts += `export { ${this._capitalize(cmd.name)}Options };
`;
}
});
return ts;
}
/**
* Generate OpenAPI specification
* @returns {Object} OpenAPI spec
*/
generateOpenAPI() {
const commands = this.registry.getAll();
const spec = {
openapi: "3.0.0",
info: {
title: "UIBridge API",
description: "In-app automation framework for web applications",
version: this.version,
contact: {
name: "UIBridge Team"
}
},
servers: [
{
url: "http://localhost:3000",
description: "Local development server"
}
],
paths: {},
components: {
schemas: {}
}
};
commands.forEach((cmd) => {
const path = `/commands/${cmd.name}`;
spec.paths[path] = {
post: {
summary: cmd.description,
description: cmd.description,
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
properties: this._generateJSONSchema(cmd.parameters),
required: cmd.parameters.filter((p) => p.required).map((p) => p.name)
}
}
}
},
responses: {
"200": {
description: "Command executed successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean" },
result: { type: "object" },
timestamp: { type: "string", format: "date-time" }
}
}
}
}
},
"400": {
description: "Invalid parameters"
},
"500": {
description: "Command execution failed"
}
}
}
};
});
return spec;
}
/**
* Get live command statistics
* @returns {Object} Command statistics
*/
getStatistics() {
const commands = this.registry.getAll();
return {
totalCommands: commands.length,
commandNames: commands.map((c) => c.name),
totalParameters: commands.reduce((sum, cmd) => sum + cmd.parameters.length, 0),
requiredParameters: commands.reduce((sum, cmd) => sum + cmd.parameters.filter((p) => p.required).length, 0),
optionalParameters: commands.reduce((sum, cmd) => sum + cmd.parameters.filter((p) => !p.required).length, 0),
commandsWithExamples: commands.filter((cmd) => cmd.examples && cmd.examples.length > 0).length,
averageParametersPerCommand: Math.round(
commands.reduce((sum, cmd) => sum + cmd.parameters.length, 0) / commands.length * 100
) / 100,
lastGenerated: (/* @__PURE__ */ new Date()).toISOString()
};
}
/**
* Helper: Capitalize string
* @param {string} str - String to capitalize
* @returns {string} Capitalized string
* @private
*/
_capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Helper: Map parameter type to TypeScript type
* @param {string} type - Parameter type
* @returns {string} TypeScript type
* @private
*/
_mapTypeToTS(type) {
const typeMap = {
"string": "string",
"number": "number",
"boolean": "boolean",
"object": "object",
"array": "any[]",
"Selector": "string | object",
"ClickOptions": "object",
"ScreenshotOptions": "object"
};
return typeMap[type] || "any";
}
/**
* Helper: Generate JSON schema for parameters
* @param {Array} parameters - Command parameters
* @returns {Object} JSON schema properties
* @private
*/
_generateJSONSchema(parameters) {
const properties = {};
parameters.forEach((param) => {
properties[param.name] = {
type: this._mapTypeToJSONSchema(param.type),
description: param.description
};
if (param.default !== void 0) {
properties[param.name].default = param.default;
}
});
return properties;
}
/**
* Helper: Map parameter type to JSON Schema type
* @param {string} type - Parameter type
* @returns {string} JSON Schema type
* @private
*/
_mapTypeToJSONSchema(type) {
const typeMap = {
"string": "string",
"number": "number",
"boolean": "boolean",
"object": "object",
"array": "array",
"Selector": "string",
// Simplified for JSON schema
"ClickOptions": "object",
"ScreenshotOptions": "object"
};
return typeMap[type] || "string";
}
};
// src/commands/click.js
var clickCommand = {
name: "click",
description: "Clicks on an element using synthetic mouse events",
examples: [
"execute('click', '#submit-button')",
"execute('click', { text: 'Submit' })",
"execute('click', { testId: 'login-btn' })",
"execute('click', '#button', { position: 'center', clickCount: 2 })"
],
parameters: [
{
name: "selector",
type: "Selector",
required: true,
description: "Element to click (string, CSS selector, or selector object)"
},
{
name: "options",
type: "ClickOptions",
required: false,
description: "Click options: { force?, position?, button?, clickCount?, delay? }"
}
],
async execute(bridge, selector, options = {}) {
const element = bridge.findElement(selector);
if (!element) {
throw new Error(`Element not found: ${JSON.stringify(selector)}`);
}
const opts = {
force: false,
position: "center",
// center, topLeft, topRight, bottomLeft, bottomRight
button: "left",
// left, right, middle
clickCount: 1,
delay: 0,
scrollIntoView: true,
...options
};
bridge._log(`Clicking element: ${bridge.selectorEngine.getElementInfo(element)?.tag || "unknown"}`);
if (opts.scrollIntoView) {
element.scrollIntoView({ behavior: "smooth", block: "center" });
await new Promise((resolve) => setTimeout(resolve, 100));
}
if (!opts.force) {
const isVisible = bridge.selectorEngine.isVisible(element);
if (!isVisible) {
throw new Error("Element is not visible. Use { force: true } to click anyway.");
}
}
if (!opts.force) {
const isActionable = this._isElementActionable(element);
if (!isActionable) {
throw new Error("Element is covered by another element. Use { force: true } to click anyway.");
}
}
const rect = element.getBoundingClientRect();
const position = this._calculatePosition(rect, opts.position);
const eventInit = {
bubbles: true,
cancelable: true,
view: window,
clientX: position.x,
clientY: position.y,
button: this._getButtonCode(opts.button),
buttons: this._getButtonsCode(opts.button),
detail: opts.clickCount
};
try {
element.dispatchEvent(new MouseEvent("mouseover", eventInit));
element.dispatchEvent(new MouseEvent("mouseenter", eventInit));
element.dispatchEvent(new MouseEvent("mousedown", eventInit));
if (opts.delay > 0) {
await new Promise((resolve) => setTimeout(resolve, opts.delay));
}
element.dispatchEvent(new MouseEvent("mouseup", eventInit));
for (let i = 0; i < opts.clickCount; i++) {
if (i > 0) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
element.dispatchEvent(new MouseEvent("click", {
...eventInit,
detail: i + 1
}));
}
if (bridge.selectorEngine._isFocusable(element)) {
element.focus();
}
await this._handleSpecialElements(element, opts);
} catch (error) {
throw new Error(`Failed to click element: ${error.message}`);
}
return {
success: true,
element: bridge.selectorEngine.getElementInfo(element),
position,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
};
},
/**
* Check if element is actionable (not covered by another element)
* @param {Element} element - Element to check
* @returns {boolean} True if element is actionable
* @private
*/
_isElementActionable(element) {
const rect = element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const elementAtPoint = document.elementFromPoint(centerX, centerY);
return element === elementAtPoint || element.contains(elementAtPoint);
},
/**
* Calculate click position based on position option
* @param {DOMRect} rect - Element bounding rectangle
* @param {string} position - Position option
* @returns {Object} Coordinates {x, y}
* @private
*/
_calculatePosition(rect, position) {
const positions = {
center: {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
},
topLeft: {
x: rect.left + 1,
y: rect.top + 1
},
topRight: {
x: rect.right - 1,
y: rect.top + 1
},
bottomLeft: {
x: rect.left + 1,
y: rect.bottom - 1
},
bottomRight: {
x: rect.right - 1,
y: rect.bottom - 1
},
topCenter: {
x: rect.left + rect.width / 2,
y: rect.top + 1
},
bottomCenter: {
x: rect.left + rect.width / 2,
y: rect.bottom - 1
},
leftCenter: {
x: rect.left + 1,
y: rect.top + rect.height / 2
},
rightCenter: {
x: rect.right - 1,
y: rect.top + rect.height / 2
}
};
return positions[position] || positions.center;
},
/**
* Get mouse button code
* @param {string} button - Button name
* @returns {number} Button code
* @private
*/
_getButtonCode(button) {
const buttons = {
left: 0,
middle: 1,
right: 2
};
return buttons[button] || 0;
},
/**
* Get mouse buttons bitmask
* @param {string} button - Button name
* @returns {number} Buttons bitmask
* @private
*/
_getButtonsCode(button) {
const buttons = {
left: 1,
middle: 4,
right: 2
};
return buttons[button] || 1;
},
/**
* Handle special element types (forms, checkboxes, etc.)
* @param {Element} element - Element that was clicked
* @param {Object} opts - Click options
* @private
*/
async _handleSpecialElements(element, opts) {
const tagName = element.tagName.toLowerCase();
const inputType = element.type?.toLowerCase();
if (tagName === "button" && element.type === "submit") {
const form = element.closest("form");
if (form) {
return;
}
}
if (tagName === "input" && (inputType === "checkbox" || inputType === "radio")) {
return;
}
if (tagName === "select") {
setTimeout(() => {
element.dispatchEvent(new Event("change", { bubbles: true }));
}, 10);
}
if (tagName === "a" && element.href) {
return;
}
}
};
// src/commands/screenshot.js
var screenshotCommand = {
name: "screenshot",
description: "Takes a screenshot of the page or a specific element",
examples: [
"execute('screenshot')",
"execute('screenshot', { format: 'png', quality: 0.9 })",
"execute('screenshot', { selector: '#main-content' })",
"execute('screenshot', { fullPage: true, saveConfig: { autoSave: true, folder: 'tests' } })"
],
parameters: [
{
name: "options",
type: "ScreenshotOptions",
required: false,
description: "Screenshot options: { selector?, format?, quality?, fullPage?, saveConfig? }"
}
],
async execute(bridge, options = {}) {
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Starting screenshot command execution");
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Raw options received:", JSON.stringify(options, null, 2));
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Bridge config:", JSON.stringify(bridge.config.defaultScreenshotConfig, null, 2));
const opts = {
selector: null,
format: "png",
// png, jpeg, webp
quality: 0.92,
// 0-1 for jpeg/webp
fullPage: false,
// capture entire page
excludeSelectors: [],
// elements to hide during capture
backgroundColor: null,
// background color override
scale: window.devicePixelRatio || 1,
// Enhanced save configuration
saveConfig: {
// Use bridge default config as base
...bridge.config.defaultScreenshotConfig,
// Override with user options
...options.saveConfig
},
...options
};
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Final processed options:", JSON.stringify(opts, null, 2));
bridge._log(`Taking screenshot with options:`, opts);
let targetElement = document.body;
if (opts.selector) {
targetElement = bridge.findElement(opts.selector);
if (!targetElement) {
throw new Error(`Element not found for screenshot: ${JSON.stringify(opts.selector)}`);
}
}
try {
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Target element:", targetElement?.tagName, targetElement?.id, targetElement?.className);
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Target element dimensions:", {
width: targetElement?.offsetWidth,
height: targetElement?.offsetHeight,
scrollWidth: targetElement?.scrollWidth,
scrollHeight: targetElement?.scrollHeight
});
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Loading html2canvas...");
await this._ensureHtml2Canvas();
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] html2canvas loaded:", !!window.html2canvas);
const hiddenElements = this._hideElements(opts.excludeSelectors);
const html2canvasOptions = {
useCORS: true,
allowTaint: false,
backgroundColor: opts.backgroundColor,
scale: opts.scale,
logging: true,
// Force logging for debugging
width: opts.fullPage ? document.documentElement.scrollWidth : void 0,
height: opts.fullPage ? document.documentElement.scrollHeight : void 0,
windowWidth: opts.fullPage ? document.documentElement.scrollWidth : void 0,
windowHeight: opts.fullPage ? document.documentElement.scrollHeight : void 0,
x: opts.fullPage ? 0 : void 0,
y: opts.fullPage ? 0 : void 0,
// Improve image quality
foreignObjectRendering: true,
imageTimeout: 15e3,
removeContainer: true
};
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] html2canvas options:", JSON.stringify(html2canvasOptions, null, 2));
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Starting html2canvas capture...");
const canvas = await window.html2canvas(targetElement, html2canvasOptions);
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Canvas created:", {
width: canvas.width,
height: canvas.height,
hasData: canvas.getContext("2d").getImageData(0, 0, 1, 1).data.some((x) => x !== 0)
});
this._restoreElements(hiddenElements);
const mimeType = `image/${opts.format}`;
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Converting to format:", mimeType, "quality:", opts.quality);
const dataUrl = canvas.toDataURL(mimeType, opts.quality);
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] DataURL created, length:", dataUrl.length, "starts with:", dataUrl.substring(0, 50));
const fileName = this._generateFileName(opts);
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Generated filename:", fileName);
if (opts.saveConfig.autoSave) {
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Auto-save enabled, saving...");
await this._saveScreenshot(dataUrl, fileName, opts.saveConfig);
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Save completed");
} else {
console.log("\u{1F5BC}\uFE0F [SCREENSHOT] Auto-save disabled");
}
const result = {
success: true,
dataUrl,
width: canvas.width,
height: canvas.height,
format: opts.format,
fileName,
filePath: opts.saveConfig.folder ? `${opts.saveConfig.folder}/${fileName}` : fileName,
size: Math.round(dataUrl.length * 0.75),
// Approximate file size in bytes
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
saveConfig: opts.saveConfig
};
if (opts.selector) {
result.element = bridge.selectorEngine.getElementInfo(targetElement);
if (opts.saveConfig.includeMetadata) {
result.metadata = {
selector: opts.selector,
element: result.element,
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
userAgent: navigator.userAgent,
timestamp: result.timestamp
};
}
}
bridge._log(`Screenshot captured: ${result.width}x${result.height}, ${result.size} bytes, saved as: ${result.filePath}`);
return result;
} catch (error) {
throw new Error(`Failed to take screenshot: ${error.message}`);
}
},
/**
* Generate filename based on configuration
* @param {Object} opts - Screenshot options
* @returns {string} Generated filename
* @private
*/
_generateFileName(opts) {
const config = opts.saveConfig;
if (config.customName) {
return this._ensureExtension(config.customName, opts.format);
}
let fileName = config.prefix || "screenshot";
if (config.includeMetadata) {
if (opts.selector) {
const selectorStr = typeof opts.selector === "string" ? opts.selector.replace(/[#.]/g, "").substring(0, 20) : "element";
fileName += `_${selectorStr}`;
}
if (opts.fullPage) {
fileName += "_fullpage";
}
fileName += `_${opts.width || "auto"}x${opts.height || "auto"}`;
}
if (config.timestamp) {
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").substring(0, 19);
fileName += `_${timestamp}`;
}
return this._ensureExtension(fileName, opts.format);
},
/**
* Ensure filename has correct extension
* @param {string} fileName - Base filename
* @param {string} format - Image format
* @returns {string} Filename with extension
* @private
*/
_ensureExtension(fileName, format) {
const extension = format === "jpeg" ? "jpg" : format;
if (!fileName.toLowerCase().endsWith(`.${extension}`)) {
return `${fileName}.${extension}`;
}
return fileName;
},
/**
* Save screenshot using available methods
* @param {string} dataUrl - Image data URL
* @param {string} fileName - File name
* @param {Object} saveConfig - Save configuration
* @private
*/
async _saveScreenshot(dataUrl, fileName, saveConfig) {
try {
await this._downloadImage(dataUrl, fileName);
if (saveConfig.serverEndpoint) {
await this._saveToServer(dataUrl, fileName, saveConfig);
}
if (saveConfig.persistInBrowser) {
await this._saveToIndexedDB(dataUrl, fileName, saveConfig);
}
} catch (error) {
console.warn("Failed to save screenshot:", error);
throw new Error(`Screenshot save failed: ${error.message}`);
}
},
/**
* Save to server endpoint (if configured)
* @param {string} dataUrl - Image data URL
* @param {string} fileName - File name
* @param {Object} saveConfig - Save configuration
* @private
*/
async _saveToServer(dataUrl, fileName, saveConfig) {
if (!saveConfig.serverEndpoint)
return;
try {
const response = await fetch(saveConfig.serverEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
fileName,
folder: saveConfig.folder,
dataUrl,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
})
});
if (!response.ok) {
throw new Error(`Server save failed: ${response.status} ${response.statusText}`);
}
console.log(`Screenshot saved to server: ${fileName}`);
} catch (error) {
console.error("Server save error:", error);
throw error;
}
},
/**
* Save to IndexedDB for browser persistence
* @param {string} dataUrl - Image data URL
* @param {string} fileName - File name
* @param {Object} saveConfig - Save configuration
* @private
*/
async _saveToIndexedDB(dataUrl, fileName, saveConfig) {
return new Promise((resolve, reject) => {
const dbName = "UIBridgeScreenshots";
const storeName = "screenshots";
const request = indexedDB.open(dbName, 1);
request.onerror = () => reject(new Error("IndexedDB open failed"));
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
const store = db.createObjectStore(storeName, { keyPath: "id", autoIncrement: true });
store.createIndex("fileName", "fileName", { unique: false });
store.createIndex("timestamp", "timestamp", { unique: false });
}
};
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction([storeName], "readwrite");
const store = transaction.objectStore(storeName);
const screenshot = {
fileName,
folder: saveConfig.folder,
dataUrl,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
size: Math.round(dataUrl.length * 0.75)
};
const addRequest = store.add(screenshot);
addRequest.onsuccess = () => {
console.log(`Screenshot saved to IndexedDB: ${fileName}`);
resolve();
};
addRequest.onerror = () => {
reject(new Error("IndexedDB save failed"));
};
};
});
},
/**
* Ensure html2canvas library is loaded
* @private
*/
async _ensureHtml2Canvas() {
if (window.html2canvas)
return;
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = "https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js";
script.integrity = "sha512-dK1lSuLiS6pQ6nrGT7iQFmQ5xOFCHBcynHgSc1h5tEGE6a86/30XnRrOXKmr5AZ+z3OqQQ4SdMzS0i1h1D5w3g==";
script.crossOrigin = "anonymous";
script.onload = () => {
if (window.html2canvas) {
resolve();
} else {
reject(new Error("html2canvas failed to load properly"));
}
};
script.onerror = () => {
reject(new Error("Failed to load html2canvas library"));
};
const existingScript = document.querySelector('script[src*="html2canvas"]');
if (existingScript) {
if (window.html2canvas) {
resolve();
} else {
existingScript.onload = resolve;
existingScript.onerror = reject;
}
return;
}
document.head.appendChild(script);
});
},
/**
* Hide elements temporarily
* @param {Array<string>} selectors - CSS selectors to hide
* @returns {Array} Array of elements that were hidden
* @private
*/
_hideElements(selectors) {
const hiddenElements = [];
for (const selector of selectors) {
try {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
const originalDisplay = element.style.display;
element.style.display = "none";
hiddenElements.push({ element, originalDisplay });
}
} catch (error) {
console.warn(`Invalid selector for hiding: ${selector}`, error);
}
}
return hiddenElements;
},
/**
* Restore previously hidden elements
* @param {Array} hiddenElements - Elements to restore
* @private
*/
_restoreElements(hiddenElements) {
for (const { element, originalDisplay } of hiddenElements) {
element.style.display = originalDisplay;
}
},
/**
* Download the image
* @param {string} dataUrl - Image data URL
* @param {string} fileName - File name
* @private
*/
_downloadImage(dataUrl, fileName) {
try {
const link = document.createElement("a");
link.download = fileName;
link.href = dataUrl;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
console.warn("Failed to auto-download screenshot:", error);
}
}
};
// src/commands/help.js
var helpCommand = {
name: "help",
description: "Get help information about UIBridge commands and usage patterns for AI automation",
examples: [
"execute('help')",
"execute('help', 'click')",
"execute('help', 'screenshot')",
"execute('--help')"
],
parameters: [
{
name: "commandName",
type: "string",
required: false,
description: "Specific command to get help for (optional)"
}
],
async execute(bridge, commandName = null) {
return bridge.getHelp(commandName);
}
};
// src/core/UIBridge.js
var UIBridge = class _UIBridge {
constructor(config = {}) {
this.config = {
debug: false,
allowedOrigins: ["*"],
commands: ["click", "screenshot", "help"],
generateCDI: true,
enableHttpDiscovery: false,
autoInit: true,
version: "1.0.0",
// Screenshot save configuration
defaultScreenshotConfig: {
autoSave: false,
folder: "uibridge-screenshots",
prefix: "screenshot",
timestamp: true,
includeMetadata: false,
persistInBrowser: false,
serverEndpoint: null,
// Optional server endpoint for saving
...config.defaultScreenshotConfig
},
...config
};
this.registry = new CommandRegistry();
this.selectorEngine = new SelectorEngine();
this.cdiGenerator = null;
this._isInitialized = false;
this._initStartTime = null;
this._commandHistory = [];
if (this.config.autoInit && typeof window !== "undefined" && typeof document !== "undefined") {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => this.init());
} else {
setTimeout(() => this.init(), 0);
}
}
}
/**
* Initialize UIBridge
* @returns {Promise<void>}
*/
async init() {
if (this._isInitialized) {
this._log("UIBridge already initialized");
return;
}
this._initStartTime = typeof performance !== "undefined" ? performance.now() : Date.now();
this._log("Initializing UIBridge...", this.config);
try {
await this._registerCoreCommands();
this.cdiGenerator = new CDIGenerator(this.registry);
this._setupDiscovery();
this._setupGlobalAPI();
this._isInitialized = true;
const initTime = (typeof performance !== "undefined" ? performance.now() : Date.now()) - this._initStartTime;
this._log(`UIBridge initialized successfully in ${initTime.toFixed(2)}ms`, {
commands: this.registry.getNames(),
version: this.config.version
});
if (this.config.generateCDI) {
this._generateCDI();
}
this._dispatchEvent("uibridge:initialized", {
version: this.config.version,
commands: this.registry.getNames(),
initTime
});
} catch (error) {
this._log("Failed to initialize UIBridge:", error);
throw new Error(`UIBridge initialization failed: ${error.message}`);
}
}
/**
* Execute a command
* @param {string} commandName - Name of the command to execute
* @param {...any} args - Command arguments
* @returns {Promise<any>} Command result
*/
async execute(commandName, ...args) {
if (commandName === "help" || commandName === "--help") {
return this.getHelp(args[0]);
}
if (!this._isInitialized) {
throw new Error("UIBridge not initialized. Call init() first.");
}
const command = this.registry.get(commandName);
if (!command) {
const available = this.registry.getAll().map((cmd) => cmd.name).join(", ");
throw new Error(`Unknown command: ${commandName}. Available commands: ${available}. Use 'help' for detailed information.`);
}
const startTime = typeof performance !== "undefined" ? performance.now() : Date.now();
const executionId = this._generateExecutionId();
this._log(`Executing command: ${commandName}`, { args, executionId });
try {
const historyEntry = {
id: executionId,
command: commandName,
args,
startTime: (/* @__PURE__ */ new Date()).toISOString(),
status: "running"
};
this._commandHistory.push(historyEntry);
const result = await command.execute(this, ...args);
const endTime = typeof performance !== "undefined" ? performance.now() : Date.now();
const duration = endTime - startTime;
historyEntry.status = "completed";
historyEntry.duration = duration;
historyEntry.result = result;
historyEntry.endTime = (/* @__PURE__ */ new Date()).toISOString();
this._log(`Command completed: ${commandName} (${duration.toFixed(2)}ms)`, result);
this._dispatchEvent("uibridge:command", {
command: commandName,
args,
result,
duration,
executionId
});
const enhancedResult = {
...result,
command: commandName,
duration,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
};
this._addToHistory({
command: commandName,
args,
result: enhancedResult,
duration,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
status: "completed"
});
return enhancedResult;
} catch (error) {
const endTime = typeof performance !== "undefined" ? performance.now() : Date.now();
const duration = endTime - startTime;
const historyEntry = this._commandHistory[this._commandHistory.length - 1];
if (historyEntry && historyEntry.id === executionId) {
historyEntry.status = "failed";
historyEntry.error = error.message;
historyEntry.duration = duration;
historyEntry.endTime = (/* @__PURE__ */ new Date()).toISOString();
}
this._log(`Command failed: ${commandName} (${duration.toFixed(2)}ms)`, error);
this._dispatchEvent("uibridge:error", {
command: commandName,
args,
error: error.message,
duration,
executionId
});
this._addToHistory({
command: commandName,
args,
error: error.message,
duration,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
status: "failed"
});
throw error;
}
}
/**
* Find an element using the selector engine
* @param {string|Object} selector - Selector to find element
* @returns {Element|null} Found element
*/
findElement(selector) {
return this.selectorEngine.find(selector);
}
/**
* Find multiple elements using the selector engine
* @param {string|Object} selector - Selector to find elements
* @returns {Array<Element>} Found elements
*/
findElements(selector) {
return this.selectorEngine.findAll(selector);
}
/**
* Get command discovery information
* @returns {Array} Array of command information
*/
discover() {
return this.registry.getAll().map((cmd) => ({
name: cmd.name,
description: cmd.description,
parameters: cmd.parameters,
examples: cmd.examples || []
}));
}
/**
* Get command execution history
* @param {number} limit - Maximum number of entries to return
* @returns {Array} Command history
*/
getHistory(limit = 50) {
return this._commandHistory.slice(-limit);
}
/**
* Clear command history
*/
clearHistory() {
this._commandHistory = [];
this._log("Command history cleared");
}
/**
* Get UIBridge status and statistics
* @returns {Object} Status information
*/
getStatus() {
return {
initialized: this._isInitialized,
version: this.config.version,
commands: this.regi