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.
package/lib/agent.js CHANGED
@@ -6,10 +6,11 @@
6
6
  */
7
7
 
8
8
  const Joi = require('joi');
9
- const { validate: validateUUID } = require('uuid');
9
+ const { validate: validateUUID, v4: uuidv4 } = require('uuid');
10
10
  const { DEFAULT_CONFIG, SUPPORTED_LLMS, DOCKER_CONFIG, INSTANCE_TYPES } = require('./constants');
11
11
  const { ToolRegistry, ToolExecutor } = require('./tools');
12
12
  const builtinTools = require('./tools/builtin');
13
+ const { PluginManager } = require('./plugins');
13
14
 
14
15
  class DankAgent {
15
16
  constructor(name, config = {}) {
@@ -26,6 +27,10 @@ class DankAgent {
26
27
  this.toolRegistry = new ToolRegistry();
27
28
  this.toolExecutor = new ToolExecutor(this.toolRegistry);
28
29
 
30
+ // Initialize plugin system
31
+ this.pluginManager = new PluginManager(this);
32
+ this.pendingPlugins = new Map(); // Store plugins to be loaded during finalization
33
+
29
34
  // Register built-in tools if enabled
30
35
  if (config.enableBuiltinTools !== false) {
31
36
  this.registerBuiltinTools();
@@ -420,6 +425,68 @@ class DankAgent {
420
425
  return this.toolRegistry.toOpenAISchema();
421
426
  }
422
427
 
428
+ /**
429
+ * Add a plugin to this agent
430
+ * Plugin will be loaded and initialized during finalization
431
+ * @param {string|PluginBase} name - Plugin name, npm package, local path, or PluginBase instance
432
+ * @param {object} config - Plugin configuration
433
+ */
434
+ addPlugin(name, config = {}) {
435
+ this.pendingPlugins.set(name, config);
436
+ return this;
437
+ }
438
+
439
+ /**
440
+ * Add multiple plugins at once
441
+ * Plugins will be loaded and initialized during finalization
442
+ * @param {object} plugins - Object mapping plugin names to configs
443
+ */
444
+ addPlugins(plugins) {
445
+ Object.entries(plugins).forEach(([name, config]) => {
446
+ this.pendingPlugins.set(name, config);
447
+ });
448
+ return this;
449
+ }
450
+
451
+ /**
452
+ * Get a plugin by name
453
+ * @param {string} name - Plugin name
454
+ */
455
+ getPlugin(name) {
456
+ return this.pluginManager.getPlugin(name);
457
+ }
458
+
459
+ /**
460
+ * Get all plugins
461
+ */
462
+ getPlugins() {
463
+ return this.pluginManager.getAllPlugins();
464
+ }
465
+
466
+ /**
467
+ * Check if a plugin is loaded
468
+ * @param {string} name - Plugin name
469
+ */
470
+ hasPlugin(name) {
471
+ return this.pluginManager.hasPlugin(name);
472
+ }
473
+
474
+ /**
475
+ * Remove a plugin
476
+ * @param {string} name - Plugin name
477
+ */
478
+ async removePlugin(name) {
479
+ await this.pluginManager.removePlugin(name);
480
+ return this;
481
+ }
482
+
483
+ /**
484
+ * Get plugin manager metadata
485
+ */
486
+ getPluginMetadata() {
487
+ return this.pluginManager.getMetadata();
488
+ }
489
+
423
490
  /**
424
491
  * Register built-in tools
425
492
  */
@@ -467,9 +534,7 @@ class DankAgent {
467
534
  }
468
535
 
469
536
  this.handlers.get(eventType).push({
470
- id: this._generateId(),
471
- handler,
472
- createdAt: new Date().toISOString()
537
+ handler
473
538
  });
474
539
 
475
540
  return this;
@@ -592,6 +657,7 @@ class DankAgent {
592
657
  /**
593
658
  * Finalize agent configuration by auto-detecting features
594
659
  * This should be called before the agent is deployed
660
+ * Plugins will be loaded when the agent is started
595
661
  */
596
662
  finalize() {
597
663
  // Validate that ID is set before finalization (REQUIRED)
@@ -601,6 +667,18 @@ class DankAgent {
601
667
  return this;
602
668
  }
603
669
 
670
+ /**
671
+ * Initialize plugins (called internally when agent starts)
672
+ * @private
673
+ */
674
+ async _initializePlugins() {
675
+ if (this.pendingPlugins.size > 0) {
676
+ const plugins = Object.fromEntries(this.pendingPlugins);
677
+ await this.pluginManager.addPlugins(plugins);
678
+ this.pendingPlugins.clear();
679
+ }
680
+ }
681
+
604
682
  /**
605
683
  * Set custom configuration
606
684
  */
@@ -776,8 +854,6 @@ class DankAgent {
776
854
 
777
855
  this.handlers.forEach((handlerList, eventType) => {
778
856
  serialized[eventType] = handlerList.map(h => ({
779
- id: h.id,
780
- createdAt: h.createdAt,
781
857
  // Note: actual function is not serialized
782
858
  hasFunction: typeof h.handler === 'function'
783
859
  }));
package/lib/cli/init.js CHANGED
@@ -126,7 +126,9 @@ async function createPackageJson(npmProjectName, projectPath) {
126
126
  clean: 'dank clean'
127
127
  },
128
128
  dependencies: {
129
- 'dank-ai': '^1.0.0'
129
+ 'dank-ai': '^1.0.0',
130
+ 'axios': '^1.6.0',
131
+ 'date-fns': '^3.0.0'
130
132
  },
131
133
  keywords: ['dank', 'ai', 'agents', 'automation', 'llm'],
132
134
  author: '',
@@ -270,6 +272,7 @@ A Dank AI agent project with modern event handling and Docker orchestration.
270
272
 
271
273
  - 🤖 **AI Agents**: Powered by multiple LLM providers (OpenAI, Anthropic, Google AI)
272
274
  - 🐳 **Docker Integration**: Containerized agents with automatic management
275
+ - 📦 **NPM Packages**: Use any npm package in your handlers with top-level imports
273
276
  - 📡 **Event System**: Real-time event handling for prompts, responses, and tools
274
277
  - 🔧 **Auto-Detection**: Automatically enables features based on usage
275
278
  - 📊 **Monitoring**: Built-in logging and status monitoring
@@ -349,6 +352,27 @@ Each agent in your project has a unique UUIDv4 identifier that is automatically
349
352
  - Once agents register with Dank Cloud services, these IDs become locked to your account
350
353
  - You can generate new UUIDs if needed: \`require('uuid').v4()\`
351
354
 
355
+ ## Using NPM Packages
356
+
357
+ You can use any npm package in your handlers by importing them at the top of \`dank.config.js\`:
358
+
359
+ \`\`\`javascript
360
+ // Import packages at the top
361
+ const axios = require('axios');
362
+ const { format } = require('date-fns');
363
+
364
+ // Use them in your handlers
365
+ .addHandler('request_output', async (data) => {
366
+ const timestamp = format(new Date(), 'yyyy-MM-dd HH:mm');
367
+ await axios.post('https://api.example.com/log', {
368
+ response: data.response,
369
+ timestamp
370
+ });
371
+ })
372
+ \`\`\`
373
+
374
+ Just add any packages you need to your \`package.json\` and run \`npm install\`.
375
+
352
376
  ## Configuration
353
377
 
354
378
  Edit \`dank.config.js\` to:
@@ -173,7 +173,9 @@ async function productionBuildCommand(options) {
173
173
  force: options.force || false,
174
174
  push: options.push || false,
175
175
  baseImageOverride: options.baseImageOverride || null,
176
- projectDir: projectDir // Pass project directory so external files can be copied
176
+ projectDir: projectDir, // Pass project directory so external files can be copied
177
+ configPath: configPath, // Pass config path for extracting top-level requires
178
+ projectRoot: process.cwd() // Pass project root for package.json location
177
179
  };
178
180
 
179
181
  const result = await dockerManager.buildProductionImage(agent, buildOptions);
package/lib/cli/run.js CHANGED
@@ -71,7 +71,9 @@ async function runCommand(options) {
71
71
 
72
72
  const container = await dockerManager.startAgent(agent, {
73
73
  rebuild: !options.noBuild, // Rebuild by default unless --no-build is specified
74
- projectDir: projectDir // Pass project directory so external files can be copied
74
+ projectDir: projectDir, // Pass project directory so external files can be copied
75
+ configPath: configPath, // Pass config path for extracting top-level requires
76
+ projectRoot: process.cwd() // Pass project root for package.json location
75
77
  });
76
78
 
77
79
  console.log(chalk.green(` ✅ ${agent.name} started (${container.id.substring(0, 12)})`));
@@ -1056,12 +1056,15 @@ class DockerManager {
1056
1056
  */
1057
1057
  async pullBaseImage(baseImageName = null, options = {}) {
1058
1058
  const imageName = baseImageName || this.defaultBaseImageName;
1059
- this.logger.info(`Pulling base Docker image: ${imageName}`);
1059
+ this.logger.info(`Pulling base Docker image: ${imageName} (platform: linux/amd64)`);
1060
1060
 
1061
1061
  try {
1062
- const stream = await this.docker.pull(imageName);
1063
-
1064
- await this.followPullProgress(stream, "Base image pull");
1062
+ // Use docker CLI with --platform to ensure we pull the AMD64 version
1063
+ // This is necessary because dockerode's pull doesn't support platform directly
1064
+ const dockerCmd = await this.resolveDockerCommand();
1065
+ const pullCommand = `${dockerCmd} pull --platform linux/amd64 ${imageName}`;
1066
+
1067
+ await this.runCommand(pullCommand, `Pull base image ${imageName}`);
1065
1068
 
1066
1069
  // Verify the image was pulled
1067
1070
  const hasImage = await this.hasImage(imageName);
@@ -1165,13 +1168,21 @@ class DockerManager {
1165
1168
  const imageName = `dank-agent-${normalizedName}`;
1166
1169
  this.logger.info(`Building image for agent: ${agent.name}`);
1167
1170
 
1171
+ // Initialize plugins before finalizing
1172
+ // This ensures plugin handlers and tools are available during code generation
1173
+ if (agent._initializePlugins) {
1174
+ await agent._initializePlugins();
1175
+ }
1176
+
1168
1177
  // Finalize agent configuration before building
1169
1178
  // This ensures ports and other configs are properly set
1170
1179
  agent.finalize();
1171
1180
 
1172
1181
  try {
1173
1182
  const buildContext = await this.createAgentBuildContext(agent, {
1174
- projectDir: options.projectDir
1183
+ projectDir: options.projectDir,
1184
+ configPath: options.configPath,
1185
+ projectRoot: options.projectRoot
1175
1186
  });
1176
1187
  const dockerCmd = await this.resolveDockerCommand();
1177
1188
 
@@ -1224,6 +1235,8 @@ class DockerManager {
1224
1235
  push = false,
1225
1236
  baseImageOverride = null, // Production-only: override base image for all agents
1226
1237
  projectDir = null, // Project directory to copy files from
1238
+ configPath = null, // Config path for extracting top-level requires
1239
+ projectRoot = null, // Project root for package.json location
1227
1240
  } = options;
1228
1241
 
1229
1242
  // Normalize all components
@@ -1250,6 +1263,12 @@ class DockerManager {
1250
1263
  `Building production image for agent: ${agent.name} -> ${imageName}`
1251
1264
  );
1252
1265
 
1266
+ // Initialize plugins before finalizing
1267
+ // This ensures plugin handlers and tools are available during code generation
1268
+ if (agent._initializePlugins) {
1269
+ await agent._initializePlugins();
1270
+ }
1271
+
1253
1272
  // Finalize agent configuration before building
1254
1273
  // This ensures ports and other configs are properly set
1255
1274
  agent.finalize();
@@ -1263,7 +1282,9 @@ class DockerManager {
1263
1282
  const buildContext = await this.createAgentBuildContext(agent, {
1264
1283
  isProductionBuild: true,
1265
1284
  baseImageOverride: baseImageOverride,
1266
- projectDir: projectDir
1285
+ projectDir: projectDir,
1286
+ configPath: configPath,
1287
+ projectRoot: projectRoot
1267
1288
  });
1268
1289
  const dockerCmd = await this.resolveDockerCommand();
1269
1290
 
@@ -1298,6 +1319,65 @@ class DockerManager {
1298
1319
  }
1299
1320
  }
1300
1321
 
1322
+ /**
1323
+ * Extract top-level require statements from a config file
1324
+ * This allows handlers to reference modules imported at the top of the config
1325
+ * @param {string} configPath - Path to dank.config.js
1326
+ * @returns {string[]} - Array of require statement strings
1327
+ */
1328
+ extractTopLevelRequires(configPath) {
1329
+ try {
1330
+ const content = fs.readFileSync(configPath, 'utf8');
1331
+ const imports = [];
1332
+
1333
+ // Regex 1: Match CommonJS require() patterns
1334
+ // - const/let/var declaration
1335
+ // - Simple name (axios) or destructured ({ get, post }) or destructured with rename ({ get: httpGet })
1336
+ // - Single or double quotes for module path
1337
+ // - Optional semicolon at end
1338
+ // - Handles multi-line destructured imports via [\s\S]*?
1339
+ const requireRegex = /^(const|let|var)\s+(\{[\s\S]*?\}|\w+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\);?/gm;
1340
+
1341
+ let match;
1342
+ while ((match = requireRegex.exec(content)) !== null) {
1343
+ let statement = match[0];
1344
+ if (!statement.endsWith(';')) {
1345
+ statement += ';';
1346
+ }
1347
+ imports.push(statement);
1348
+ }
1349
+
1350
+ // Regex 2: Match dynamic import() patterns (for ESM-only packages in CommonJS)
1351
+ // Handles:
1352
+ // - const x = import("y");
1353
+ // - const x = import("y").then((m) => m.default);
1354
+ // - const x = import("y").then((m) => { return m.default; });
1355
+ // - Multiline .then() callbacks
1356
+ // - Single or double quotes
1357
+ const dynamicImportRegex = /^(const|let|var)\s+\w+\s*=\s*import\s*\(\s*['"][^'"]+['"]\s*\)[\s\S]*?;/gm;
1358
+
1359
+ while ((match = dynamicImportRegex.exec(content)) !== null) {
1360
+ let statement = match[0];
1361
+ // Clean up extra whitespace in multiline statements
1362
+ statement = statement.replace(/\n\s+/g, '\n ');
1363
+ if (!statement.endsWith(';')) {
1364
+ statement += ';';
1365
+ }
1366
+ imports.push(statement);
1367
+ }
1368
+
1369
+ const requireCount = imports.filter(s => s.includes('require(')).length;
1370
+ const importCount = imports.filter(s => s.includes('import(')).length;
1371
+
1372
+ this.logger.info(`📦 Extracted ${imports.length} top-level imports from config (${requireCount} require, ${importCount} dynamic import)`);
1373
+
1374
+ return imports;
1375
+ } catch (error) {
1376
+ this.logger.warn(`⚠️ Failed to extract imports from config: ${error.message}`);
1377
+ return [];
1378
+ }
1379
+ }
1380
+
1301
1381
  /**
1302
1382
  * Generate handlers code from agent configuration
1303
1383
  */
@@ -1398,6 +1478,12 @@ class DockerManager {
1398
1478
  has_docker_config: !!agent.config.docker
1399
1479
  });
1400
1480
 
1481
+ // Initialize plugins before finalizing
1482
+ // This ensures plugin handlers and tools are available during code generation
1483
+ if (agent._initializePlugins) {
1484
+ await agent._initializePlugins();
1485
+ }
1486
+
1401
1487
  // Finalize agent configuration (auto-detect features)
1402
1488
  // This will validate that agent.id is set (required)
1403
1489
  agent.finalize();
@@ -1433,14 +1519,19 @@ class DockerManager {
1433
1519
  const hasImage = await this.hasImage(imageName);
1434
1520
  if (!hasImage || options.rebuild) {
1435
1521
  await this.buildAgentImage(agent, {
1436
- projectDir: options.projectDir
1522
+ projectDir: options.projectDir,
1523
+ configPath: options.configPath,
1524
+ projectRoot: options.projectRoot
1437
1525
  });
1438
1526
  }
1439
1527
 
1440
1528
  // Prepare container configuration
1529
+ // Platform is set to linux/amd64 to match the build platform
1530
+ // This ensures consistent behavior and allows ARM machines to run via QEMU emulation
1441
1531
  const containerConfig = {
1442
1532
  Image: imageName,
1443
1533
  name: containerName,
1534
+ platform: 'linux/amd64',
1444
1535
  Env: this.prepareEnvironmentVariables(agent),
1445
1536
  HostConfig: {
1446
1537
  Memory: AgentConfig.parseMemory(AgentConfig.getResourcesFromInstanceType(agent.config.instanceType).memory),
@@ -1636,11 +1727,35 @@ class DockerManager {
1636
1727
 
1637
1728
  /**
1638
1729
  * Copy project files to build context (excluding common ignore patterns)
1730
+ * @param {string} projectDir - Directory containing code files to copy
1731
+ * @param {string} contextDir - Build context directory
1732
+ * @param {object} options - Additional options
1733
+ * @param {string} options.projectRoot - Root directory for package.json (defaults to process.cwd())
1639
1734
  */
1640
- async copyProjectFiles(projectDir, contextDir) {
1735
+ async copyProjectFiles(projectDir, contextDir, options = {}) {
1641
1736
  const agentCodeDir = path.join(contextDir, "agent-code");
1642
1737
  await fs.ensureDir(agentCodeDir);
1643
1738
 
1739
+ // Copy package.json and package-lock.json from project root (not projectDir)
1740
+ // This ensures dependencies are installed correctly even for compiled projects
1741
+ // where projectDir might be ./dist but package.json is at root
1742
+ const projectRoot = options.projectRoot || process.cwd();
1743
+ const packageJsonPath = path.join(projectRoot, 'package.json');
1744
+ const packageLockPath = path.join(projectRoot, 'package-lock.json');
1745
+
1746
+ try {
1747
+ if (await fs.pathExists(packageJsonPath)) {
1748
+ await fs.copy(packageJsonPath, path.join(agentCodeDir, 'package.json'));
1749
+ this.logger.info(`📦 Copied package.json from project root`);
1750
+ }
1751
+ if (await fs.pathExists(packageLockPath)) {
1752
+ await fs.copy(packageLockPath, path.join(agentCodeDir, 'package-lock.json'));
1753
+ this.logger.info(`📦 Copied package-lock.json from project root`);
1754
+ }
1755
+ } catch (error) {
1756
+ this.logger.warn(`⚠️ Failed to copy package files: ${error.message}`);
1757
+ }
1758
+
1644
1759
  // Patterns to exclude when copying project files
1645
1760
  const ignorePatterns = [
1646
1761
  'node_modules',
@@ -1654,7 +1769,9 @@ class DockerManager {
1654
1769
  'build',
1655
1770
  '.dank',
1656
1771
  'coverage',
1657
- '.nyc_output'
1772
+ '.nyc_output',
1773
+ 'package.json', // Already copied from project root
1774
+ 'package-lock.json' // Already copied from project root
1658
1775
  ];
1659
1776
 
1660
1777
  try {
@@ -1707,7 +1824,9 @@ class DockerManager {
1707
1824
  // Copy project files if project directory is provided
1708
1825
  // This allows handlers to reference functions from other files
1709
1826
  if (options.projectDir) {
1710
- await this.copyProjectFiles(options.projectDir, contextDir);
1827
+ await this.copyProjectFiles(options.projectDir, contextDir, {
1828
+ projectRoot: options.projectRoot
1829
+ });
1711
1830
  }
1712
1831
 
1713
1832
  // Get the base image for this agent
@@ -1745,14 +1864,39 @@ class DockerManager {
1745
1864
  this.logger.info(` - DOCKER_PORT: ${env.DOCKER_PORT || 'not set'}`);
1746
1865
  }
1747
1866
 
1867
+ // Check if package.json exists in project root to determine if we need npm install
1868
+ const projectRoot = options.projectRoot || process.cwd();
1869
+ const hasPackageJson = await fs.pathExists(path.join(projectRoot, 'package.json'));
1870
+
1871
+ // Generate npm install step if package.json exists
1872
+ // We copy package files first for Docker layer caching, then install, then copy code
1873
+ const npmInstallStep = hasPackageJson ? `
1874
+ # Copy dependency files first (for Docker layer caching)
1875
+ COPY agent-code/package.json /app/agent-code/package.json
1876
+ COPY agent-code/package-lock.json* /app/agent-code/
1877
+
1878
+ # Install dependencies
1879
+ WORKDIR /app/agent-code
1880
+ RUN npm ci --production 2>/dev/null || npm install --production
1881
+
1882
+ # Reset working directory
1883
+ WORKDIR /app
1884
+ ` : '';
1885
+
1748
1886
  // Create Dockerfile for agent
1749
1887
  const dockerfile = `FROM ${baseImageName}
1888
+ ${npmInstallStep}
1889
+ # Copy agent code
1750
1890
  COPY agent-code/ /app/agent-code/
1751
1891
  ${envStatements}
1752
1892
  USER dankuser
1753
1893
  `;
1754
1894
 
1755
1895
  await fs.writeFile(path.join(contextDir, "Dockerfile"), dockerfile);
1896
+
1897
+ if (hasPackageJson) {
1898
+ this.logger.info(`📦 Dockerfile includes npm ci step for dependency installation`);
1899
+ }
1756
1900
 
1757
1901
  // Copy agent code if it exists
1758
1902
  const agentCodeDir = path.join(contextDir, "agent-code");
@@ -1763,6 +1907,16 @@ USER dankuser
1763
1907
  const handlersCode = this.generateHandlersCode(agent);
1764
1908
  const routesCode = this.generateRoutesCode(agent);
1765
1909
 
1910
+ // Extract top-level requires from config file for injection into generated code
1911
+ // This solves the closure problem where handlers reference modules imported at top of config
1912
+ const topLevelRequires = options.configPath
1913
+ ? this.extractTopLevelRequires(options.configPath)
1914
+ : [];
1915
+
1916
+ const injectedRequires = topLevelRequires.length > 0
1917
+ ? `// Injected top-level requires from dank.config.js\n${topLevelRequires.join('\n')}\n`
1918
+ : '';
1919
+
1766
1920
  // Check if project files were copied (indicated by presence of files other than index.js)
1767
1921
  const hasProjectFiles = options.projectDir ? true : false;
1768
1922
  const projectFilesNote = hasProjectFiles
@@ -1775,6 +1929,7 @@ USER dankuser
1775
1929
  // Agent: ${agent.name}
1776
1930
  // Generated by Dank Agent Service
1777
1931
 
1932
+ ${injectedRequires}
1778
1933
  ${projectFilesNote}
1779
1934
 
1780
1935
  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,