create-donobu-plugin 1.2.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +173 -65
  2. package/index.js +295 -193
  3. package/package.json +31 -22
package/README.md CHANGED
@@ -1,99 +1,191 @@
1
1
  # create-donobu-plugin
2
2
 
3
- `create-donobu-plugin` is the official scaffolding CLI for Donobu Studio plugins. It generates a TypeScript workspace wired to Donobus plugin API, pins dependencies to whatever versions your local Studio installation already uses, and ships with helper utilities so you can focus on describing new tools instead of wiring.
3
+ `create-donobu-plugin` is the official scaffolding CLI for Donobu Studio plugins. It generates a TypeScript project wired to Donobu's plugin API and pins dependencies to whatever versions your local Donobu installation already uses.
4
4
 
5
5
  ## How Donobu loads plugins
6
6
 
7
- - Donobu watches a plugins directory inside its working data folder (macOS: `~/Library/Application Support/Donobu Studio/plugins`, Windows: `%APPDATA%/Donobu Studio/plugins`, Linux: `~/.config/Donobu Studio/plugins`).
8
- - Each plugin ships a bundled `dist/index.mjs` that exports one async function named `loadCustomTools(dependencies)`. When Donobu starts it imports every plugin bundle and calls that function to collect tools.
9
- - `npm exec install-donobu-plugin` builds your project and copies the resulting `dist/` folder into `<workingDir>/plugins/<plugin-name>`, so restarting Donobu is enough to pick up changes.
7
+ Donobu watches a plugins directory inside its working data folder:
10
8
 
11
- Keep those three rules in mind and your plugin will load reliably.
9
+ | Platform | Path |
10
+ | -------- | ------------------------------------------------------ |
11
+ | macOS | `~/Library/Application Support/Donobu Studio/plugins/` |
12
+ | Windows | `%APPDATA%/Donobu Studio/plugins/` |
13
+ | Linux | `~/.config/Donobu Studio/plugins/` |
14
+
15
+ Each plugin ships a bundled `dist/index.mjs`. On startup Donobu imports every plugin and calls the exported functions it finds:
16
+
17
+ - **`loadCustomTools(deps)`** — returns an array of `Tool` instances.
18
+ - **`loadPersistencePlugin(deps)`** — returns `{ key, plugin }` to register a persistence backend, or `null` to skip.
19
+
20
+ A single plugin may export both functions. Scoped package names (e.g., `@myorg/my-plugin`) are supported.
21
+
22
+ `npm exec install-donobu-plugin` builds your project and copies `dist/` into the plugins directory. Restarting Donobu is enough to pick up changes.
12
23
 
13
24
  ## Prerequisites
14
25
 
15
- - Node.js 18+ and npm 8+
26
+ - Node.js 20+ and a package manager (npm 8+, pnpm 10+, or yarn)
16
27
  - A local Donobu Studio installation (desktop or backend) so the installer has somewhere to copy the plugin bundle
17
- - Playwright browsers installed (Donobu will prompt you if they are missing)
18
28
  - Write access to the Donobu Studio working directory
19
29
 
20
- ## Installation
30
+ ## Quick start
31
+
32
+ ### Tool plugin (default)
21
33
 
22
34
  ```bash
23
35
  npx create-donobu-plugin my-support-tools
36
+ cd my-support-tools && npm install
37
+ # Edit src/index.ts to add your custom tools
38
+ npm run build
39
+ npm exec install-donobu-plugin
40
+ # Restart Donobu Studio
24
41
  ```
25
42
 
26
- Provide the plugin name as the first argument. Names may include lowercase letters, numbers, hyphens, and underscores. The CLI prints usage information if the argument is missing or invalid.
43
+ ### Persistence plugin
27
44
 
28
- ## Quick start
45
+ ```bash
46
+ npx create-donobu-plugin my-storage --type persistence
47
+ cd my-storage && npm install
48
+ # Edit src/index.ts to implement your persistence plugin
49
+ npm run build
50
+ npm exec install-donobu-plugin
51
+ # Set PERSISTENCE_PRIORITY to include your plugin key, then restart Donobu
52
+ ```
29
53
 
30
- 1. Scaffold a plugin: `npx create-donobu-plugin my-support-tools`
31
- 2. `cd my-support-tools && npm install`
32
- 3. Implement your tools in `src/index.ts`
33
- 4. `npm run build`
34
- 5. `npm exec install-donobu-plugin`
35
- 6. Restart Donobu Studio and verify the tools appear in the UI/logs
54
+ Plugin names may include lowercase letters, numbers, hyphens, and underscores. The CLI prints usage information if the argument is missing or invalid.
36
55
 
37
56
  ## Generated project layout
38
57
 
