donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
349 lines • 15.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluginLoader = void 0;
const promises_1 = __importDefault(require("node:fs/promises"));
const node_path_1 = __importDefault(require("node:path"));
const node_url_1 = require("node:url");
const playwright = __importStar(require("playwright"));
const Logger_1 = require("../utils/Logger");
const MiscUtils_1 = require("../utils/MiscUtils");
async function getDefaultDependencies() {
let sharpModule;
try {
sharpModule = (await import('sharp')).default;
}
catch {
// sharp is an optional peer dependency — not required unless a plugin needs it.
}
return {
donobu: await import('../main.js'),
playwright: playwright,
sharp: sharpModule,
};
}
/**
* Walk up from this file's directory to find the nearest `node_modules`
* ancestor, then return the `@donobu` scope path within it.
*
* When installed via npm, this file lives at
* `<project>/node_modules/donobu/dist/managers/PluginLoader.js`, so
* walking up finds `<project>/node_modules/@donobu/`.
*
* Returns `undefined` when not inside a `node_modules` tree (e.g. during
* development in the monorepo).
*/
function findNodeModulesDonobuScope() {
let dir = __dirname;
while (dir !== node_path_1.default.dirname(dir)) {
if (node_path_1.default.basename(dir) === 'node_modules') {
return node_path_1.default.join(dir, '@donobu');
}
dir = node_path_1.default.dirname(dir);
}
return undefined;
}
/**
* Resolve the file PluginLoader should dynamic-import for a plugin at
* `pkgDir`, given its (possibly undefined) `package.json` `exports`
* field. Three-tier cascade — see PluginLoader.resolvePlugin for the
* semantic rationale for each tier.
*/
function resolvePluginEntryPath(pkgDir, exportsField) {
if (exportsField && typeof exportsField === 'object') {
const exp = exportsField;
// 1. Subpath export: exports["./plugin"].import
const subpath = exp['./plugin'];
if (subpath && typeof subpath === 'object') {
const subpathImport = subpath.import;
if (typeof subpathImport === 'string') {
return node_path_1.default.resolve(pkgDir, subpathImport);
}
}
// 2. Flat export: exports.import
const flatImport = exp.import;
if (typeof flatImport === 'string') {
return node_path_1.default.resolve(pkgDir, flatImport);
}
}
// 3. Convention fallback.
return node_path_1.default.join(pkgDir, 'index.mjs');
}
/**
* Loads Donobu plugins from two sources:
*
* 1. **App-data plugins directory** — the `plugins/` folder inside the
* Donobu Studio data directory (e.g. `~/Library/Application Support/
* Donobu Studio/plugins/`). Plugins here are standalone `index.mjs`
* files, typically installed via `install-donobu-plugin`.
*
* 2. **`node_modules`** — `@donobu/*` packages installed via npm that
* declare `"donobu-plugin": true` in their `package.json`. This is
* the primary discovery path for SDK users who `npm install` plugins
* alongside the `donobu` package.
*
* When a plugin is found in both sources, the node_modules copy wins —
* an explicit project-level dependency should override global Studio
* state so tests run against the version the project declares.
*/
class PluginLoader {
constructor(pluginsPath, pluginDependencies, nodeModulesScopePath) {
this.pluginsPath = pluginsPath;
this.pluginDependencies = pluginDependencies;
this.nodeModulesScopePath = nodeModulesScopePath;
}
/**
* Creates a PluginLoader with optional configuration parameters.
* If parameters are not provided, defaults will be used.
*/
static async create(pluginsPath, pluginDependencies) {
const finalPluginsPath = pluginsPath ?? node_path_1.default.join(MiscUtils_1.MiscUtils.baseWorkingDirectory(), 'plugins');
const finalDependencies = pluginDependencies ?? (await getDefaultDependencies());
return new PluginLoader(finalPluginsPath, finalDependencies, findNodeModulesDonobuScope());
}
/**
* Discovers and loads all plugins from both the app-data directory and
* `node_modules`. All plugin types are collected and returned.
*/
async loadAllPlugins() {
const dirPlugins = await this.discoverDirectoryPlugins();
const nmPlugins = await this.discoverNodeModulesPlugins();
// Deduplicate: node_modules plugins take precedence over app-data.
// A project that explicitly depends on a plugin version (via the
// install CLI's `--into ./node_modules` or a regular npm install)
// should get that version, not whatever global Studio state
// happens to carry. App-data is the fallback for plugins the project
// hasn't declared.
const seen = new Set(nmPlugins.map((p) => p.name));
const allPlugins = [...nmPlugins];
for (const p of dirPlugins) {
if (!seen.has(p.name)) {
allPlugins.push(p);
seen.add(p.name);
}
}
const tools = [];
const persistencePlugins = new Map();
const gptClientPlugins = [];
const targetRuntimePlugins = [];
for (const plugin of allPlugins) {
Logger_1.appLogger.info(`↳ Loading plugin "${plugin.name}" from ${plugin.entryPoint}`);
try {
const mod = await import((0, node_url_1.pathToFileURL)(plugin.entryPoint).href);
if (!mod) {
continue;
}
// Tool plugins
if (typeof mod.loadCustomTools === 'function') {
const pluginTools = await mod.loadCustomTools(this.pluginDependencies);
tools.push(...pluginTools);
Logger_1.appLogger.info(` ✔ Loaded ${pluginTools.length} tool(s) from plugin "${plugin.name}".`);
pluginTools.forEach((t) => {
Logger_1.appLogger.info(` ↳ Loaded tool "${t.name}"`);
});
}
// Persistence plugins
if (typeof mod.loadPersistencePlugin ===
'function') {
const result = await mod.loadPersistencePlugin(this.pluginDependencies);
if (result) {
persistencePlugins.set(result.key, result.plugin);
Logger_1.appLogger.info(` ✔ Loaded persistence plugin "${result.key}" from "${plugin.name}".`);
}
}
// GPT client plugins
if (typeof mod.loadGptClientPlugins ===
'function') {
const gptPlugins = await mod.loadGptClientPlugins(this.pluginDependencies);
for (const gptPlugin of gptPlugins) {
gptClientPlugins.push(gptPlugin);
Logger_1.appLogger.info(` ✔ Loaded GPT client plugin "${gptPlugin.type}" from "${plugin.name}".`);
}
}
// Target runtime plugins
if (typeof mod.loadTargetRuntimePlugins ===
'function') {
const targetPlugins = await mod
.loadTargetRuntimePlugins(this.pluginDependencies);
for (const targetPlugin of targetPlugins) {
targetRuntimePlugins.push(targetPlugin);
Logger_1.appLogger.info(` ✔ Loaded target runtime plugin "${targetPlugin.type}" from "${plugin.name}".`);
}
}
}
catch (err) {
Logger_1.appLogger.error(`Plugin "${plugin.name}" - failed to load entry point "${node_path_1.default.basename(plugin.entryPoint)}":`, err);
/* swallow – other plugins should still load */
}
}
Logger_1.appLogger.info(`Loaded ${tools.length} custom tool(s) from plugins.`);
return {
tools,
persistencePlugins,
gptClientPlugins,
targetRuntimePlugins,
};
}
// ---------------------------------------------------------------------------
// Plugin resolution
// ---------------------------------------------------------------------------
/**
* Determine which file PluginLoader should dynamic-import for a plugin
* living at `pkgDir`, using a three-tier cascade:
*
* 1. `exports["./plugin"].import` — preferred for dual-use packages
* (those that also expose an SDK surface on the default entry).
* Points at a bundle built from plugin hooks only, with no
* value-level `donobu` imports, so it loads cleanly even from the
* app-data plugins dir where no sibling `donobu` module exists.
* Example: `@donobu/donobu-mobile`.
* 2. `exports.import` — the old flat shape used by plugin-only
* packages (no SDK surface, no value-level `donobu` imports).
* Examples: `tools-captcha`, `tools-mfa`, `donobu-aws`,
* `donobu-google`.
* 3. `<pkgDir>/index.mjs` — convention fallback for hand-dropped
* plugins that ship no `package.json` at all.
*
* `requirePackageJson` toggles whether the caller demands a
* package.json with `"donobu-plugin": true` at the package root
* (node_modules discovery) or is happy with any directory that has an
* `index.mjs` (app-data discovery — supports hand-dropped legacy
* plugins).
*/
async resolvePlugin(pkgDir, requirePackageJson) {
let pkgJson = null;
try {
const raw = await promises_1.default.readFile(node_path_1.default.join(pkgDir, 'package.json'), 'utf-8');
pkgJson = JSON.parse(raw);
}
catch {
if (requirePackageJson) {
return null;
}
// No package.json is fine for the app-data path; the dir is
// treated as a bare `index.mjs` plugin drop (legacy pattern).
}
if (requirePackageJson && !pkgJson?.['donobu-plugin']) {
return null;
}
const entryPoint = resolvePluginEntryPath(pkgDir, pkgJson?.exports);
const name = pkgJson?.name ?? node_path_1.default.basename(pkgDir);
return { name, entryPoint };
}
// ---------------------------------------------------------------------------
// Discovery: app-data plugins directory
// ---------------------------------------------------------------------------
/**
* Scan the app-data plugins directory for standalone plugin bundles.
* Each plugin is a directory containing a `package.json` with
* `"donobu-plugin": true`. Scoped packages (`@scope/name`) are supported
* one level deep.
*/
async discoverDirectoryPlugins() {
Logger_1.appLogger.info(`Scanning "${this.pluginsPath}" for plugins...`);
let dirEntries;
try {
dirEntries = await promises_1.default.readdir(this.pluginsPath, { withFileTypes: true });
}
catch {
dirEntries = [];
}
const plugins = [];
for (const entry of dirEntries.filter((d) => d.isDirectory())) {
if (entry.name.startsWith('@')) {
try {
const scopeDir = node_path_1.default.join(this.pluginsPath, entry.name);
const scopeEntries = await promises_1.default.readdir(scopeDir, {
withFileTypes: true,
});
for (const sub of scopeEntries.filter((d) => d.isDirectory())) {
const resolved = await this.resolvePlugin(node_path_1.default.join(scopeDir, sub.name), false);
if (resolved) {
plugins.push(resolved);
}
}
}
catch {
/* ignore unreadable scope dirs */
}
}
else {
const resolved = await this.resolvePlugin(node_path_1.default.join(this.pluginsPath, entry.name), false);
if (resolved) {
plugins.push(resolved);
}
}
}
return plugins;
}
// ---------------------------------------------------------------------------
// Discovery: node_modules/@donobu/*
// ---------------------------------------------------------------------------
/**
* Scan `node_modules/@donobu/` for npm-installed packages that declare
* `"donobu-plugin": true` in their `package.json`. Delegates the actual
* entry-point resolution to `resolvePlugin` so app-data and
* node_modules installs follow the same rules.
*/
async discoverNodeModulesPlugins() {
if (!this.nodeModulesScopePath) {
return [];
}
let entries;
try {
entries = await promises_1.default.readdir(this.nodeModulesScopePath, {
withFileTypes: true,
});
}
catch {
return [];
}
const plugins = [];
for (const entry of entries.filter((d) => d.isDirectory())) {
const pkgDir = node_path_1.default.join(this.nodeModulesScopePath, entry.name);
const resolved = await this.resolvePlugin(pkgDir, true);
if (resolved) {
plugins.push(resolved);
Logger_1.appLogger.info(`Found npm plugin "${resolved.name}" at ${pkgDir}`);
}
}
return plugins;
}
}
exports.PluginLoader = PluginLoader;
//# sourceMappingURL=PluginLoader.js.map