dank-ai 1.0.39 → 1.0.42

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.
@@ -0,0 +1,324 @@
1
+ /**
2
+ * PluginBase - Abstract base class for all Dank plugins
3
+ *
4
+ * Provides lifecycle hooks, event handling, tool registration, and state management
5
+ * for plugins that integrate with Dank agents.
6
+ */
7
+
8
+ const EventEmitter = require('events');
9
+ const { v4: uuidv4 } = require('uuid');
10
+
11
+ class PluginBase extends EventEmitter {
12
+ constructor(name, config = {}) {
13
+ super();
14
+
15
+ if (!name || typeof name !== 'string') {
16
+ throw new Error('Plugin name must be a non-empty string');
17
+ }
18
+
19
+ this.name = name;
20
+ this.id = uuidv4();
21
+ this.config = config;
22
+ this.state = new Map();
23
+ this.tools = new Map();
24
+ this.handlers = new Map();
25
+ this.status = 'initialized'; // initialized, starting, running, stopping, stopped, error
26
+ this.agentContext = null;
27
+ this.pluginManager = null;
28
+ this.createdAt = new Date().toISOString();
29
+ this.startedAt = null;
30
+ this.stoppedAt = null;
31
+ }
32
+
33
+ /**
34
+ * Initialize the plugin
35
+ * Override this method to set up connections, validate config, etc.
36
+ */
37
+ async init() {
38
+ this.status = 'initialized';
39
+ return this;
40
+ }
41
+
42
+ /**
43
+ * Start the plugin
44
+ * Override this method to start services, begin listening, etc.
45
+ */
46
+ async start() {
47
+ if (this.status === 'running') {
48
+ return this;
49
+ }
50
+
51
+ this.status = 'starting';
52
+ this.startedAt = new Date().toISOString();
53
+
54
+ try {
55
+ await this.onStart();
56
+ this.status = 'running';
57
+ this.emit('started');
58
+ return this;
59
+ } catch (error) {
60
+ this.status = 'error';
61
+ this.emit('error', error);
62
+ throw error;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Stop the plugin
68
+ * Override this method to clean up resources, close connections, etc.
69
+ */
70
+ async stop() {
71
+ if (this.status === 'stopped' || this.status === 'stopping') {
72
+ return this;
73
+ }
74
+
75
+ this.status = 'stopping';
76
+
77
+ try {
78
+ await this.onStop();
79
+ this.status = 'stopped';
80
+ this.stoppedAt = new Date().toISOString();
81
+ this.emit('stopped');
82
+ return this;
83
+ } catch (error) {
84
+ this.status = 'error';
85
+ this.emit('error', error);
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Destroy the plugin (cleanup)
92
+ * Override this method for final cleanup
93
+ */
94
+ async destroy() {
95
+ if (this.status !== 'stopped') {
96
+ await this.stop();
97
+ }
98
+
99
+ await this.onDestroy();
100
+ this.removeAllListeners();
101
+ this.state.clear();
102
+ this.tools.clear();
103
+ this.handlers.clear();
104
+ return this;
105
+ }
106
+
107
+ /**
108
+ * Lifecycle hooks (override these in subclasses)
109
+ */
110
+ async onStart() {
111
+ // Override in subclass
112
+ }
113
+
114
+ async onStop() {
115
+ // Override in subclass
116
+ }
117
+
118
+ async onDestroy() {
119
+ // Override in subclass
120
+ }
121
+
122
+ /**
123
+ * Register an event handler
124
+ * Uses the same pattern as agent handlers
125
+ */
126
+ on(eventType, handler) {
127
+ if (typeof handler !== 'function') {
128
+ throw new Error('Handler must be a function');
129
+ }
130
+
131
+ if (!this.handlers.has(eventType)) {
132
+ this.handlers.set(eventType, []);
133
+ }
134
+
135
+ this.handlers.get(eventType).push({
136
+ handler,
137
+ plugin: this.name
138
+ });
139
+
140
+ // Also register with EventEmitter for compatibility
141
+ super.on(eventType, handler);
142
+
143
+ return this;
144
+ }
145
+
146
+ /**
147
+ * Remove an event handler
148
+ */
149
+ off(eventType, handler) {
150
+ if (this.handlers.has(eventType)) {
151
+ const handlers = this.handlers.get(eventType);
152
+ const index = handlers.findIndex(h => h.handler === handler);
153
+ if (index !== -1) {
154
+ handlers.splice(index, 1);
155
+ }
156
+ }
157
+
158
+ super.off(eventType, handler);
159
+ return this;
160
+ }
161
+
162
+ /**
163
+ * Emit an event (delegates to EventEmitter)
164
+ */
165
+ emit(eventName, ...args) {
166
+ // Prefix plugin events with plugin name
167
+ const prefixedEvent = `plugin:${this.name}:${eventName}`;
168
+ super.emit(prefixedEvent, ...args);
169
+ super.emit(eventName, ...args);
170
+ return this;
171
+ }
172
+
173
+ /**
174
+ * Register a tool that agents can use
175
+ */
176
+ registerTool(name, definition) {
177
+ if (typeof name !== 'string' || name.trim().length === 0) {
178
+ throw new Error('Tool name must be a non-empty string');
179
+ }
180
+
181
+ // Prefix tool name with plugin name
182
+ const prefixedName = `plugin:${this.name}:${name}`;
183
+
184
+ this.tools.set(prefixedName, {
185
+ ...definition,
186
+ name: prefixedName,
187
+ plugin: this.name,
188
+ registeredAt: new Date().toISOString()
189
+ });
190
+
191
+ // Emit event for tool registration
192
+ this.emit('tool:registered', { name: prefixedName, definition });
193
+
194
+ return this;
195
+ }
196
+
197
+ /**
198
+ * Get all registered tools
199
+ */
200
+ getTools() {
201
+ return Array.from(this.tools.values());
202
+ }
203
+
204
+ /**
205
+ * Get a specific tool
206
+ */
207
+ getTool(name) {
208
+ const prefixedName = name.startsWith(`plugin:${this.name}:`)
209
+ ? name
210
+ : `plugin:${this.name}:${name}`;
211
+ return this.tools.get(prefixedName);
212
+ }
213
+
214
+ /**
215
+ * Set plugin state (key-value store)
216
+ */
217
+ setState(key, value) {
218
+ this.state.set(key, value);
219
+ this.emit('state:changed', { key, value });
220
+ return this;
221
+ }
222
+
223
+ /**
224
+ * Get plugin state
225
+ */
226
+ getState(key) {
227
+ return this.state.get(key);
228
+ }
229
+
230
+ /**
231
+ * Get all plugin state
232
+ */
233
+ getAllState() {
234
+ return Object.fromEntries(this.state);
235
+ }
236
+
237
+ /**
238
+ * Clear plugin state
239
+ */
240
+ clearState() {
241
+ this.state.clear();
242
+ this.emit('state:cleared');
243
+ return this;
244
+ }
245
+
246
+ /**
247
+ * Get agent context (set by PluginManager)
248
+ */
249
+ getAgentContext() {
250
+ return this.agentContext;
251
+ }
252
+
253
+ /**
254
+ * Set agent context (called by PluginManager)
255
+ */
256
+ setAgentContext(context) {
257
+ this.agentContext = context;
258
+ return this;
259
+ }
260
+
261
+ /**
262
+ * Get plugin manager reference
263
+ */
264
+ getPluginManager() {
265
+ return this.pluginManager;
266
+ }
267
+
268
+ /**
269
+ * Set plugin manager reference (called by PluginManager)
270
+ */
271
+ setPluginManager(manager) {
272
+ this.pluginManager = manager;
273
+ return this;
274
+ }
275
+
276
+ /**
277
+ * Get another plugin by name
278
+ */
279
+ getPlugin(name) {
280
+ if (!this.pluginManager) {
281
+ throw new Error('Plugin manager not available');
282
+ }
283
+ return this.pluginManager.getPlugin(name);
284
+ }
285
+
286
+ /**
287
+ * Get all other plugins
288
+ */
289
+ getPlugins() {
290
+ if (!this.pluginManager) {
291
+ throw new Error('Plugin manager not available');
292
+ }
293
+ return this.pluginManager.getAllPlugins();
294
+ }
295
+
296
+ /**
297
+ * Get plugin metadata
298
+ */
299
+ getMetadata() {
300
+ return {
301
+ name: this.name,
302
+ id: this.id,
303
+ status: this.status,
304
+ createdAt: this.createdAt,
305
+ startedAt: this.startedAt,
306
+ stoppedAt: this.stoppedAt,
307
+ toolCount: this.tools.size,
308
+ handlerCount: this.handlers.size,
309
+ stateSize: this.state.size
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Validate plugin configuration
315
+ * Override this method to add custom validation
316
+ */
317
+ validateConfig(config) {
318
+ // Override in subclass for custom validation
319
+ return true;
320
+ }
321
+ }
322
+
323
+ module.exports = { PluginBase };
324
+
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Plugin Configuration - Schema validation and management
3
+ *
4
+ * Provides Joi-based schema validation for plugin configurations,
5
+ * environment variable injection, and secret management.
6
+ */
7
+
8
+ const Joi = require('joi');
9
+
10
+ class PluginConfig {
11
+ /**
12
+ * Validate plugin configuration against schema
13
+ */
14
+ static validate(name, config, schema) {
15
+ if (!schema) {
16
+ // No schema provided, just return config as-is
17
+ return config;
18
+ }
19
+
20
+ const { error, value } = schema.validate(config, {
21
+ stripUnknown: true,
22
+ abortEarly: false
23
+ });
24
+
25
+ if (error) {
26
+ const details = error.details.map(d => d.message).join(', ');
27
+ throw new Error(`Invalid configuration for plugin '${name}': ${details}`);
28
+ }
29
+
30
+ return value;
31
+ }
32
+
33
+ /**
34
+ * Inject environment variables into config
35
+ * Replaces ${ENV_VAR} or ${ENV_VAR:default} patterns
36
+ */
37
+ static injectEnvVars(config) {
38
+ if (typeof config === 'string') {
39
+ return this._replaceEnvVars(config);
40
+ }
41
+
42
+ if (Array.isArray(config)) {
43
+ return config.map(item => this.injectEnvVars(item));
44
+ }
45
+
46
+ if (config && typeof config === 'object') {
47
+ const result = {};
48
+ for (const [key, value] of Object.entries(config)) {
49
+ result[key] = this.injectEnvVars(value);
50
+ }
51
+ return result;
52
+ }
53
+
54
+ return config;
55
+ }
56
+
57
+ /**
58
+ * Replace environment variable patterns in string
59
+ */
60
+ static _replaceEnvVars(str) {
61
+ if (typeof str !== 'string') {
62
+ return str;
63
+ }
64
+
65
+ // Match ${ENV_VAR} or ${ENV_VAR:default}
66
+ return str.replace(/\$\{([^}:]+)(?::([^}]+))?\}/g, (match, envVar, defaultValue) => {
67
+ const value = process.env[envVar];
68
+ if (value !== undefined) {
69
+ return value;
70
+ }
71
+ if (defaultValue !== undefined) {
72
+ return defaultValue;
73
+ }
74
+ return match; // Keep original if no replacement found
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Common configuration schemas for plugin types
80
+ */
81
+ static schemas = {
82
+ /**
83
+ * Database connection schema
84
+ */
85
+ database: Joi.object({
86
+ connectionString: Joi.string().optional(),
87
+ host: Joi.string().optional(),
88
+ port: Joi.number().optional(),
89
+ database: Joi.string().optional(),
90
+ username: Joi.string().optional(),
91
+ password: Joi.string().optional(),
92
+ poolSize: Joi.number().min(1).max(100).default(10),
93
+ timeout: Joi.number().min(1000).default(30000),
94
+ ssl: Joi.boolean().default(false)
95
+ }).or('connectionString', 'host'),
96
+
97
+ /**
98
+ * API key schema
99
+ */
100
+ apiKey: Joi.object({
101
+ apiKey: Joi.string().required(),
102
+ baseURL: Joi.string().uri().optional(),
103
+ timeout: Joi.number().min(1000).default(30000),
104
+ retries: Joi.number().min(0).max(5).default(2)
105
+ }),
106
+
107
+ /**
108
+ * Vector database schema
109
+ */
110
+ vectorDB: Joi.object({
111
+ apiKey: Joi.string().required(),
112
+ environment: Joi.string().optional(),
113
+ index: Joi.string().optional(),
114
+ dimension: Joi.number().optional(),
115
+ timeout: Joi.number().min(1000).default(30000)
116
+ }),
117
+
118
+ /**
119
+ * File storage schema
120
+ */
121
+ fileStorage: Joi.object({
122
+ path: Joi.string().required(),
123
+ maxSize: Joi.number().min(0).optional(),
124
+ allowedExtensions: Joi.array().items(Joi.string()).optional()
125
+ }),
126
+
127
+ /**
128
+ * Redis schema
129
+ */
130
+ redis: Joi.object({
131
+ host: Joi.string().default('localhost'),
132
+ port: Joi.number().min(1).max(65535).default(6379),
133
+ password: Joi.string().optional(),
134
+ db: Joi.number().min(0).default(0),
135
+ keyPrefix: Joi.string().optional()
136
+ })
137
+ };
138
+
139
+ /**
140
+ * Get a schema by name
141
+ */
142
+ static getSchema(name) {
143
+ return this.schemas[name];
144
+ }
145
+
146
+ /**
147
+ * Merge multiple schemas
148
+ */
149
+ static mergeSchemas(...schemas) {
150
+ return schemas.reduce((merged, schema) => {
151
+ return merged ? merged.concat(schema) : schema;
152
+ }, null);
153
+ }
154
+
155
+ /**
156
+ * Create a schema with environment variable support
157
+ */
158
+ static createEnvSchema(baseSchema) {
159
+ return baseSchema.custom((value, helpers) => {
160
+ const injected = this.injectEnvVars(value);
161
+ const { error } = baseSchema.validate(injected);
162
+ if (error) {
163
+ return helpers.error('any.custom', { message: error.message });
164
+ }
165
+ return injected;
166
+ });
167
+ }
168
+ }
169
+
170
+ module.exports = { PluginConfig };
171
+
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Plugin Event System Utilities
3
+ *
4
+ * Provides utilities for plugin event handling, pattern matching,
5
+ * and event routing between plugins and agents.
6
+ */
7
+
8
+ class PluginEventSystem {
9
+ /**
10
+ * Check if an event name matches a handler pattern
11
+ * Supports wildcards and specific patterns (same as agent handlers)
12
+ */
13
+ static matchesEventPattern(eventName, pattern) {
14
+ // Exact match
15
+ if (eventName === pattern) {
16
+ return true;
17
+ }
18
+
19
+ // Wildcard patterns
20
+ if (pattern.includes('*')) {
21
+ const regexPattern = pattern.replace(/\*/g, '.*').replace(/:/g, ':');
22
+ const regex = new RegExp(`^${regexPattern}$`);
23
+ return regex.test(eventName);
24
+ }
25
+
26
+ // Prefix match (e.g., 'plugin:postgres:*' matches 'plugin:postgres:query')
27
+ if (pattern.endsWith(':*')) {
28
+ const prefix = pattern.slice(0, -2);
29
+ return eventName.startsWith(prefix + ':');
30
+ }
31
+
32
+ return false;
33
+ }
34
+
35
+ /**
36
+ * Normalize event name (add plugin prefix if needed)
37
+ */
38
+ static normalizeEventName(eventName, pluginName) {
39
+ if (eventName.startsWith('plugin:')) {
40
+ return eventName;
41
+ }
42
+ return `plugin:${pluginName}:${eventName}`;
43
+ }
44
+
45
+ /**
46
+ * Extract plugin name from event name
47
+ */
48
+ static extractPluginName(eventName) {
49
+ const match = eventName.match(/^plugin:([^:]+):/);
50
+ return match ? match[1] : null;
51
+ }
52
+
53
+ /**
54
+ * Create event name for plugin
55
+ */
56
+ static createPluginEvent(pluginName, eventName) {
57
+ return `plugin:${pluginName}:${eventName}`;
58
+ }
59
+
60
+ /**
61
+ * Create tool event name
62
+ */
63
+ static createToolEvent(toolName, action) {
64
+ return `tool:${toolName}:${action}`;
65
+ }
66
+
67
+ /**
68
+ * Create plugin tool event name
69
+ */
70
+ static createPluginToolEvent(pluginName, toolName, action) {
71
+ return `plugin:${pluginName}:tool:${toolName}:${action}`;
72
+ }
73
+
74
+ /**
75
+ * Find all matching handlers for an event
76
+ */
77
+ static findMatchingHandlers(eventName, handlersMap) {
78
+ const matchingHandlers = [];
79
+
80
+ for (const [handlerPattern, handlers] of handlersMap) {
81
+ if (this.matchesEventPattern(eventName, handlerPattern)) {
82
+ matchingHandlers.push(...handlers);
83
+ }
84
+ }
85
+
86
+ return matchingHandlers;
87
+ }
88
+
89
+ /**
90
+ * Execute handlers and collect responses
91
+ */
92
+ static async executeHandlers(handlers, data) {
93
+ let modifiedData = { ...data };
94
+
95
+ for (const handlerObj of handlers) {
96
+ try {
97
+ const handler = typeof handlerObj === 'function'
98
+ ? handlerObj
99
+ : handlerObj.handler;
100
+
101
+ if (typeof handler !== 'function') {
102
+ continue;
103
+ }
104
+
105
+ const result = await handler(modifiedData);
106
+
107
+ // If handler returns an object, merge it with the current data
108
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
109
+ modifiedData = { ...modifiedData, ...result };
110
+ }
111
+ } catch (error) {
112
+ console.error(`Error in event handler:`, error);
113
+ // Continue executing other handlers even if one fails
114
+ }
115
+ }
116
+
117
+ return modifiedData;
118
+ }
119
+
120
+ /**
121
+ * Emit event to multiple targets
122
+ */
123
+ static emitToTargets(targets, eventName, data) {
124
+ targets.forEach(target => {
125
+ if (target && typeof target.emit === 'function') {
126
+ target.emit(eventName, data);
127
+ }
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Create event router for plugin-to-plugin communication
133
+ */
134
+ static createEventRouter() {
135
+ const routes = new Map();
136
+
137
+ return {
138
+ /**
139
+ * Register a route from event pattern to target
140
+ */
141
+ route(pattern, target) {
142
+ if (!routes.has(pattern)) {
143
+ routes.set(pattern, []);
144
+ }
145
+ routes.get(pattern).push(target);
146
+ },
147
+
148
+ /**
149
+ * Remove a route
150
+ */
151
+ unroute(pattern, target) {
152
+ if (routes.has(pattern)) {
153
+ const targets = routes.get(pattern);
154
+ const index = targets.indexOf(target);
155
+ if (index !== -1) {
156
+ targets.splice(index, 1);
157
+ }
158
+ }
159
+ },
160
+
161
+ /**
162
+ * Route an event
163
+ */
164
+ emit(eventName, data) {
165
+ for (const [pattern, targets] of routes) {
166
+ if (this.matchesEventPattern(eventName, pattern)) {
167
+ this.emitToTargets(targets, eventName, data);
168
+ }
169
+ }
170
+ },
171
+
172
+ /**
173
+ * Get all routes
174
+ */
175
+ getRoutes() {
176
+ return Array.from(routes.entries()).map(([pattern, targets]) => ({
177
+ pattern,
178
+ targets: targets.length
179
+ }));
180
+ }
181
+ };
182
+ }
183
+ }
184
+
185
+ module.exports = { PluginEventSystem };
186
+
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Dank Plugin System - Main Exports
3
+ *
4
+ * Provides the plugin system infrastructure for extending Dank agents
5
+ * with third-party plugins for databases, vector stores, and other services.
6
+ */
7
+
8
+ const { PluginBase } = require('./base');
9
+ const { PluginRegistry } = require('./registry');
10
+ const { PluginManager } = require('./manager');
11
+ const { PluginConfig } = require('./config');
12
+ const { PluginEventSystem } = require('./events');
13
+
14
+ module.exports = {
15
+ // Core classes
16
+ PluginBase,
17
+ PluginRegistry,
18
+ PluginManager,
19
+ PluginConfig,
20
+ PluginEventSystem,
21
+
22
+ // Convenience exports
23
+ createPlugin: (name, config) => {
24
+ // This is a helper for creating plugins, but typically plugins
25
+ // should extend PluginBase directly
26
+ throw new Error('Plugins must extend PluginBase. Use: class MyPlugin extends PluginBase { ... }');
27
+ }
28
+ };
29
+