39
- | Item | Description |
40
- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
- | `package.json` | Private ESM package whose `main`/`types` fields point to `dist/`. Dev dependencies are aligned with the local Donobu install so you don’t chase version drift. |
42
- | `tsconfig.json` | NodeNext compiler settings that emit JS + declarations into `dist/`. |
43
- | `esbuild.config.mjs` | Bundles the compiled JS into `dist/index.mjs`, keeping Donobu and Playwright as external dependencies. |
44
- | `src/index.ts` | Entry point that must export `loadCustomTools(dependencies)` and return an array of tools. |
45
- | `src/createTool.ts` | Helper around `dependencies.donobu.Tool` that reduces boilerplate when defining tools. |
46
- | `src/PluginDependencies.ts` | Strongly typed view of the objects Donobu injects into your plugin (`donobu` and `playwright`). |
47
- | `README.md` | Template instructions for the team that owns the generated plugin. |
58
+ | Item | Description |
59
+ | -------------------- | ----------------------------------------------------------------------------------------------- |
60
+ | `package.json` | Private package with `"donobu-plugin": true`. `donobu` is a peer/dev dependency, never bundled. |
61
+ | `tsconfig.json` | ES2021 + NodeNext compiler settings that emit declarations into `dist/`. |
62
+ | `esbuild.config.mjs` | Bundles TypeScript source into `dist/index.mjs`, keeping `donobu` external. |
63
+ | `src/index.ts` | Entry point. Exports `loadCustomTools(deps)` and/or `loadPersistencePlugin(deps)`. |
64
+ | `README.md` | Template instructions for the team that owns the generated plugin. |
65
+
66
+ ## The deps pattern
67
+
68
+ Plugins receive all runtime values through the `deps` parameter — **never use value imports from `donobu`**. Only `import type` is allowed because type imports are erased at compile time, while value imports would produce an `import ... from "donobu"` statement in the bundle that Node cannot resolve from the plugins directory.
69
+
70
+ ```ts
71
+ // CORRECT — type imports are erased, runtime values come from deps
72
+ import type { Tool, PluginDependencies } from 'donobu';
73
+
74
+ export async function loadCustomTools(
75
+ deps: PluginDependencies,
76
+ ): Promise<Tool<any, any>[]> {
77
+ const logger = deps.donobu.appLogger; // runtime access via deps
78
+ // ...
79
+ }
80
+ ```
81
+
82
+ ```ts
83
+ // WRONG — this creates a runtime import that will fail at load time
84
+ import { appLogger } from 'donobu';
85
+ ```
86
+
87
+ ### What is available on `deps`
88
+
89
+ - **`deps.donobu`** — the full public SDK: `createTool`, `Tool`, `ReplayableInteraction`, `appLogger`, `MiscUtils`, `PlaywrightUtils`, schemas, exception classes, etc.
48
90
 
49
- Because the scaffold reuses the Donobu versions already on disk, regenerating the project after a Donobu upgrade is the fastest way to stay in lockstep.
91
+ Feel free to add your own npm dependencies; esbuild bundles everything except the packages listed in `external`.
50
92
 
51
- ## Authoring tools
93
+ ## Authoring tool plugins
52
94
 
53
- Every plugin exports `loadCustomTools(dependencies)` and returns an array of `Tool` instances. You can instantiate `dependencies.donobu.Tool` directly or use the provided helper:
95
+ Every tool plugin exports `loadCustomTools(deps)` and returns an array of `Tool` instances. The simplest way to create a tool is `deps.donobu.createTool()`:
54
96
 
55
97
  ```ts
56
- // src/index.ts
98
+ import type { Tool, PluginDependencies } from 'donobu';
57
99
  import { z } from 'zod/v4';
58
- import { createTool } from './createTool.js';
59
- import type { PluginDependencies } from './PluginDependencies.js';
60
100
 
61
- export async function loadCustomTools(deps: PluginDependencies) {
62
- const pingSchema = z.object({
63
- url: z.string().url(),
64
- });
65
-
66
- const pingTool = createTool(deps, {
67
- name: 'pingEndpoint',
68
- description: 'Fetches a URL within the Donobu-controlled browser context.',
69
- requiresGpt: false,
70
- schema: pingSchema,
71
- async call(ctx, { url }) {
72
- const page = await ctx.browserContext.newPage();
73
- const response = await page.goto(url);
74
- return {
75
- isSuccessful: !!response,
76
- forLlm: `Fetched ${url} with status ${response?.status()}`,
77
- metadata: { status: response?.status() },
78
- };
79
- },
80
- });
101
+ export async function loadCustomTools(
102
+ deps: PluginDependencies,
103
+ ): Promise<Tool<any, any>[]> {
104
+ return [
105
+ deps.donobu.createTool(deps, {
106
+ name: 'judgeWebpageTitle',
107
+ description: 'Judge the quality of a title to a webpage.',
108
+ schema: z.object({ webpageTitle: z.string() }),
109
+ requiresGpt: true,
110
+ call: async (context, parameters) => {
111
+ const resp = await context.gptClient!.getMessage([
112
+ {
113
+ type: 'user',
114
+ items: [
115
+ {
116
+ type: 'text',
117
+ text: `Judge the quality of this webpage title on a scale of 1 (worst) to 10 (best) and explain why.
118
+ Title: ${parameters.webpageTitle}`,
119
+ },
120
+ ],
121
+ },
122
+ ]);
123
+ return {
124
+ isSuccessful: true,
125
+ forLlm: resp.text,
126
+ metadata: null,
127
+ };
128
+ },
129
+ }),
130
+ ];
131
+ }
132
+ ```
133
+
134
+ For tools that need to extend `ReplayableInteraction` or another base class, use a factory function so the base class comes from `deps.donobu` rather than a value import:
135
+
136
+ ```ts
137
+ import type { Tool, PluginDependencies } from 'donobu';
138
+
139
+ function createMyTool(deps: PluginDependencies): Tool<any, any> {
140
+ const { ReplayableInteraction } = deps.donobu;
141
+
142
+ class MyTool extends ReplayableInteraction</* ... */> {
143
+ // ...
144
+ }
81
145
 
82
- return [pingTool];
146
+ return new MyTool();
147
+ }
148
+
149
+ export async function loadCustomTools(deps: PluginDependencies) {
150
+ return [createMyTool(deps)];
83
151
  }
84
152
  ```
