dank-ai 1.0.0

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,968 @@
1
+ /**
2
+ * Docker Container Manager
3
+ *
4
+ * Manages Docker containers for Dank agents including:
5
+ * - Building agent images
6
+ * - Starting/stopping containers
7
+ * - Monitoring container health
8
+ * - Managing Docker resources
9
+ */
10
+
11
+ const Docker = require('dockerode');
12
+ const fs = require('fs-extra');
13
+ const path = require('path');
14
+ const tar = require('tar');
15
+ const winston = require('winston');
16
+ const { spawn, exec } = require('child_process');
17
+ const { promisify } = require('util');
18
+ const { DOCKER_CONFIG } = require('../constants');
19
+ const { AgentConfig } = require('../config');
20
+
21
+ const execAsync = promisify(exec);
22
+
23
+ class DockerManager {
24
+ constructor(options = {}) {
25
+ this.docker = new Docker(options.dockerOptions || {});
26
+ this.logger = options.logger || winston.createLogger({
27
+ level: 'info',
28
+ format: winston.format.simple(),
29
+ transports: [new winston.transports.Console()]
30
+ });
31
+
32
+ this.defaultBaseImageName = `${DOCKER_CONFIG.baseImagePrefix}:${DOCKER_CONFIG.defaultTag}`;
33
+ this.networkName = DOCKER_CONFIG.networkName;
34
+ this.containers = new Map();
35
+ }
36
+
37
+ /**
38
+ * Initialize Docker environment
39
+ */
40
+ async initialize() {
41
+ try {
42
+ // Ensure Docker is available and running
43
+ await this.ensureDockerAvailable();
44
+
45
+ // Check Docker connection
46
+ await this.docker.ping();
47
+ this.logger.info('Docker connection established');
48
+
49
+ // Create network if it doesn't exist
50
+ await this.ensureNetwork();
51
+
52
+ // Check if default base image exists, pull if not found
53
+ const hasBaseImage = await this.hasImage(this.defaultBaseImageName);
54
+ if (!hasBaseImage) {
55
+ this.logger.info(`Default base image '${this.defaultBaseImageName}' not found. Pulling from registry...`);
56
+ await this.pullBaseImage();
57
+ }
58
+
59
+ } catch (error) {
60
+ throw new Error(`Failed to initialize Docker: ${error.message}`);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Ensure Docker is installed and running
66
+ */
67
+ async ensureDockerAvailable() {
68
+ try {
69
+ // First, try to ping Docker to see if it's running
70
+ await this.docker.ping();
71
+ this.logger.info('Docker is running and accessible');
72
+ return;
73
+ } catch (error) {
74
+ this.logger.info('Docker is not accessible, checking installation...');
75
+ }
76
+
77
+ // Check if Docker is installed
78
+ const isInstalled = await this.isDockerInstalled();
79
+
80
+ if (!isInstalled) {
81
+ this.logger.info('Docker is not installed. Installing Docker...');
82
+ await this.installDocker();
83
+ } else {
84
+ this.logger.info('Docker is installed but not running. Starting Docker...');
85
+ await this.startDocker();
86
+ }
87
+
88
+ // Wait for Docker to become available
89
+ await this.waitForDocker();
90
+ }
91
+
92
+ /**
93
+ * Check if Docker is installed on the system
94
+ */
95
+ async isDockerInstalled() {
96
+ try {
97
+ await execAsync('docker --version');
98
+ return true;
99
+ } catch (error) {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Install Docker on the system
106
+ */
107
+ async installDocker() {
108
+ const platform = process.platform;
109
+
110
+ this.logger.info(`Installing Docker for ${platform}...`);
111
+
112
+ try {
113
+ if (platform === 'darwin') {
114
+ await this.installDockerMacOS();
115
+ } else if (platform === 'linux') {
116
+ await this.installDockerLinux();
117
+ } else if (platform === 'win32') {
118
+ await this.installDockerWindows();
119
+ } else {
120
+ throw new Error(`Unsupported platform: ${platform}`);
121
+ }
122
+
123
+ this.logger.info('Docker installation completed');
124
+ } catch (error) {
125
+ throw new Error(`Failed to install Docker: ${error.message}`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Install Docker on macOS
131
+ */
132
+ async installDockerMacOS() {
133
+ this.logger.info('Installing Docker Desktop for macOS...');
134
+
135
+ // Check if Homebrew is available
136
+ try {
137
+ await execAsync('which brew');
138
+ this.logger.info('Using Homebrew to install Docker Desktop...');
139
+
140
+ // Install Docker Desktop via Homebrew
141
+ await this.runCommand('brew install --cask docker', 'Installing Docker Desktop via Homebrew');
142
+
143
+ } catch (error) {
144
+ this.logger.warn('Homebrew not found. Please install Docker Desktop manually from https://www.docker.com/products/docker-desktop/');
145
+ throw new Error('Docker Desktop installation requires manual intervention. Please install from https://www.docker.com/products/docker-desktop/');
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Install Docker on Linux
151
+ */
152
+ async installDockerLinux() {
153
+ this.logger.info('Installing Docker on Linux...');
154
+
155
+ try {
156
+ // Update package index
157
+ await this.runCommand('sudo apt-get update', 'Updating package index');
158
+
159
+ // Install prerequisites
160
+ await this.runCommand('sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release', 'Installing prerequisites');
161
+
162
+ // Add Docker's official GPG key
163
+ await this.runCommand('curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg', 'Adding Docker GPG key');
164
+
165
+ // Add Docker repository
166
+ await this.runCommand('echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null', 'Adding Docker repository');
167
+
168
+ // Update package index again
169
+ await this.runCommand('sudo apt-get update', 'Updating package index with Docker repository');
170
+
171
+ // Install Docker
172
+ await this.runCommand('sudo apt-get install -y docker-ce docker-ce-cli containerd.io', 'Installing Docker');
173
+
174
+ // Add current user to docker group
175
+ await this.runCommand('sudo usermod -aG docker $USER', 'Adding user to docker group');
176
+
177
+ this.logger.info('Docker installation completed. You may need to log out and back in for group changes to take effect.');
178
+
179
+ } catch (error) {
180
+ throw new Error(`Failed to install Docker on Linux: ${error.message}`);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Install Docker on Windows
186
+ */
187
+ async installDockerWindows() {
188
+ this.logger.info('Installing Docker Desktop for Windows...');
189
+
190
+ // Check if Chocolatey is available
191
+ try {
192
+ await execAsync('choco --version');
193
+ this.logger.info('Using Chocolatey to install Docker Desktop...');
194
+
195
+ // Install Docker Desktop via Chocolatey
196
+ await this.runCommand('choco install docker-desktop -y', 'Installing Docker Desktop via Chocolatey');
197
+
198
+ } catch (error) {
199
+ this.logger.warn('Chocolatey not found. Please install Docker Desktop manually from https://www.docker.com/products/docker-desktop/');
200
+ throw new Error('Docker Desktop installation requires manual intervention. Please install from https://www.docker.com/products/docker-desktop/');
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Start Docker service
206
+ */
207
+ async startDocker() {
208
+ const platform = process.platform;
209
+
210
+ try {
211
+ if (platform === 'darwin') {
212
+ // On macOS, try to start Docker Desktop
213
+ await this.runCommand('open -a Docker', 'Starting Docker Desktop');
214
+ } else if (platform === 'linux') {
215
+ // On Linux, start Docker service
216
+ await this.runCommand('sudo systemctl start docker', 'Starting Docker service');
217
+ await this.runCommand('sudo systemctl enable docker', 'Enabling Docker service');
218
+ } else if (platform === 'win32') {
219
+ // On Windows, try to start Docker Desktop
220
+ await this.runCommand('start "" "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"', 'Starting Docker Desktop');
221
+ }
222
+
223
+ this.logger.info('Docker service started');
224
+ } catch (error) {
225
+ this.logger.warn(`Failed to start Docker service: ${error.message}`);
226
+ this.logger.info('Please start Docker manually and try again');
227
+ throw error;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Wait for Docker to become available
233
+ */
234
+ async waitForDocker() {
235
+ this.logger.info('Waiting for Docker to become available...');
236
+
237
+ const maxAttempts = 30; // 30 seconds
238
+ const delay = 1000; // 1 second
239
+
240
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
241
+ try {
242
+ await this.docker.ping();
243
+ this.logger.info('Docker is now available');
244
+ return;
245
+ } catch (error) {
246
+ if (attempt === maxAttempts) {
247
+ throw new Error('Docker did not become available within the expected time');
248
+ }
249
+
250
+ this.logger.info(`Waiting for Docker... (${attempt}/${maxAttempts})`);
251
+ await this.sleep(delay);
252
+ }
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Run a command and log output
258
+ */
259
+ async runCommand(command, description) {
260
+ this.logger.info(`${description}...`);
261
+
262
+ return new Promise((resolve, reject) => {
263
+ const child = spawn('sh', ['-c', command], {
264
+ stdio: ['inherit', 'pipe', 'pipe'],
265
+ shell: true
266
+ });
267
+
268
+ let stdout = '';
269
+ let stderr = '';
270
+
271
+ child.stdout.on('data', (data) => {
272
+ stdout += data.toString();
273
+ });
274
+
275
+ child.stderr.on('data', (data) => {
276
+ stderr += data.toString();
277
+ });
278
+
279
+ child.on('close', (code) => {
280
+ if (code === 0) {
281
+ this.logger.info(`${description} completed successfully`);
282
+ resolve({ stdout, stderr });
283
+ } else {
284
+ const error = new Error(`Command failed with exit code ${code}: ${stderr}`);
285
+ this.logger.error(`${description} failed: ${error.message}`);
286
+ reject(error);
287
+ }
288
+ });
289
+
290
+ child.on('error', (error) => {
291
+ this.logger.error(`${description} failed: ${error.message}`);
292
+ reject(error);
293
+ });
294
+ });
295
+ }
296
+
297
+ /**
298
+ * Sleep utility
299
+ */
300
+ sleep(ms) {
301
+ return new Promise(resolve => setTimeout(resolve, ms));
302
+ }
303
+
304
+ /**
305
+ * Pull the base Docker image
306
+ */
307
+ async pullBaseImage(baseImageName = null, options = {}) {
308
+ const imageName = baseImageName || this.defaultBaseImageName;
309
+ this.logger.info(`Pulling base Docker image: ${imageName}`);
310
+
311
+ try {
312
+ const stream = await this.docker.pull(imageName);
313
+
314
+ await this.followPullProgress(stream, 'Base image pull');
315
+
316
+ // Verify the image was pulled
317
+ const hasImage = await this.hasImage(imageName);
318
+ if (hasImage) {
319
+ this.logger.info(`Base image '${imageName}' pulled successfully`);
320
+ } else {
321
+ throw new Error(`Base image '${imageName}' was not pulled successfully`);
322
+ }
323
+
324
+ } catch (error) {
325
+ throw new Error(`Failed to pull base image: ${error.message}`);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Clean up existing containers from previous runs
331
+ */
332
+ async cleanupExistingContainers(agents) {
333
+ this.logger.info('Cleaning up existing containers from previous runs...');
334
+
335
+ try {
336
+ // Get all containers (running and stopped) that match our agent naming pattern
337
+ const containers = await this.docker.listContainers({ all: true });
338
+
339
+ const agentNames = agents.map(agent => agent.name.toLowerCase());
340
+ const containersToCleanup = containers.filter(container => {
341
+ // Check if container name matches our dank agent pattern
342
+ const containerName = container.Names[0].replace(/^\//, ''); // Remove leading slash
343
+ return agentNames.some(agentName =>
344
+ containerName.startsWith(`dank-${agentName}-`) ||
345
+ containerName === `dank-${agentName}`
346
+ );
347
+ });
348
+
349
+ if (containersToCleanup.length === 0) {
350
+ this.logger.info('No existing containers found to cleanup');
351
+ return;
352
+ }
353
+
354
+ this.logger.info(`Found ${containersToCleanup.length} existing containers to cleanup`);
355
+
356
+ // Stop and remove each container
357
+ for (const containerInfo of containersToCleanup) {
358
+ const container = this.docker.getContainer(containerInfo.Id);
359
+ const containerName = containerInfo.Names[0].replace(/^\//, '');
360
+
361
+ try {
362
+ // Stop container if running
363
+ if (containerInfo.State === 'running') {
364
+ this.logger.info(`Stopping container: ${containerName}`);
365
+ await container.stop({ t: 10 }); // 10 second timeout
366
+ }
367
+
368
+ // Remove container
369
+ this.logger.info(`Removing container: ${containerName}`);
370
+ await container.remove({ force: true });
371
+
372
+ } catch (error) {
373
+ // Log but don't fail if we can't clean up a specific container
374
+ this.logger.warn(`Failed to cleanup container ${containerName}: ${error.message}`);
375
+ }
376
+ }
377
+
378
+ this.logger.info('Container cleanup completed');
379
+
380
+ } catch (error) {
381
+ this.logger.error('Failed to cleanup existing containers:', error.message);
382
+ // Don't throw - we want to continue even if cleanup fails
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Build agent-specific image
388
+ */
389
+ async buildAgentImage(agent, options = {}) {
390
+ const imageName = `dank-agent-${agent.name.toLowerCase()}`;
391
+ this.logger.info(`Building image for agent: ${agent.name}`);
392
+
393
+ try {
394
+ const buildContext = await this.createAgentBuildContext(agent);
395
+
396
+ const stream = await this.docker.buildImage(buildContext, {
397
+ t: imageName,
398
+ dockerfile: 'Dockerfile',
399
+ nocache: options.rebuild || options.noCache || false
400
+ });
401
+
402
+ await this.followBuildProgress(stream, `Agent ${agent.name} build`);
403
+
404
+ this.logger.info(`Agent image '${imageName}' built successfully`);
405
+
406
+ // Clean up build context
407
+ await fs.remove(buildContext);
408
+
409
+ return imageName;
410
+
411
+ } catch (error) {
412
+ throw new Error(`Failed to build agent image: ${error.message}`);
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Generate handlers code from agent configuration
418
+ */
419
+ generateHandlersCode(agent) {
420
+ const handlers = {};
421
+
422
+ // Add default handlers
423
+ handlers.output = ['(data) => console.log("Output:", data)'];
424
+ handlers.error = ['(error) => console.error("Error:", error)'];
425
+
426
+ // Add custom handlers from agent configuration
427
+ if (agent.handlers && agent.handlers.size > 0) {
428
+ for (const [eventName, handlerList] of agent.handlers) {
429
+ if (!handlers[eventName]) {
430
+ handlers[eventName] = [];
431
+ }
432
+
433
+ // Convert handler functions to string representations
434
+ handlerList.forEach(handlerObj => {
435
+ if (handlerObj && typeof handlerObj.handler === 'function') {
436
+ // Convert function to string, handling the function properly
437
+ const handlerStr = handlerObj.handler.toString();
438
+ handlers[eventName].push(handlerStr);
439
+ }
440
+ });
441
+ }
442
+ }
443
+
444
+ // Generate the JavaScript object code
445
+ const handlersEntries = Object.entries(handlers).map(([eventName, handlerArray]) => {
446
+ const handlersStr = handlerArray.join(',\n ');
447
+ // Quote event names that contain special characters (like colons)
448
+ const quotedEventName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(eventName) ? eventName : `"${eventName}"`;
449
+ return ` ${quotedEventName}: [\n ${handlersStr}\n ]`;
450
+ }).join(',\n');
451
+
452
+ return `{\n${handlersEntries}\n }`;
453
+ }
454
+
455
+ /**
456
+ * Start agent container
457
+ */
458
+ async startAgent(agent, options = {}) {
459
+ // Finalize agent configuration (auto-detect features)
460
+ agent.finalize();
461
+
462
+ const imageName = `dank-agent-${agent.name.toLowerCase()}`;
463
+ const containerName = `dank-${agent.name.toLowerCase()}-${agent.id.split('_').pop()}`;
464
+ const baseImageName = agent.config.docker?.baseImage || this.defaultBaseImageName;
465
+
466
+ try {
467
+ // Ensure base image exists
468
+ const hasBaseImage = await this.hasImage(baseImageName);
469
+ if (!hasBaseImage) {
470
+ this.logger.info(`Base image '${baseImageName}' not found for agent ${agent.name}. Pulling...`);
471
+ await this.pullBaseImage(baseImageName);
472
+ }
473
+
474
+ // Check if agent image exists, build if necessary
475
+ const hasImage = await this.hasImage(imageName);
476
+ if (!hasImage || options.rebuild) {
477
+ await this.buildAgentImage(agent, options);
478
+ }
479
+
480
+ // Prepare container configuration
481
+ const containerConfig = {
482
+ Image: imageName,
483
+ name: containerName,
484
+ Env: this.prepareEnvironmentVariables(agent),
485
+ HostConfig: {
486
+ Memory: AgentConfig.parseMemory(agent.config.resources.memory),
487
+ CpuQuota: Math.floor(agent.config.resources.cpu * 100000),
488
+ CpuPeriod: 100000,
489
+ RestartPolicy: {
490
+ Name: 'on-failure',
491
+ MaximumRetryCount: agent.config.resources.maxRestarts || 3
492
+ },
493
+ NetworkMode: this.networkName,
494
+ ...this.preparePortConfiguration(agent)
495
+ },
496
+ NetworkingConfig: {
497
+ EndpointsConfig: {
498
+ [this.networkName]: {}
499
+ }
500
+ },
501
+ ...this.prepareExposedPorts(agent)
502
+ };
503
+
504
+ // Create container
505
+ this.logger.info(`Creating container for agent: ${agent.name}`);
506
+ const container = await this.docker.createContainer(containerConfig);
507
+
508
+ // Start container
509
+ await container.start();
510
+
511
+ // Store container reference
512
+ this.containers.set(agent.name, {
513
+ container,
514
+ agent,
515
+ startTime: new Date(),
516
+ status: 'running'
517
+ });
518
+
519
+ agent.containerId = container.id;
520
+ agent.status = 'running';
521
+
522
+ this.logger.info(`Agent ${agent.name} started successfully (${container.id.substring(0, 12)})`);
523
+
524
+ return container;
525
+
526
+ } catch (error) {
527
+ agent.status = 'error';
528
+ throw new Error(`Failed to start agent ${agent.name}: ${error.message}`);
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Stop agent container
534
+ */
535
+ async stopAgent(agentName, options = {}) {
536
+ const containerInfo = this.containers.get(agentName);
537
+ if (!containerInfo) {
538
+ throw new Error(`Agent ${agentName} not found or not running`);
539
+ }
540
+
541
+ try {
542
+ const { container, agent } = containerInfo;
543
+
544
+ this.logger.info(`Stopping agent: ${agentName}`);
545
+
546
+ if (options.force) {
547
+ await container.kill();
548
+ } else {
549
+ await container.stop({ t: 10 }); // 10 second timeout
550
+ }
551
+
552
+ await container.remove();
553
+
554
+ this.containers.delete(agentName);
555
+ agent.status = 'stopped';
556
+ agent.containerId = null;
557
+
558
+ this.logger.info(`Agent ${agentName} stopped successfully`);
559
+
560
+ } catch (error) {
561
+ throw new Error(`Failed to stop agent ${agentName}: ${error.message}`);
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Get agent status
567
+ */
568
+ async getAgentStatus(agentName) {
569
+ const containerInfo = this.containers.get(agentName);
570
+ if (!containerInfo) {
571
+ return { status: 'not_running' };
572
+ }
573
+
574
+ try {
575
+ const { container, agent, startTime } = containerInfo;
576
+ const containerData = await container.inspect();
577
+
578
+ return {
579
+ status: containerData.State.Running ? 'running' : 'stopped',
580
+ containerId: container.id,
581
+ startTime,
582
+ uptime: Date.now() - startTime.getTime(),
583
+ health: containerData.State.Health?.Status || 'unknown',
584
+ restartCount: containerData.RestartCount,
585
+ resources: {
586
+ memory: agent.config.resources.memory,
587
+ cpu: agent.config.resources.cpu
588
+ }
589
+ };
590
+
591
+ } catch (error) {
592
+ return { status: 'error', error: error.message };
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Get container logs
598
+ */
599
+ async getAgentLogs(agentName, options = {}) {
600
+ const containerInfo = this.containers.get(agentName);
601
+ if (!containerInfo) {
602
+ throw new Error(`Agent ${agentName} not found`);
603
+ }
604
+
605
+ const { container } = containerInfo;
606
+
607
+ const logStream = await container.logs({
608
+ stdout: true,
609
+ stderr: true,
610
+ follow: options.follow || false,
611
+ tail: options.tail || 100,
612
+ since: options.since || undefined,
613
+ timestamps: true
614
+ });
615
+
616
+ return logStream;
617
+ }
618
+
619
+ /**
620
+ * Create build context for base image
621
+ */
622
+ async createBaseBuildContext() {
623
+ const contextDir = path.join(__dirname, '../../.build-context-base');
624
+ await fs.ensureDir(contextDir);
625
+
626
+ // Copy Docker files
627
+ await fs.copy(path.join(__dirname, '../../docker'), contextDir);
628
+
629
+ // Create runtime directory
630
+ const runtimeDir = path.join(contextDir, 'runtime');
631
+ await fs.ensureDir(runtimeDir);
632
+
633
+ // Create tarball
634
+ const tarPath = path.join(__dirname, '../../.base-build-context.tar');
635
+ await tar.create({
636
+ file: tarPath,
637
+ cwd: contextDir
638
+ }, ['.']);
639
+
640
+ return tarPath;
641
+ }
642
+
643
+ /**
644
+ * Create build context for agent
645
+ */
646
+ async createAgentBuildContext(agent) {
647
+ const contextDir = path.join(__dirname, `../../.build-context-${agent.name}`);
648
+ await fs.ensureDir(contextDir);
649
+
650
+ // Get the base image for this agent
651
+ const baseImageName = agent.config.docker?.baseImage || this.defaultBaseImageName;
652
+
653
+ // Create Dockerfile for agent
654
+ const dockerfile = `FROM ${baseImageName}
655
+ COPY agent-code/ /app/agent-code/
656
+ USER dankuser
657
+ `;
658
+
659
+ await fs.writeFile(path.join(contextDir, 'Dockerfile'), dockerfile);
660
+
661
+ // Copy agent code if it exists
662
+ const agentCodeDir = path.join(contextDir, 'agent-code');
663
+ await fs.ensureDir(agentCodeDir);
664
+
665
+ // Create basic agent code structure
666
+ // Generate handlers from agent configuration
667
+ const handlersCode = this.generateHandlersCode(agent);
668
+
669
+ const agentCode = `
670
+ // Agent: ${agent.name}
671
+ // Generated by Dank Agent Service
672
+
673
+ module.exports = {
674
+ async main(context) {
675
+ const { llmClient, handlers, tools, config } = context;
676
+ console.log('Agent ${agent.name} started');
677
+ console.log('Available context:', Object.keys(context));
678
+
679
+ // Basic agent loop
680
+ setInterval(async () => {
681
+ try {
682
+ // Trigger heartbeat
683
+ const heartbeatHandlers = handlers.get('heartbeat') || [];
684
+ heartbeatHandlers.forEach(handler => {
685
+ try {
686
+ handler();
687
+ } catch (handlerError) {
688
+ console.error('Heartbeat handler error:', handlerError);
689
+ }
690
+ });
691
+
692
+ // Custom agent logic would go here
693
+ console.log('Agent ${agent.name} heartbeat - uptime:', Math.floor(process.uptime()), 'seconds');
694
+
695
+ } catch (error) {
696
+ console.error('Agent loop error:', error);
697
+ const errorHandlers = handlers.get('error') || [];
698
+ errorHandlers.forEach(handler => {
699
+ try {
700
+ handler(error);
701
+ } catch (handlerError) {
702
+ console.error('Error handler failed:', handlerError);
703
+ }
704
+ });
705
+ }
706
+ }, 10000);
707
+ },
708
+
709
+ handlers: ${handlersCode}
710
+ };
711
+ `;
712
+
713
+ await fs.writeFile(path.join(agentCodeDir, 'index.js'), agentCode);
714
+
715
+ // Create tarball
716
+ const tarPath = path.join(__dirname, `../../.agent-${agent.name}-context.tar`);
717
+ await tar.create({
718
+ file: tarPath,
719
+ cwd: contextDir
720
+ }, ['.']);
721
+
722
+ return tarPath;
723
+ }
724
+
725
+ /**
726
+ * Prepare environment variables for container
727
+ */
728
+ prepareEnvironmentVariables(agent) {
729
+ const env = AgentConfig.generateContainerEnv(agent);
730
+ return Object.entries(env).map(([key, value]) => `${key}=${value}`);
731
+ }
732
+
733
+ /**
734
+ * Prepare port configuration for container
735
+ */
736
+ preparePortConfiguration(agent) {
737
+ const portConfig = {};
738
+ const portBindings = {};
739
+
740
+ // Always bind the main agent port
741
+ const mainPort = agent.config.docker?.port || DOCKER_CONFIG.defaultPort;
742
+ portBindings[`${mainPort}/tcp`] = [{ HostPort: mainPort.toString() }];
743
+
744
+ // Also bind HTTP port if HTTP is enabled and different from main port
745
+ if (agent.config.http && agent.config.http.enabled) {
746
+ const httpPort = agent.config.http.port;
747
+ if (httpPort !== mainPort) {
748
+ portBindings[`${httpPort}/tcp`] = [{ HostPort: httpPort.toString() }];
749
+ }
750
+ }
751
+
752
+ // Always bind health check port
753
+ const healthPort = DOCKER_CONFIG.healthCheckPort;
754
+ portBindings[`${healthPort}/tcp`] = [{ HostPort: healthPort.toString() }];
755
+
756
+ portConfig.PortBindings = portBindings;
757
+ return portConfig;
758
+ }
759
+
760
+ /**
761
+ * Prepare exposed ports for container
762
+ */
763
+ prepareExposedPorts(agent) {
764
+ const exposedPorts = {};
765
+
766
+ // Always expose the main agent port
767
+ const mainPort = agent.config.docker?.port || DOCKER_CONFIG.defaultPort;
768
+ exposedPorts[`${mainPort}/tcp`] = {};
769
+
770
+ // Also expose HTTP port if HTTP is enabled and different from main port
771
+ if (agent.config.http && agent.config.http.enabled) {
772
+ const httpPort = agent.config.http.port;
773
+ if (httpPort !== mainPort) {
774
+ exposedPorts[`${httpPort}/tcp`] = {};
775
+ }
776
+ }
777
+
778
+ // Always expose health check port
779
+ const healthPort = DOCKER_CONFIG.healthCheckPort;
780
+ exposedPorts[`${healthPort}/tcp`] = {};
781
+
782
+ return { ExposedPorts: exposedPorts };
783
+ }
784
+
785
+ /**
786
+ * Ensure Docker network exists
787
+ */
788
+ async ensureNetwork() {
789
+ try {
790
+ await this.docker.getNetwork(this.networkName).inspect();
791
+ this.logger.debug(`Network '${this.networkName}' already exists`);
792
+ } catch (error) {
793
+ if (error.statusCode === 404) {
794
+ this.logger.info(`Creating Docker network: ${this.networkName}`);
795
+ await this.docker.createNetwork({
796
+ Name: this.networkName,
797
+ Driver: 'bridge'
798
+ });
799
+ } else {
800
+ throw error;
801
+ }
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Check if Docker image exists
807
+ */
808
+ async hasImage(imageName) {
809
+ try {
810
+ await this.docker.getImage(imageName).inspect();
811
+ return true;
812
+ } catch (error) {
813
+ if (error.statusCode === 404) {
814
+ return false;
815
+ }
816
+ throw error;
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Follow build progress and log output
822
+ */
823
+ async followBuildProgress(stream, buildName) {
824
+ return new Promise((resolve, reject) => {
825
+ this.docker.modem.followProgress(stream, (err, res) => {
826
+ if (err) {
827
+ reject(err);
828
+ } else {
829
+ resolve(res);
830
+ }
831
+ }, (event) => {
832
+ if (event.stream) {
833
+ process.stdout.write(event.stream);
834
+ } else if (event.status) {
835
+ this.logger.debug(`${buildName}: ${event.status}`);
836
+ }
837
+ });
838
+ });
839
+ }
840
+
841
+ /**
842
+ * Follow pull progress and log output
843
+ */
844
+ async followPullProgress(stream, pullName) {
845
+ return new Promise((resolve, reject) => {
846
+ this.docker.modem.followProgress(stream, (err, res) => {
847
+ if (err) {
848
+ reject(err);
849
+ } else {
850
+ resolve(res);
851
+ }
852
+ }, (event) => {
853
+ if (event.status) {
854
+ if (event.progress) {
855
+ process.stdout.write(`\r${pullName}: ${event.status} ${event.progress}`);
856
+ } else {
857
+ this.logger.info(`${pullName}: ${event.status}`);
858
+ }
859
+ }
860
+ });
861
+ });
862
+ }
863
+
864
+ /**
865
+ * Clean up Docker resources
866
+ */
867
+ async cleanup(options = {}) {
868
+ this.logger.info('Cleaning up Docker resources...');
869
+
870
+ try {
871
+ if (options.containers || options.all) {
872
+ // Stop and remove all Dank containers
873
+ const containers = await this.docker.listContainers({
874
+ all: true,
875
+ filters: { name: ['dank-'] }
876
+ });
877
+
878
+ for (const containerInfo of containers) {
879
+ const container = this.docker.getContainer(containerInfo.Id);
880
+ try {
881
+ if (containerInfo.State === 'running') {
882
+ await container.stop();
883
+ }
884
+ await container.remove();
885
+ this.logger.info(`Removed container: ${containerInfo.Names[0]}`);
886
+ } catch (error) {
887
+ this.logger.warn(`Failed to remove container ${containerInfo.Names[0]}: ${error.message}`);
888
+ }
889
+ }
890
+ }
891
+
892
+ if (options.images || options.all) {
893
+ // Remove Dank images
894
+ const images = await this.docker.listImages({
895
+ filters: { reference: ['dank-*'] }
896
+ });
897
+
898
+ for (const imageInfo of images) {
899
+ const image = this.docker.getImage(imageInfo.Id);
900
+ try {
901
+ await image.remove();
902
+ this.logger.info(`Removed image: ${imageInfo.RepoTags?.[0] || imageInfo.Id}`);
903
+ } catch (error) {
904
+ this.logger.warn(`Failed to remove image: ${error.message}`);
905
+ }
906
+ }
907
+ }
908
+
909
+ if (options.buildContexts || options.all) {
910
+ // Clean up build context directories and tarballs
911
+ await this.cleanupBuildContexts();
912
+ }
913
+
914
+ this.logger.info('Cleanup completed');
915
+
916
+ } catch (error) {
917
+ throw new Error(`Cleanup failed: ${error.message}`);
918
+ }
919
+ }
920
+
921
+ /**
922
+ * Clean up build context directories and tarballs
923
+ */
924
+ async cleanupBuildContexts() {
925
+ const projectRoot = path.join(__dirname, '../..');
926
+
927
+ try {
928
+ // Find all build context directories
929
+ const entries = await fs.readdir(projectRoot);
930
+ const buildContextDirs = entries.filter(entry => entry.startsWith('.build-context-'));
931
+
932
+ // Remove build context directories
933
+ for (const dir of buildContextDirs) {
934
+ const dirPath = path.join(projectRoot, dir);
935
+ try {
936
+ await fs.remove(dirPath);
937
+ this.logger.info(`Removed build context directory: ${dir}`);
938
+ } catch (error) {
939
+ this.logger.warn(`Failed to remove build context directory ${dir}: ${error.message}`);
940
+ }
941
+ }
942
+
943
+ // Find and remove tarball files
944
+ const tarballs = entries.filter(entry =>
945
+ entry.endsWith('-context.tar') || entry.endsWith('-build-context.tar')
946
+ );
947
+
948
+ for (const tarball of tarballs) {
949
+ const tarballPath = path.join(projectRoot, tarball);
950
+ try {
951
+ await fs.remove(tarballPath);
952
+ this.logger.info(`Removed build context tarball: ${tarball}`);
953
+ } catch (error) {
954
+ this.logger.warn(`Failed to remove tarball ${tarball}: ${error.message}`);
955
+ }
956
+ }
957
+
958
+ if (buildContextDirs.length === 0 && tarballs.length === 0) {
959
+ this.logger.info('No build context files found to clean up');
960
+ }
961
+
962
+ } catch (error) {
963
+ this.logger.warn(`Error during build context cleanup: ${error.message}`);
964
+ }
965
+ }
966
+ }
967
+
968
+ module.exports = { DockerManager };