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.
- package/README.md +490 -1365
- package/docker/entrypoint.js +48 -32
- package/lib/agent.js +80 -0
- package/lib/cli/init.js +25 -1
- package/lib/cli/production-build.js +3 -1
- package/lib/cli/run.js +3 -1
- package/lib/docker/manager.js +193 -11
- package/lib/index.js +8 -0
- package/lib/plugins/base.js +324 -0
- package/lib/plugins/config.js +171 -0
- package/lib/plugins/events.js +186 -0
- package/lib/plugins/index.js +29 -0
- package/lib/plugins/manager.js +258 -0
- package/lib/plugins/registry.js +268 -0
- package/lib/project.js +15 -8
- package/package.json +1 -1
package/lib/docker/manager.js
CHANGED
|
@@ -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
|
-
|
|
1063
|
-
|
|
1064
|
-
await this.
|
|
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)
|
|
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
|
+
|