85
153
 
86
- Guidelines:
154
+ ## Authoring persistence plugins
87
155
 
88
- - Define Zod schemas for every tool so inputs from humans or the LLM are validated before your code runs.
89
- - Use `callFromGpt` if the GPT path needs different behavior; otherwise the helper routes GPT calls through `call`.
90
- - Request `requiresGpt: true` only when the tool cannot run without LLM assistance.
156
+ Persistence plugins export `loadPersistencePlugin(deps)`. The returned `key` must appear in the `PERSISTENCE_PRIORITY` environment variable for Donobu to use it.
91
157
 
92
- ## Working with injected dependencies
158
+ ```ts
159
+ import type {
160
+ EnvPersistence,
161
+ FlowsPersistence,
162
+ PersistencePlugin,
163
+ PluginDependencies,
164
+ } from 'donobu';
165
+
166
+ export async function loadPersistencePlugin(
167
+ deps: PluginDependencies,
168
+ ): Promise<{ key: string; plugin: PersistencePlugin } | null> {
169
+ const bucket = process.env.MY_STORAGE_BUCKET;
170
+ if (!bucket) return null; // skip registration if config is missing
171
+
172
+ return {
173
+ key: 'MY_CUSTOM',
174
+ plugin: {
175
+ async createFlowsPersistence(): Promise<FlowsPersistence | null> {
176
+ // Return your FlowsPersistence implementation, or null.
177
+ return null;
178
+ },
179
+ async createEnvPersistence(): Promise<EnvPersistence | null> {
180
+ // Return your EnvPersistence implementation, or null.
181
+ return null;
182
+ },
183
+ },
184
+ };
185
+ }
186
+ ```
93
187
 
94
- - `dependencies.donobu` exposes the public SDK: the base `Tool` class, `PlaywrightUtils`, logging helpers, type definitions, etc.
95
- - `dependencies.playwright` points to the same Playwright build Donobu uses, so your plugin and the core runtime stay aligned.
96
- - Feel free to add your own dependencies; `esbuild` bundles everything except the packages listed in `external`.
188
+ If your persistence classes need SDK utilities like `appLogger` or `FlowNotFoundException`, accept `deps.donobu` as a constructor parameter rather than importing them directly.
97
189
 
98
190
  ## Build and install
99
191
 
@@ -102,7 +194,24 @@ npm run build
102
194
  npm exec install-donobu-plugin
