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.
@@ -494,10 +494,11 @@ class AgentRuntime {
494
494
  }
495
495
 
496
496
  /**
497
- * Emit an event to all matching handlers
497
+ * Emit an event to all matching handlers (fire-and-forget)
498
+ * Supports both sync and async handlers
498
499
  * Supports pattern matching for tool events
499
500
  */
500
- emitEvent(eventName, data = null) {
501
+ async emitEvent(eventName, data = null) {
501
502
  // Find all matching handlers (exact match and pattern match)
502
503
  const matchingHandlers = [];
503
504
 
@@ -507,19 +508,27 @@ class AgentRuntime {
507
508
  }
508
509
  }
509
510
 
510
- // Execute all matching handlers
511
- matchingHandlers.forEach((handler) => {
511
+ // Execute all matching handlers (in parallel for fire-and-forget)
512
+ const promises = matchingHandlers.map(async (handler) => {
512
513
  try {
514
+ let result;
513
515
  if (typeof handler === "function") {
514
- handler(data);
516
+ result = handler(data);
515
517
  } else if (handler.handler && typeof handler.handler === "function") {
516
- handler.handler(data);
518
+ result = handler.handler(data);
519
+ }
520
+ // Await if handler returns a promise
521
+ if (result && typeof result.then === "function") {
522
+ await result;
517
523
  }
518
524
  } catch (error) {
519
525
  logger.error(`Error in event handler for '${eventName}':`, error);
520
526
  }
521
527
  });
522
528
 
529
+ // Wait for all handlers to complete
530
+ await Promise.all(promises);
531
+
523
532
  if (matchingHandlers.length > 0) {
524
533
  logger.debug(
525
534
  `Emitted event '${eventName}' to ${matchingHandlers.length} handlers`
@@ -529,8 +538,10 @@ class AgentRuntime {
529
538
 
530
539
  /**
531
540
  * Emit an event and collect responses from handlers that return modified data
541
+ * Supports both sync and async handlers
542
+ * Handlers are executed sequentially to allow proper chaining of modifications
532
543
  */
533
- emitEventWithResponse(eventName, data) {
544
+ async emitEventWithResponse(eventName, data) {
534
545
  // Find all matching handlers (exact match and pattern match)
535
546
  const matchingHandlers = [];
536
547
 
@@ -542,8 +553,8 @@ class AgentRuntime {
542
553
 
543
554
  let modifiedData = { ...data };
544
555
 
545
- // Execute all matching handlers and collect responses
546
- matchingHandlers.forEach((handler) => {
556
+ // Execute handlers sequentially to allow chaining of modifications
557
+ for (const handler of matchingHandlers) {
547
558
  try {
548
559
  let result;
549
560
  if (typeof handler === "function") {
@@ -552,6 +563,11 @@ class AgentRuntime {
552
563
  result = handler.handler(modifiedData);
553
564
  }
554
565
 
566
+ // Await if handler returns a promise
567
+ if (result && typeof result.then === "function") {
568
+ result = await result;
569
+ }
570
+
555
571
  // If handler returns an object, merge it with the current data
556
572
  if (result && typeof result === "object" && !Array.isArray(result)) {
557
573
  modifiedData = { ...modifiedData, ...result };
@@ -559,7 +575,7 @@ class AgentRuntime {
559
575
  } catch (error) {
560
576
  logger.error(`Handler error for event '${eventName}':`, error);
561
577
  }
562
- });
578
+ }
563
579
 
564
580
  if (matchingHandlers.length > 0) {
565
581
  logger.debug(
@@ -1193,7 +1209,7 @@ class AgentRuntime {
1193
1209
  this.mainApp.post("/prompt", async (req, res) => {
1194
1210
  logger.info(`📥 Received POST /prompt request from ${req.ip || 'unknown'}`);
1195
1211
  try {
1196
- const { prompt, conversationId, metadata } = req.body;
1212
+ const { prompt, ...requestBodyFields } = req.body;
1197
1213
 
1198
1214
  if (!prompt) {
1199
1215
  return res.status(400).json({
@@ -1202,16 +1218,18 @@ class AgentRuntime {
1202
1218
  });
1203
1219
  }
1204
1220
 
1205
- const response = await this.processDirectPrompt(prompt, {
1206
- conversationId,
1207
- metadata,
1221
+ // Build metadata object with all user-provided fields from request body (except prompt)
1222
+ const metadata = {
1223
+ ...requestBodyFields,
1224
+ };
1225
+
1226
+ const response = await this.processDirectPrompt(prompt, metadata, {
1208
1227
  protocol: "http",
1209
1228
  clientIp: req.ip,
1210
1229
  });
1211
1230
 
1212
1231
  res.json({
1213
1232
  response: response.content,
1214
- conversationId: conversationId || response.conversationId,
1215
1233
  metadata: response.metadata,
1216
1234
  timestamp: new Date().toISOString(),
1217
1235
  });
@@ -1233,24 +1251,23 @@ class AgentRuntime {
1233
1251
  /**
1234
1252
  * Process a direct prompt and emit events
1235
1253
  */
1236
- async processDirectPrompt(prompt, context = {}) {
1254
+ async processDirectPrompt(prompt, metadata = {}, systemFields = {}) {
1237
1255
  const startTime = Date.now();
1238
- const conversationId = context.conversationId || require("uuid").v4();
1256
+ let finalPrompt = prompt; // Declare outside try block so it's available in catch
1239
1257
 
1240
1258
  try {
1241
1259
  // Emit request start event and allow handlers to modify the prompt
1242
1260
  const startEventData = {
1243
1261
  prompt,
1244
- conversationId,
1245
- context,
1262
+ metadata,
1263
+ ...systemFields, // protocol, clientIp, etc.
1246
1264
  timestamp: new Date().toISOString(),
1247
1265
  };
1248
1266
 
1249
- const modifiedData = this.emitEventWithResponse("request_output:start", startEventData);
1267
+ const modifiedData = await this.emitEventWithResponse("request_output:start", startEventData);
1250
1268
 
1251
1269
  // Use modified prompt if handlers returned one, otherwise use original
1252
- const finalPrompt = modifiedData?.prompt || prompt;
1253
-
1270
+ finalPrompt = modifiedData?.prompt || prompt;
1254
1271
  // Process with LLM
1255
1272
  let response;
1256
1273
  if (this.llmProvider === "openai") {
@@ -1281,12 +1298,12 @@ class AgentRuntime {
1281
1298
  const processingTime = Date.now() - startTime;
1282
1299
 
1283
1300
  // Emit request output event
1284
- this.emitEvent("request_output", {
1301
+ await this.emitEvent("request_output", {
1285
1302
  prompt,
1286
1303
  finalPrompt, // Include the final prompt that was sent to LLM
1287
1304
  response: response.content,
1288
- conversationId,
1289
- context,
1305
+ metadata,
1306
+ ...systemFields, // protocol, clientIp, etc.
1290
1307
  usage: response.usage,
1291
1308
  model: response.model,
1292
1309
  processingTime,
@@ -1296,11 +1313,11 @@ class AgentRuntime {
1296
1313
 
1297
1314
  // Emit end event and allow handlers to modify the response before returning
1298
1315
  const endEventData = {
1299
- conversationId,
1300
1316
  prompt,
1301
1317
  finalPrompt,
1302
1318
  response: response.content,
1303
- context,
1319
+ metadata,
1320
+ ...systemFields, // protocol, clientIp, etc.
1304
1321
  usage: response.usage,
1305
1322
  model: response.model,
1306
1323
  processingTime,
@@ -1309,14 +1326,13 @@ class AgentRuntime {
1309
1326
  timestamp: new Date().toISOString(),
1310
1327
  };
1311
1328
 
1312
- const modifiedEndData = this.emitEventWithResponse("request_output:end", endEventData);
1329
+ const modifiedEndData = await this.emitEventWithResponse("request_output:end", endEventData);
1313
1330
 
1314
1331
  // Use the final response (potentially modified by handlers)
1315
1332
  const finalResponse = modifiedEndData.response || response.content;
1316
1333
 
1317
1334
  return {
1318
1335
  content: finalResponse,
1319
- conversationId,
1320
1336
  metadata: {
1321
1337
  usage: response.usage,
1322
1338
  model: response.model,
@@ -1328,11 +1344,11 @@ class AgentRuntime {
1328
1344
  const processingTime = Date.now() - startTime;
1329
1345
 
1330
1346
  // Emit error event
1331
- this.emitEvent("request_output:error", {
1347
+ await this.emitEvent("request_output:error", {
1332
1348
  prompt,
1333
1349
  finalPrompt,
1334
- conversationId,
1335
- context,
1350
+ metadata,
1351
+ ...systemFields, // protocol, clientIp, etc.
1336
1352
  error: error.message,
1337
1353
  processingTime,
1338
1354
  promptModified: finalPrompt !== prompt,
package/lib/agent.js CHANGED
@@ -10,6 +10,7 @@ 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
  */
@@ -590,6 +657,7 @@ class DankAgent {
590
657
  /**
591
658
  * Finalize agent configuration by auto-detecting features
592
659
  * This should be called before the agent is deployed
660
+ * Plugins will be loaded when the agent is started
593
661
  */
594
662
  finalize() {
595
663
  // Validate that ID is set before finalization (REQUIRED)
@@ -599,6 +667,18 @@ class DankAgent {
599
667
  return this;
600
668
  }
601
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
+
602
682
  /**
603
683
  * Set custom configuration
604
684
  */
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)})`));