dank-ai 1.0.41 → 1.0.45

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.
@@ -1051,17 +1051,47 @@ class DockerManager {
1051
1051
  return new Promise((resolve) => setTimeout(resolve, ms));
1052
1052
  }
1053
1053
 
1054
+ /**
1055
+ * Escape special characters in environment variable values for Dockerfile ENV statements
1056
+ * Handles newlines, quotes, backslashes, and Docker variable syntax
1057
+ *
1058
+ * @param {string} value - The environment variable value to escape
1059
+ * @returns {string} - Escaped value safe for Dockerfile ENV statements
1060
+ */
1061
+ escapeDockerfileEnvValue(value) {
1062
+ if (value === null || value === undefined) {
1063
+ return '';
1064
+ }
1065
+
1066
+ return String(value)
1067
+ // Escape backslashes first (must be first to avoid double-escaping)
1068
+ .replace(/\\/g, '\\\\')
1069
+ // Escape dollar signs for Dockerfile variable syntax ($VAR becomes $$VAR)
1070
+ .replace(/\$/g, '$$$$')
1071
+ // Escape double quotes
1072
+ .replace(/"/g, '\\"')
1073
+ // Escape newlines as \n (will be interpreted as newline when container reads env var)
1074
+ .replace(/\n/g, '\\n')
1075
+ // Escape carriage returns as \r
1076
+ .replace(/\r/g, '\\r')
1077
+ // Escape tabs as \t
1078
+ .replace(/\t/g, '\\t');
1079
+ }
1080
+
1054
1081
  /**
1055
1082
  * Pull the base Docker image
1056
1083
  */
1057
1084
  async pullBaseImage(baseImageName = null, options = {}) {
1058
1085
  const imageName = baseImageName || this.defaultBaseImageName;
1059
- this.logger.info(`Pulling base Docker image: ${imageName}`);
1086
+ this.logger.info(`Pulling base Docker image: ${imageName} (platform: linux/amd64)`);
1060
1087
 
1061
1088
  try {
1062
- const stream = await this.docker.pull(imageName);
1063
-
1064
- await this.followPullProgress(stream, "Base image pull");
1089
+ // Use docker CLI with --platform to ensure we pull the AMD64 version
1090
+ // This is necessary because dockerode's pull doesn't support platform directly
1091
+ const dockerCmd = await this.resolveDockerCommand();
1092
+ const pullCommand = `${dockerCmd} pull --platform linux/amd64 ${imageName}`;
1093
+
1094
+ await this.runCommand(pullCommand, `Pull base image ${imageName}`);
1065
1095
 
1066
1096
  // Verify the image was pulled
1067
1097
  const hasImage = await this.hasImage(imageName);
@@ -1165,13 +1195,21 @@ class DockerManager {
1165
1195
  const imageName = `dank-agent-${normalizedName}`;
1166
1196
  this.logger.info(`Building image for agent: ${agent.name}`);
1167
1197
 
1198
+ // Initialize plugins before finalizing
1199
+ // This ensures plugin handlers and tools are available during code generation
1200
+ if (agent._initializePlugins) {
1201
+ await agent._initializePlugins();
1202
+ }
1203
+
1168
1204
  // Finalize agent configuration before building
1169
1205
  // This ensures ports and other configs are properly set
1170
1206
  agent.finalize();
1171
1207
 
1172
1208
  try {
1173
1209
  const buildContext = await this.createAgentBuildContext(agent, {
1174
- projectDir: options.projectDir
1210
+ projectDir: options.projectDir,
1211
+ configPath: options.configPath,
1212
+ projectRoot: options.projectRoot
1175
1213
  });
1176
1214
  const dockerCmd = await this.resolveDockerCommand();
1177
1215
 
@@ -1224,6 +1262,8 @@ class DockerManager {
1224
1262
  push = false,
1225
1263
  baseImageOverride = null, // Production-only: override base image for all agents
1226
1264
  projectDir = null, // Project directory to copy files from
1265
+ configPath = null, // Config path for extracting top-level requires
1266
+ projectRoot = null, // Project root for package.json location
1227
1267
  } = options;
1228
1268
 
1229
1269
  // Normalize all components
@@ -1250,6 +1290,12 @@ class DockerManager {
1250
1290
  `Building production image for agent: ${agent.name} -> ${imageName}`
1251
1291
  );
1252
1292
 
1293
+ // Initialize plugins before finalizing
1294
+ // This ensures plugin handlers and tools are available during code generation
1295
+ if (agent._initializePlugins) {
1296
+ await agent._initializePlugins();
1297
+ }
1298
+
1253
1299
  // Finalize agent configuration before building
1254
1300
  // This ensures ports and other configs are properly set
1255
1301
  agent.finalize();
@@ -1263,7 +1309,9 @@ class DockerManager {
1263
1309
  const buildContext = await this.createAgentBuildContext(agent, {
1264
1310
  isProductionBuild: true,
1265
1311
  baseImageOverride: baseImageOverride,
1266
- projectDir: projectDir
1312
+ projectDir: projectDir,
1313
+ configPath: configPath,
1314
+ projectRoot: projectRoot
1267
1315
  });
1268
1316
  const dockerCmd = await this.resolveDockerCommand();
1269
1317
 
@@ -1298,6 +1346,65 @@ class DockerManager {
1298
1346
  }
1299
1347
  }
1300
1348
 
1349
+ /**
1350
+ * Extract top-level require statements from a config file
1351
+ * This allows handlers to reference modules imported at the top of the config
1352
+ * @param {string} configPath - Path to dank.config.js
1353
+ * @returns {string[]} - Array of require statement strings
1354
+ */
1355
+ extractTopLevelRequires(configPath) {
1356
+ try {
1357
+ const content = fs.readFileSync(configPath, 'utf8');
1358
+ const imports = [];
1359
+
1360
+ // Regex 1: Match CommonJS require() patterns
1361
+ // - const/let/var declaration
1362
+ // - Simple name (axios) or destructured ({ get, post }) or destructured with rename ({ get: httpGet })
1363
+ // - Single or double quotes for module path
1364
+ // - Optional semicolon at end
1365
+ // - Handles multi-line destructured imports via [\s\S]*?
1366
+ const requireRegex = /^(const|let|var)\s+(\{[\s\S]*?\}|\w+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\);?/gm;
1367
+
1368
+ let match;
1369
+ while ((match = requireRegex.exec(content)) !== null) {
1370
+ let statement = match[0];
1371
+ if (!statement.endsWith(';')) {
1372
+ statement += ';';
1373
+ }
1374
+ imports.push(statement);
1375
+ }
1376
+
1377
+ // Regex 2: Match dynamic import() patterns (for ESM-only packages in CommonJS)
1378
+ // Handles:
1379
+ // - const x = import("y");
1380
+ // - const x = import("y").then((m) => m.default);
1381
+ // - const x = import("y").then((m) => { return m.default; });
1382
+ // - Multiline .then() callbacks
1383
+ // - Single or double quotes
1384
+ const dynamicImportRegex = /^(const|let|var)\s+\w+\s*=\s*import\s*\(\s*['"][^'"]+['"]\s*\)[\s\S]*?;/gm;
1385
+
1386
+ while ((match = dynamicImportRegex.exec(content)) !== null) {
1387
+ let statement = match[0];
1388
+ // Clean up extra whitespace in multiline statements
1389
+ statement = statement.replace(/\n\s+/g, '\n ');
1390
+ if (!statement.endsWith(';')) {
1391
+ statement += ';';
1392
+ }
1393
+ imports.push(statement);
1394
+ }
1395
+
1396
+ const requireCount = imports.filter(s => s.includes('require(')).length;
1397
+ const importCount = imports.filter(s => s.includes('import(')).length;
1398
+
1399
+ this.logger.info(`📦 Extracted ${imports.length} top-level imports from config (${requireCount} require, ${importCount} dynamic import)`);
1400
+
1401
+ return imports;
1402
+ } catch (error) {
1403
+ this.logger.warn(`⚠️ Failed to extract imports from config: ${error.message}`);
1404
+ return [];
1405
+ }
1406
+ }
1407
+
1301
1408
  /**
1302
1409
  * Generate handlers code from agent configuration
1303
1410
  */
@@ -1398,6 +1505,12 @@ class DockerManager {
1398
1505
  has_docker_config: !!agent.config.docker
1399
1506
  });
1400
1507
 
1508
+ // Initialize plugins before finalizing
1509
+ // This ensures plugin handlers and tools are available during code generation
1510
+ if (agent._initializePlugins) {
1511
+ await agent._initializePlugins();
1512
+ }
1513
+
1401
1514
  // Finalize agent configuration (auto-detect features)
1402
1515
  // This will validate that agent.id is set (required)
1403
1516
  agent.finalize();
@@ -1433,14 +1546,19 @@ class DockerManager {
1433
1546
  const hasImage = await this.hasImage(imageName);
1434
1547
  if (!hasImage || options.rebuild) {
1435
1548
  await this.buildAgentImage(agent, {
1436
- projectDir: options.projectDir
1549
+ projectDir: options.projectDir,
1550
+ configPath: options.configPath,
1551
+ projectRoot: options.projectRoot
1437
1552
  });
1438
1553
  }
1439
1554
 
1440
1555
  // Prepare container configuration
1556
+ // Platform is set to linux/amd64 to match the build platform
1557
+ // This ensures consistent behavior and allows ARM machines to run via QEMU emulation
1441
1558
  const containerConfig = {
1442
1559
  Image: imageName,
1443
1560
  name: containerName,
1561
+ platform: 'linux/amd64',
1444
1562
  Env: this.prepareEnvironmentVariables(agent),
1445
1563
  HostConfig: {
1446
1564
  Memory: AgentConfig.parseMemory(AgentConfig.getResourcesFromInstanceType(agent.config.instanceType).memory),
@@ -1636,11 +1754,35 @@ class DockerManager {
1636
1754
 
1637
1755
  /**
1638
1756
  * Copy project files to build context (excluding common ignore patterns)
1757
+ * @param {string} projectDir - Directory containing code files to copy
1758
+ * @param {string} contextDir - Build context directory
1759
+ * @param {object} options - Additional options
1760
+ * @param {string} options.projectRoot - Root directory for package.json (defaults to process.cwd())
1639
1761
  */
1640
- async copyProjectFiles(projectDir, contextDir) {
1762
+ async copyProjectFiles(projectDir, contextDir, options = {}) {
1641
1763
  const agentCodeDir = path.join(contextDir, "agent-code");
1642
1764
  await fs.ensureDir(agentCodeDir);
1643
1765
 
1766
+ // Copy package.json and package-lock.json from project root (not projectDir)
1767
+ // This ensures dependencies are installed correctly even for compiled projects
1768
+ // where projectDir might be ./dist but package.json is at root
1769
+ const projectRoot = options.projectRoot || process.cwd();
1770
+ const packageJsonPath = path.join(projectRoot, 'package.json');
1771
+ const packageLockPath = path.join(projectRoot, 'package-lock.json');
1772
+
1773
+ try {
1774
+ if (await fs.pathExists(packageJsonPath)) {
1775
+ await fs.copy(packageJsonPath, path.join(agentCodeDir, 'package.json'));
1776
+ this.logger.info(`📦 Copied package.json from project root`);
1777
+ }
1778
+ if (await fs.pathExists(packageLockPath)) {
1779
+ await fs.copy(packageLockPath, path.join(agentCodeDir, 'package-lock.json'));
1780
+ this.logger.info(`📦 Copied package-lock.json from project root`);
1781
+ }
1782
+ } catch (error) {
1783
+ this.logger.warn(`⚠️ Failed to copy package files: ${error.message}`);
1784
+ }
1785
+
1644
1786
  // Patterns to exclude when copying project files
1645
1787
  const ignorePatterns = [
1646
1788
  'node_modules',
@@ -1654,7 +1796,9 @@ class DockerManager {
1654
1796
  'build',
1655
1797
  '.dank',
1656
1798
  'coverage',
1657
- '.nyc_output'
1799
+ '.nyc_output',
1800
+ 'package.json', // Already copied from project root
1801
+ 'package-lock.json' // Already copied from project root
1658
1802
  ];
1659
1803
 
1660
1804
  try {
@@ -1707,7 +1851,9 @@ class DockerManager {
1707
1851
  // Copy project files if project directory is provided
1708
1852
  // This allows handlers to reference functions from other files
1709
1853
  if (options.projectDir) {
1710
- await this.copyProjectFiles(options.projectDir, contextDir);
1854
+ await this.copyProjectFiles(options.projectDir, contextDir, {
1855
+ projectRoot: options.projectRoot
1856
+ });
1711
1857
  }
1712
1858
 
1713
1859
  // Get the base image for this agent
@@ -1736,7 +1882,7 @@ class DockerManager {
1736
1882
  envStatements = Object.entries(env)
1737
1883
  .map(([key, value]) => {
1738
1884
  // Escape special characters in values for Dockerfile ENV
1739
- const escapedValue = String(value).replace(/\$/g, '$$$$').replace(/"/g, '\\"');
1885
+ const escapedValue = this.escapeDockerfileEnvValue(String(value));
1740
1886
  return `ENV ${key}="${escapedValue}"`;
1741
1887
  })
1742
1888
  .join('\n');
@@ -1745,14 +1891,39 @@ class DockerManager {
1745
1891
  this.logger.info(` - DOCKER_PORT: ${env.DOCKER_PORT || 'not set'}`);
1746
1892
  }
1747
1893
 
1894
+ // Check if package.json exists in project root to determine if we need npm install
1895
+ const projectRoot = options.projectRoot || process.cwd();
1896
+ const hasPackageJson = await fs.pathExists(path.join(projectRoot, 'package.json'));
1897
+
1898
+ // Generate npm install step if package.json exists
1899
+ // We copy package files first for Docker layer caching, then install, then copy code
1900
+ const npmInstallStep = hasPackageJson ? `
1901
+ # Copy dependency files first (for Docker layer caching)
1902
+ COPY agent-code/package.json /app/agent-code/package.json
1903
+ COPY agent-code/package-lock.json* /app/agent-code/
1904
+
1905
+ # Install dependencies
1906
+ WORKDIR /app/agent-code
1907
+ RUN npm ci --production 2>/dev/null || npm install --production
1908
+
1909
+ # Reset working directory
1910
+ WORKDIR /app
1911
+ ` : '';
1912
+
1748
1913
  // Create Dockerfile for agent
1749
1914
  const dockerfile = `FROM ${baseImageName}
1915
+ ${npmInstallStep}
1916
+ # Copy agent code
1750
1917
  COPY agent-code/ /app/agent-code/
1751
1918
  ${envStatements}
1752
1919
  USER dankuser
1753
1920
  `;
1754
1921
 
1755
1922
  await fs.writeFile(path.join(contextDir, "Dockerfile"), dockerfile);
1923
+
1924
+ if (hasPackageJson) {
1925
+ this.logger.info(`📦 Dockerfile includes npm ci step for dependency installation`);
1926
+ }
1756
1927
 
1757
1928
  // Copy agent code if it exists
1758
1929
  const agentCodeDir = path.join(contextDir, "agent-code");
@@ -1763,6 +1934,16 @@ USER dankuser
1763
1934
  const handlersCode = this.generateHandlersCode(agent);
1764
1935
  const routesCode = this.generateRoutesCode(agent);
1765
1936
 
1937
+ // Extract top-level requires from config file for injection into generated code
1938
+ // This solves the closure problem where handlers reference modules imported at top of config
1939
+ const topLevelRequires = options.configPath
1940
+ ? this.extractTopLevelRequires(options.configPath)
1941
+ : [];
1942
+
1943
+ const injectedRequires = topLevelRequires.length > 0
1944
+ ? `// Injected top-level requires from dank.config.js\n${topLevelRequires.join('\n')}\n`
1945
+ : '';
1946
+
1766
1947
  // Check if project files were copied (indicated by presence of files other than index.js)
1767
1948
  const hasProjectFiles = options.projectDir ? true : false;
1768
1949
  const projectFilesNote = hasProjectFiles
@@ -1775,6 +1956,7 @@ USER dankuser
1775
1956
  // Agent: ${agent.name}
1776
1957
  // Generated by Dank Agent Service
1777
1958
 
1959
+ ${injectedRequires}
1778
1960
  ${projectFilesNote}
1779
1961
 
1780
1962
  module.exports = {
package/lib/index.js CHANGED
@@ -9,6 +9,7 @@ const { DankAgent } = require('./agent');
9
9
  const { DankProject } = require('./project');
10
10
  const { AgentConfig } = require('./config');
11
11
  const { SUPPORTED_LLMS, DEFAULT_CONFIG } = require('./constants');
12
+ const { PluginBase, PluginRegistry, PluginManager, PluginConfig, PluginEventSystem } = require('./plugins');
12
13
 
13
14
  module.exports = {
14
15
  // Main classes
@@ -16,6 +17,13 @@ module.exports = {
16
17
  DankProject,
17
18
  AgentConfig,
18
19
 
20
+ // Plugin system
21
+ PluginBase,
22
+ PluginRegistry,
23
+ PluginManager,
24
+ PluginConfig,
25
+ PluginEventSystem,
26
+
19
27
  // Constants
20
28
  SUPPORTED_LLMS,
21
29
  DEFAULT_CONFIG,
@@ -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
+