103
195
  ```
104
196
 
105
- `npm run build` cleans `dist/`, reinstalls dependencies (to ensure version parity), runs TypeScript, and bundles the result. `npm exec install-donobu-plugin` validates the current folder, infers the plugin name (prefers `package.json`, falls back to the git repo name or `$USER`), and copies `dist/` into the Donobu plugins directory. Restart Donobu after every install so the new bundle is loaded.
197
+ `npm run build` cleans `dist/`, runs TypeScript (for declarations), and bundles the source with esbuild. `npm exec install-donobu-plugin` infers the plugin name from `package.json` and copies `dist/` into the Donobu plugins directory. Restart Donobu after installing so the new bundle is loaded.
198
+
199
+ ## esbuild considerations
200
+
201
+ The generated `esbuild.config.mjs` bundles your TypeScript source and all third-party dependencies into a single `dist/index.mjs`, while keeping `donobu` external (it is injected at runtime by the plugin loader). The `mainFields: ['module', 'main']` option ensures esbuild prefers ESM entry points from third-party packages for cleaner output.
202
+
203
+ If you add dependencies that include CJS modules using `require()` for Node builtins (common in Google Cloud, AWS, or gRPC libraries), the bundle may fail at runtime with `"Dynamic require of X is not supported"`. Fix this by adding a `banner` that provides a real `require()`:
204
+
205
+ ```js
206
+ await build({
207
+ // ... your existing options ...
208
+ banner: {
209
+ js: 'import { createRequire } from "module"; const require = createRequire(import.meta.url);',
210
+ },
211
+ });
212
+ ```
213
+
214
+ This injects a real `require()` via Node's `createRequire`, allowing bundled CJS code to resolve Node builtins normally in the ESM context.
106
215
 
107
216
  ## Recommended development loop
108
217
 
@@ -111,10 +220,9 @@ npm exec install-donobu-plugin
111
220
  3. Restart Donobu Studio or the backend process
112
221
  4. Trigger your tool from the UI or API to verify behavior
113
222
 
114
- Because the default build script runs `npm install`, consider splitting the script into `build` and `bundle` variants if you need faster inner-loop iterations.
115
-
116
223
  ## Troubleshooting
117
224
 
118
225
  - **Plugin not appearing:** Ensure `npm exec install-donobu-plugin` ran successfully and that `dist/index.mjs` exists. Restart Donobu and watch the logs for plugin loading messages.
119
- - **Schema errors:** Zod throws runtime errors when inputs don’t match your schema. Log the error message to quickly see which field failed.
120
- - **Version mismatch:** If Donobu upgrades Playwright or its SDK, re-run the scaffold (or update the plugin’s dev dependencies) so you stay compatible.
226
+ - **"Cannot find package 'donobu'":** Your bundle contains a runtime `import ... from "donobu"`. Ensure all `donobu` imports use `import type` and access runtime values through `deps.donobu`.
227
+ - **"Dynamic require of X is not supported":** A bundled CJS dependency calls `require()` for a Node builtin. Add the `createRequire` banner to your esbuild config (see above).
228
+ - **Schema errors:** Zod throws runtime errors when inputs don't match your schema. Log the error message to quickly see which field failed.
package/index.js CHANGED
@@ -1,152 +1,131 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { mkdir, writeFile, readFile } from 'fs/promises';
4
- import { join, dirname } from 'path';
5
- import { createRequire } from 'module';
3
+ import { mkdir, writeFile } from 'fs/promises';
4
+ import { join } from 'path';
6
5
 
7
- async function getDonobuVersions() {
8
- try {
9
- // Find the donobu package.json by resolving the module path
10
- const require = createRequire(import.meta.url);
11
- const donobuPath = require.resolve('donobu');
12
- // Navigate up to find package.json (donobu path might point to dist/main.js)
13
- const donobuDir = dirname(donobuPath);
14
- let packageJsonPath = join(donobuDir, 'package.json');
15
-
16
- try {
17
- await readFile(packageJsonPath, 'utf8');
18
- } catch {
19
- // If not found, try going up one more directory
20
- packageJsonPath = join(dirname(donobuDir), 'package.json');
21
- }
6
+ const DONOBU_VERSION = '5.17.2';
7
+ const VALID_PLUGIN_TYPES = ['tool', 'persistence'];
22
8
 
23
- const packageJsonContent = await readFile(packageJsonPath, 'utf8');
24
- const donobuPackageJson = JSON.parse(packageJsonContent);
25
-
26
- return {
27
- donobuVersion: donobuPackageJson.version,
28
- playwrightVersion:
29
- donobuPackageJson.devDependencies?.playwright ||
30
- donobuPackageJson.dependencies?.playwright ||
31
- donobuPackageJson.peerDependencies?.playwright?.replace(/^>=/, '') ||
32
- '1.53.2',
33
- playwrightTestVersion:
34
- donobuPackageJson.devDependencies?.['@playwright/test'] ||
35
- donobuPackageJson.dependencies?.['@playwright/test'] ||
36
- donobuPackageJson.peerDependencies?.['@playwright/test']?.replace(
37
- /^>=/,
38
- ''
39
- ) ||
40
- '1.53.2',
41
- };
42
- } catch (error) {
43
- console.error('Could not read the Donobu package.json file!');
44
- throw error;
45
- }
46
- }
47
-
48
- async function createPluginStructure(pluginName) {
9
+ async function createPluginStructure(pluginName, pluginType) {
49
10
  const pluginDir = join(process.cwd(), pluginName);
50
- console.log(`Creating plugin directory: ${pluginDir}`);
51
- // Get current donobu and playwright versions
52
- const versions = await getDonobuVersions();
53
- console.log(`Using Donobu version: ${versions.donobuVersion}`);
54
- console.log(`Using Playwright version: ${versions.playwrightVersion}`);
11
+ console.log(`Creating ${pluginType} plugin directory: ${pluginDir}`);
12
+ const donobuVersion = DONOBU_VERSION;
13
+ console.log(`Using Donobu version: ${donobuVersion}`);
55
14
  // Create directory structure
56
15
  await mkdir(pluginDir, { recursive: true });
57
16
  await mkdir(join(pluginDir, 'src'), { recursive: true });
58
17
  console.log('Creating files...');
59
- await createTemplateFiles(pluginDir, pluginName, versions);
18
+
19
+ if (pluginType === 'persistence') {
20
+ await createPersistenceTemplateFiles(pluginDir, pluginName, donobuVersion);
21
+ } else {
22
+ await createToolTemplateFiles(pluginDir, pluginName, donobuVersion);
23
+ }
24
+
60
25
  console.log(`\nPlugin "${pluginName}" created successfully!`);
61
26
  console.log(`\nNext steps:`);
62
27
  console.log(` cd ${pluginName}`);
63
28
  console.log(` npm install`);
64
- console.log(` # Edit src/index.ts to add your custom tools`);
29
+ if (pluginType === 'persistence') {
30
+ console.log(` # Edit src/index.ts to implement your persistence plugin`);
31
+ } else {
32
+ console.log(` # Edit src/index.ts to add your custom tools`);
33
+ }
65
34
  console.log(` npx install-donobu-plugin`);
66
35
  }
67
36
 
68
- async function createTemplateFiles(pluginDir, pluginName, versions) {
37
+ // ---------------------------------------------------------------------------
38
+ // Shared template helpers
39
+ // ---------------------------------------------------------------------------
40
+
41
+ function createTsConfig() {
42
+ return {
43
+ compilerOptions: {
44
+ target: 'ES2021',
45
+ lib: ['ES2021', 'DOM'],
46
+ module: 'NodeNext',
47
+ moduleResolution: 'NodeNext',
48
+ rootDir: './src',
49
+ outDir: './dist',
50
+ declaration: true,
51
+ declarationMap: true,
52
+ sourceMap: true,
53
+ esModuleInterop: true,
54
+ forceConsistentCasingInFileNames: true,
55
+ strict: true,
56
+ skipLibCheck: true,
57
+ allowUnusedLabels: false,
58
+ allowUnreachableCode: false,
59
+ },
60
+ include: ['src'],
61
+ exclude: ['node_modules', 'dist'],
62
+ };
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Tool plugin templates
67
+ // ---------------------------------------------------------------------------
68
+
69
+ async function createToolTemplateFiles(pluginDir, pluginName, donobuVersion) {
69
70
  // Create package.json
70
71
  const packageJson = {
71
72
  name: pluginName,
73
+ 'donobu-plugin': true,
72
74
  version: '1.0.0',
73
75
  private: true,
74
- type: 'module',
75
76
  description: 'Custom tools for use by Donobu.',
76
- main: './dist/index.mjs',
77
- types: './dist/index.d.ts',
77
+ main: 'dist/index.js',
78
+ types: 'dist/index.d.ts',
78
79
  exports: {
79
- '.': './dist/index.mjs',
80
+ types: './dist/index.d.ts',
81
+ import: './dist/index.mjs',
82
+ require: './dist/index.js',
80
83
  },
81
84
  files: ['dist'],
82
85
  scripts: {
83
86
  clean: 'rm -rf dist',
84
- build:
85
- 'npm run clean && npm install && tsc -p tsconfig.json && node esbuild.config.mjs',
87
+ build: 'npm run clean && tsc && node esbuild.config.mjs',
86
88
  },
87
89
  license: 'UNLICENSED',
88
- devDependencies: {
89
- donobu: `^${versions.donobuVersion}`,
90
- playwright: versions.playwrightVersion,
91
- '@playwright/test': versions.playwrightTestVersion,
92
- typescript: '^5.8.3',
93
- esbuild: '^0.25.6',
94
- },
95
90
  peerDependencies: {
96
- donobu: `>=${versions.donobuVersion}`,
97
- playwright: `>=${versions.playwrightVersion}`,
98
- '@playwright/test': `>=${versions.playwrightTestVersion}`,
91
+ donobu: `>=${donobuVersion}`,
99
92
  },
100
- peerDependenciesMeta: {
101
- playwright: {
102
- optional: false,
103
- },
104
- '@playwright/test': {
105
- optional: false,
106
- },
93
+ devDependencies: {
94
+ '@types/node': '^22.19.13',
95
+ donobu: `^${donobuVersion}`,
96
+ esbuild: '^0.25.12',
97
+ typescript: '^5.8.3',
107
98
  },
108
- dependencies: {
109
- zod: '^3.25.76',
99
+ pnpm: {
100
+ onlyBuiltDependencies: ['better-sqlite3', 'esbuild'],
110
101
  },
111
102
  };
112
103
 
113
104
  await writeFile(
114
105
  join(pluginDir, 'package.json'),
115
- JSON.stringify(packageJson, null, 2)
106
+ JSON.stringify(packageJson, null, 2),
116
107
  );
117
108
 
118
- // Create tsconfig.json
119
- const tsConfig = {
120
- compilerOptions: {
121
- target: 'ES2020',
122
- module: 'NodeNext',
123
- moduleResolution: 'NodeNext',
124
- rootDir: './src',
125
- outDir: './dist',
126
- declaration: true,
127
- strict: true,
128
- esModuleInterop: true,
129
- skipLibCheck: true,
130
- forceConsistentCasingInFileNames: true,
131
- },
132
- include: ['src/**/*'],
133
- };
134
-
135
109
  await writeFile(
136
110
  join(pluginDir, 'tsconfig.json'),
137
- JSON.stringify(tsConfig, null, 2)
111
+ JSON.stringify(createTsConfig(), null, 2),
138
112
  );
139
113
 
140
114
  // Create esbuild.config.mjs
141
115
  const esbuildConfig = `import { build } from 'esbuild';
142
116
 
143
117
  await build({
144
- entryPoints: ['dist/index.js'],
118
+ entryPoints: ['src/index.ts'],
145
119
  outfile: 'dist/index.mjs',
146
120
  bundle: true,
147
121
  format: 'esm',
148
122
  platform: 'node',
149
- external: ['donobu', 'playwright'],
123
+
124
+ // 'donobu' is provided at runtime by the plugin loader via deps.donobu.
125
+ external: ['donobu'],
126
+
127
+ mainFields: ['module', 'main'],
128
+
150
129
  minify: true,
151
130
  });
152
131
  `;
@@ -154,150 +133,273 @@ await build({
154
133
  await writeFile(join(pluginDir, 'esbuild.config.mjs'), esbuildConfig);
155
134
 
156
135
  // Create src/index.ts
157
- const indexTs = `import type { Tool } from "donobu";
158
- import { PluginDependencies } from "./PluginDependencies.js";
136
+ const indexTs = `// Note that non-type imports from 'donobu' should be extracted from the given
137
+ // 'deps.donobu' parameter in 'loadCustomTools'.
138
+ import type { Tool, PluginDependencies } from 'donobu';
139
+ import { z } from 'zod/v4';
159
140
 
160
141
  export async function loadCustomTools(
161
- dependencies: PluginDependencies
142
+ deps: PluginDependencies
162
143
  ): Promise<Tool<any, any>[]> {
163
144
  // Register your custom tools here.
164
- return [];
165
- }
166
- `;
145
+ return [
146
+ // Here is a small example tool for reference.
147
+ deps.donobu.createTool(deps, {
148
+ // The tool name, description, and schema are shared with the Donobu agent
149
+ // to help it decide when, where, and how this tool will be called.
150
+ name: 'judgeWebpageTitle',
151
+ description: 'Judge the quality of a title to a webpage.',
152
+ schema: z.object({ webpageTitle: z.string() }),
153
+ // This example tool uses a GPT/LLM so we call it out here.
154
+ requiresGpt: true,
155
+ call: async (context, parameters) => {
156
+ // 'requiredGpt' is true, so we are guaranteed a valid gptClient handle.
157
+ const resp = await context.gptClient!.getMessage([
158
+ {
159
+ type: 'user',
160
+ items: [
161
+ {
162
+ type: 'text',
163
+ text: \`Judge the quality of this webpage title on a scale of 1 (worst) to 10 (best) and explain why.
164
+ Title: \${parameters.webpageTitle}\`,
165
+ },
166
+ ],
167
+ },
168
+ ]);
169
+ return {
170
+ // Returning false or throwing an exception will signal tool call failure.
171
+ isSuccessful: true,
172
+ // This is what is shared back with the Donobu agent.
173
+ forLlm: resp.text,
174
+ // Metadata is persisted with the tool call result, but is not shared
175
+ // with the Donobu agent, so this can be used to store large complex data.
176
+ metadata: null,
177
+ };
178
+ },
179
+ }),
180
+ ];
181
+ }`;
167
182
 
168
183
  await writeFile(join(pluginDir, 'src', 'index.ts'), indexTs);
169
184
 
170
- // Create src/createTool.ts
171
- const createToolTs = `import { z } from "zod/v4";
172
- import type { ToolCallContext, ToolCallResult, Tool } from "donobu";
173
- import { PluginDependencies } from "./PluginDependencies.js";
185
+ // Create a basic README
186
+ const readme = `# ${pluginName}
174
187
 
175
- /**
176
- * Factory function to create a Donobu Tool instance.
177
- */
178
- export function createTool<
179
- CallSchema extends z.ZodObject<any>,
180
- CallFromGptSchema extends z.ZodObject<any> = CallSchema,
181
- >(
182
- dependencies: PluginDependencies,
183
- options: CreateToolOptions<CallSchema, CallFromGptSchema>
184
- ): Tool<CallSchema, CallFromGptSchema> {
185
- class AnonymousTool extends dependencies.donobu.Tool<
186
- CallSchema,
187
- CallFromGptSchema
188
- > {
189
- public constructor() {
190
- super(
191
- options.name,
192
- options.description,
193
- options.schema,
194
- (options.gptSchema ?? options.schema) as CallFromGptSchema,
195
- options.requiresGpt
196
- );
197
- }
198
- public override async call(
199
- ctx: ToolCallContext,
200
- params: z.infer<CallSchema>
201
- ): Promise<ToolCallResult> {
202
- return options.call(ctx, params);
203
- }
204
- public override async callFromGpt(
205
- ctx: ToolCallContext,
206
- params: z.infer<CallFromGptSchema>
207
- ): Promise<ToolCallResult> {
208
- return options.callFromGpt
209
- ? options.callFromGpt(ctx, params)
210
- : this.call(ctx, params);
211
- }
212
- }
188
+ A custom Donobu tool plugin for browser automation.
213
189
 
214
- return new AnonymousTool();
215
- }
190
+ ## Development
216
191
 
217
- export interface CreateToolOptions<
218
- CallSchema extends z.ZodObject<any>,
219
- CallFromGptSchema extends z.ZodObject<any> = CallSchema,
220
- > {
221
- /**
222
- * @param name This is the name for the tool that will be shared with the LLM when making requests.
223
- * @param description This is the description that will be shared with the LLM when making requests.
224
- * @param inputSchema This is the JSON-schema for the tool when it is invoked by a non-LLM.
225
- * @param inputSchemaForGpt This is the JSON-schema that will be shared with the LLM when making
226
- * requests during autonomous flows.
227
- * @param requiresGpt Set to true if this tool requires the usage of a GPT.
228
- * @param call This is the function that will be invoked when the tool is called by a non-LLM.
229
- * @param callFromGpt This is the function that will be invoked when the tool is called by an LLM.
230
- */
231
- name: string;
232
- description: string;
233
- requiresGpt: boolean;
234
- schema: CallSchema;
235
- gptSchema?: CallFromGptSchema;
236
- call(
237
- ctx: ToolCallContext,
238
- params: z.infer<CallSchema>
239
- ): Promise<ToolCallResult> | ToolCallResult;
240
- callFromGpt?(
241
- ctx: ToolCallContext,
242
- params: z.infer<CallFromGptSchema>
243
- ): Promise<ToolCallResult> | ToolCallResult;
244
- }
192
+ 1. Edit \`src/index.ts\` to add your custom tools
193
+ 2. Build and install the plugin:
194
+ \`\`\`bash
195
+ npm run build
196
+ npm exec install-donobu-plugin
197
+ \`\`\`
198
+ 3. Restart Donobu to load your plugin
245
199
  `;
246
200
 
247
- await writeFile(join(pluginDir, 'src', 'createTool.ts'), createToolTs);
248
-
249
- // Create src/PluginDependencies.ts
250
- const pluginDepsTs = `export interface PluginDependencies {
251
- donobu: typeof import("donobu");
252
- playwright: typeof import("playwright");
201
+ await writeFile(join(pluginDir, 'README.md'), readme);
253
202
  }
254
- `;
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Persistence plugin templates
206
+ // ---------------------------------------------------------------------------
207
+
208
+ async function createPersistenceTemplateFiles(
209
+ pluginDir,
210
+ pluginName,
211
+ donobuVersion,
212
+ ) {
213
+ // Create package.json — no playwright dependency needed
214
+ const packageJson = {
215
+ name: pluginName,
216
+ 'donobu-plugin': true,
217
+ version: '1.0.0',
218
+ private: true,
219
+ description: 'Custom persistence plugin for Donobu.',
220
+ main: 'dist/index.js',
221
+ types: 'dist/index.d.ts',
222
+ exports: {
223
+ types: './dist/index.d.ts',
224
+ import: './dist/index.mjs',
225
+ require: './dist/index.js',
226
+ },
227
+ files: ['dist'],
228
+ scripts: {
229
+ clean: 'rm -rf dist',
230
+ build: 'npm run clean && tsc && node esbuild.config.mjs',
231
+ },
232
+ license: 'UNLICENSED',
233
+ peerDependencies: {
234
+ donobu: `>=${donobuVersion}`,
235
+ },
236
+ devDependencies: {
237
+ '@types/node': '^22.19.13',
238
+ donobu: `^${donobuVersion}`,
239
+ esbuild: '^0.25.12',
240
+ typescript: '^5.8.3',
241
+ },
242
+ pnpm: {
243
+ onlyBuiltDependencies: ['better-sqlite3', 'esbuild'],
244
+ },
245
+ };
255
246
 
256
247
  await writeFile(
257
- join(pluginDir, 'src', 'PluginDependencies.ts'),
258
- pluginDepsTs
248
+ join(pluginDir, 'package.json'),
249
+ JSON.stringify(packageJson, null, 2),
250
+ );
251
+
252
+ await writeFile(
253
+ join(pluginDir, 'tsconfig.json'),
254
+ JSON.stringify(createTsConfig(), null, 2),
259
255
  );
260
256
 
257
+ // Create esbuild.config.mjs — only externalize donobu
258
+ const esbuildConfig = `import { build } from 'esbuild';
259
+
260
+ await build({
261
+ entryPoints: ['src/index.ts'],
262
+ outfile: 'dist/index.mjs',
263
+ bundle: true,
264
+ format: 'esm',
265
+ platform: 'node',
266
+
267
+ // 'donobu' is provided at runtime by the plugin loader via deps.donobu.
268
+ external: ['donobu'],
269
+
270
+ mainFields: ['module', 'main'],
271
+
272
+ minify: true,
273
+ });
274
+ `;
275
+
276
+ await writeFile(join(pluginDir, 'esbuild.config.mjs'), esbuildConfig);
277
+
278
+ // Create src/index.ts
279
+ const indexTs = `import type {
280
+ EnvPersistence,
281
+ FlowsPersistence,
282
+ PersistencePlugin,
283
+ PluginDependencies,
284
+ } from 'donobu';
285
+
286
+ /**
287
+ * Called by Donobu on startup when this plugin is installed.
288
+ *
289
+ * Return an object with a \`key\` (matching an entry in the
290
+ * PERSISTENCE_PRIORITY environment variable) and a \`plugin\`
291
+ * implementing the PersistencePlugin interface.
292
+ *
293
+ * Return \`null\` to skip registration (e.g., if required
294
+ * environment variables are missing).
295
+ */
296
+ export async function loadPersistencePlugin(
297
+ deps: PluginDependencies,
298
+ ): Promise<{ key: string; plugin: PersistencePlugin } | null> {
299
+ // Example: skip registration if required config is not set.
300
+ // const bucket = process.env.MY_STORAGE_BUCKET;
301
+ // if (!bucket) return null;
302
+
303
+ return {
304
+ // This key must appear in the PERSISTENCE_PRIORITY env variable
305
+ // for Donobu to use this plugin. For example:
306
+ // PERSISTENCE_PRIORITY=MY_CUSTOM,LOCAL,RAM
307
+ key: 'MY_CUSTOM',
308
+ plugin: {
309
+ async createFlowsPersistence(): Promise<FlowsPersistence | null> {
310
+ // Return your FlowsPersistence implementation here.
311
+ // Return null if the required configuration is missing.
312
+ return null;
313
+ },
314
+ async createEnvPersistence(): Promise<EnvPersistence | null> {
315
+ // Return your EnvPersistence implementation here.
316
+ // Return null if the required configuration is missing.
317
+ return null;
318
+ },
319
+ },
320
+ };
321
+ }`;
322
+
323
+ await writeFile(join(pluginDir, 'src', 'index.ts'), indexTs);
324
+
261
325
  // Create a basic README
262
326
  const readme = `# ${pluginName}
263
327
 
264
- A custom Donobu plugin with tools for browser automation.
328
+ A custom Donobu persistence plugin.
265
329
 
266
330
  ## Development
267
331
 
268
- 1. Edit \`src/index.ts\` to add your custom tools
332
+ 1. Edit \`src/index.ts\` to implement your persistence plugin
269
333
  2. Build and install the plugin:
270
334
  \`\`\`bash
271
335
  npm run build
272
336
  npm exec install-donobu-plugin
273
337
  \`\`\`
274
- 3. Restart Donobu to see your tools
338
+ 3. Set the \`PERSISTENCE_PRIORITY\` environment variable to include your plugin key
339
+ 4. Restart Donobu to load your plugin
275
340
  `;
276
341
 
277
342
  await writeFile(join(pluginDir, 'README.md'), readme);
278
343
  }
279
344
 
345
+ // ---------------------------------------------------------------------------
346
+ // CLI
347
+ // ---------------------------------------------------------------------------
348
+
349
+ function parseArgs(argv) {
350
+ const args = argv.slice(2);
351
+ let pluginName = null;
352
+ let pluginType = 'tool';
353
+
354
+ for (let i = 0; i < args.length; i++) {
355
+ if (args[i] === '--type' && i + 1 < args.length) {
356
+ pluginType = args[i + 1];
357
+ i++; // skip value
358
+ } else if (!args[i].startsWith('-')) {
359
+ pluginName = args[i];
360
+ }
361
+ }
362
+
363
+ return { pluginName, pluginType };
364
+ }
365
+
280
366
  async function main() {
281
- const pluginName = process.argv[2];
367
+ const { pluginName, pluginType } = parseArgs(process.argv);
282
368
 
283
369
  if (!pluginName) {
284
- console.error('Usage: create-donobu-plugin <plugin-name>');
370
+ console.error(
371
+ 'Usage: create-donobu-plugin <plugin-name> [--type <tool|persistence>]',
372
+ );
285
373
  console.error('');
286
- console.error('Example:');
374
+ console.error('Options:');
375
+ console.error(
376
+ ' --type <type> Plugin type: "tool" (default) or "persistence"',
377
+ );
378
+ console.error('');
379
+ console.error('Examples:');
287
380
  console.error(' npm create donobu-plugin my-awesome-plugin');
381
+ console.error(' npm create donobu-plugin my-storage --type persistence');
288
382
  process.exit(1);
289
383
  }
290
384
 
291
385
  // Validate plugin name
292
386
  if (!/^[a-z0-9-_]+$/.test(pluginName)) {
293
387
  console.error(
294
- 'Plugin name must contain only lowercase letters, numbers, hyphens, and underscores'
388
+ 'Plugin name must contain only lowercase letters, numbers, hyphens, and underscores',
389
+ );
390
+ process.exit(1);
391
+ }
392
+
393
+ // Validate plugin type
394
+ if (!VALID_PLUGIN_TYPES.includes(pluginType)) {
395
+ console.error(
396
+ `Invalid plugin type "${pluginType}". Must be one of: ${VALID_PLUGIN_TYPES.join(', ')}`,
295
397
  );
296
398
  process.exit(1);
297
399
  }
298
400
 
299
401
  try {
300
- await createPluginStructure(pluginName);
402
+ await createPluginStructure(pluginName, pluginType);
301
403
  } catch (error) {
302
404
  console.error('Failed to create plugin:', error.message);
303
405
  process.exit(1);
package/package.json CHANGED
@@ -1,24 +1,33 @@
1
1
  {
2
- "name": "create-donobu-plugin",
3
- "version": "1.2.1",
4
- "type": "module",
5
- "description": "Create a new Donobu plugin",
6
- "author": "Donobu",
7
- "homepage": "https://donobu.com",
8
- "main": "index.js",
9
- "bin": {
10
- "create-donobu-plugin": "./index.js"
11
- },
12
- "files": [
13
- "index.js"
14
- ],
15
- "dependencies": {
16
- "donobu": "^3.3.0"
17
- },
18
- "keywords": [
19
- "donobu",
20
- "plugin",
21
- "create"
22
- ],
23
- "license": "UNLICENSED"
2
+ "name": "create-donobu-plugin",
3
+ "version": "1.7.0",
4
+ "type": "module",
5
+ "description": "Create a new Donobu plugin",
6
+ "author": "Donobu",
7
+ "homepage": "https://donobu.com",
8
+ "main": "index.js",
9
+ "bin": {
10
+ "create-donobu-plugin": "./index.js"
11
+ },
12
+ "files": [
13
+ "index.js"
14
+ ],
15
+ "devDependencies": {
16
+ "@eslint/js": "^10.0.1",
17
+ "eslint": "^10.0.2",
18
+ "eslint-plugin-simple-import-sort": "^12.1.1",
19
+ "globals": "^16.0.0",
20
+ "prettier": "^3.8.0"
21
+ },
22
+ "keywords": [
23
+ "donobu",
24
+ "plugin",
25
+ "create"
26
+ ],
27
+ "license": "UNLICENSED",
28
+ "scripts": {
29
+ "build": "pnpm install --frozen-lockfile && pnpm run lint --fix && pnpm run format && pnpm run lint",
30
+ "lint": "eslint .",
31
+ "format": "prettier --write ."
32
+ }
24
33
  }