dank-ai 1.0.25 → 1.0.27

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 CHANGED
@@ -169,6 +169,8 @@ dank build:prod --push # Build and push to registry (CLI only)
169
169
  dank build:prod --tag v1.0.0 # Build with custom tag
170
170
  dank build:prod --registry ghcr.io # Build for specific registry
171
171
  dank build:prod --force # Force rebuild without cache
172
+ dank build:prod --output-metadata deployment.json # Generate deployment metadata
173
+ dank build:prod --json # Output JSON summary to stdout
172
174
  ```
173
175
 
174
176
  > **šŸ’” Push Control**: The `--push` option is the only way to push images to registries. Agent configuration defines naming, CLI controls pushing.
@@ -190,6 +192,8 @@ dank build:prod --registry ghcr.io # Build for GitHub Container Registry
190
192
  dank build:prod --namespace mycompany # Build with custom namespace
191
193
  dank build:prod --tag-by-agent # Use agent name as tag (common repo)
192
194
  dank build:prod --force # Force rebuild without cache
195
+ dank build:prod --output-metadata <file> # Output deployment metadata JSON
196
+ dank build:prod --json # Output machine-readable JSON summary
193
197
  ```
194
198
 
195
199
  ## šŸ¤– Agent Configuration
@@ -719,6 +723,93 @@ dank build:prod --force --push
719
723
  dank build:prod --tag release-2024.1 --push
720
724
  ```
721
725
 
726
+ **Deployment Metadata Output:**
727
+ ```bash
728
+ # Generate deployment metadata JSON file
729
+ dank build:prod --output-metadata deployment.json
730
+
731
+ # Build, push, and generate metadata
732
+ dank build:prod --push --output-metadata deployment.json
733
+
734
+ # Use with custom configuration
735
+ dank build:prod --config production.config.js --output-metadata deployment.json
736
+ ```
737
+
738
+ The `--output-metadata` option generates a JSON file containing all deployment information needed for your backend infrastructure:
739
+ - **Base image** used (`setBaseImage()` value)
740
+ - **Prompting server** configuration (protocol, port, authentication, maxConnections)
741
+ - **Resource limits** (memory, CPU, timeout)
742
+ - **Ports** that need to be opened
743
+ - **Features enabled** (direct prompting, HTTP API, event handlers)
744
+ - **HTTP server** configuration (if enabled)
745
+ - **LLM provider** and model information
746
+ - **Event handlers** registered
747
+ - **Environment variables** required
748
+ - **Build options** (registry, namespace, tag, image name)
749
+
750
+ This metadata file is perfect for CI/CD pipelines to automatically configure your deployment infrastructure, determine which ports to open, and which features to enable/disable.
751
+
752
+ **Example Metadata Output:**
753
+ ```json
754
+ {
755
+ "project": "my-agent-project",
756
+ "buildTimestamp": "2024-01-15T10:30:00.000Z",
757
+ "agents": [
758
+ {
759
+ "name": "customer-service",
760
+ "imageName": "ghcr.io/mycompany/customer-service:v1.2.0",
761
+ "baseImage": {
762
+ "full": "deltadarkly/dank-agent-base:nodejs-20",
763
+ "tag": "nodejs-20"
764
+ },
765
+ "promptingServer": {
766
+ "protocol": "http",
767
+ "port": 3000,
768
+ "authentication": false,
769
+ "maxConnections": 50,
770
+ "timeout": 30000
771
+ },
772
+ "resources": {
773
+ "memory": "512m",
774
+ "cpu": 1,
775
+ "timeout": 30000
776
+ },
777
+ "ports": [
778
+ {
779
+ "port": 3000,
780
+ "protocol": "http",
781
+ "description": "Direct prompting server"
782
+ }
783
+ ],
784
+ "features": {
785
+ "directPrompting": true,
786
+ "httpApi": false,
787
+ "eventHandlers": true
788
+ },
789
+ "llm": {
790
+ "provider": "openai",
791
+ "model": "gpt-3.5-turbo",
792
+ "temperature": 0.7,
793
+ "maxTokens": 1000
794
+ },
795
+ "handlers": ["request_output", "request_output:start"],
796
+ "buildOptions": {
797
+ "registry": "ghcr.io",
798
+ "namespace": "mycompany",
799
+ "tag": "v1.2.0",
800
+ "tagByAgent": false
801
+ }
802
+ }
803
+ ],
804
+ "summary": {
805
+ "total": 1,
806
+ "successful": 1,
807
+ "failed": 0,
808
+ "pushed": 1
809
+ }
810
+ }
811
+ ```
812
+
722
813
  #### šŸ·ļø **Image Naming Convention**
723
814
 
724
815
  **Default (Per-Agent Repository):**
package/bin/dank CHANGED
@@ -90,6 +90,8 @@ program
90
90
  .option('--registry <registry>', 'Docker registry URL (e.g., docker.io, ghcr.io)')
91
91
  .option('--namespace <namespace>', 'Docker namespace/organization')
92
92
  .option('--tag-by-agent', 'Use agent name as the image tag (common image name)')
93
+ .option('--json', 'Output machine-readable JSON summary to stdout')
94
+ .option('--output-metadata <file>', 'Output deployment metadata JSON file')
93
95
  .option('--force', 'Force rebuild without cache')
94
96
  .action(async (options) => {
95
97
  const { productionBuildCommand } = require('../lib/cli/production-build');
@@ -8,6 +8,116 @@ const chalk = require('chalk');
8
8
  const { DockerManager } = require('../docker/manager');
9
9
  const analytics = require('../analytics');
10
10
 
11
+ /**
12
+ * Extract deployment metadata from an agent configuration
13
+ */
14
+ function extractAgentMetadata(agent, buildOptions, imageName) {
15
+ const config = agent.config || {};
16
+ const dockerConfig = config.docker || {};
17
+ const resources = config.resources || {};
18
+ const communication = config.communication || {};
19
+ const directPrompting = communication.directPrompting || {};
20
+ const httpConfig = config.http || {};
21
+ const llmConfig = config.llm || {};
22
+
23
+ // Extract base image tag (remove prefix)
24
+ // setBaseImage() sets docker.baseImage to "deltadarkly/dank-agent-base:tag"
25
+ const baseImage = dockerConfig.baseImage || '';
26
+ let baseImageTag = '';
27
+ if (baseImage.includes(':')) {
28
+ baseImageTag = baseImage.split(':').slice(1).join(':'); // Handle tags with colons
29
+ } else if (baseImage) {
30
+ baseImageTag = baseImage;
31
+ }
32
+
33
+ // Extract prompting server configuration
34
+ const promptingServer = directPrompting.enabled ? {
35
+ protocol: directPrompting.protocol || 'http',
36
+ port: dockerConfig.port || 3000,
37
+ authentication: directPrompting.authentication || false,
38
+ maxConnections: directPrompting.maxConnections || 50,
39
+ timeout: directPrompting.timeout || 30000
40
+ } : null;
41
+
42
+ // Extract resources configuration
43
+ const resourcesConfig = {
44
+ memory: resources.memory || '512m',
45
+ cpu: resources.cpu || 1,
46
+ timeout: resources.timeout || 30000
47
+ };
48
+
49
+ // Extract HTTP server configuration if enabled
50
+ const httpServer = httpConfig.enabled ? {
51
+ port: httpConfig.port || 3000,
52
+ host: httpConfig.host || '0.0.0.0',
53
+ cors: httpConfig.cors !== false,
54
+ routes: httpConfig.routes ? Array.from(httpConfig.routes.keys()).map(routeKey => {
55
+ const [method, path] = routeKey.split(':');
56
+ return { method, path };
57
+ }) : []
58
+ } : null;
59
+
60
+ // Extract handler information
61
+ const handlers = agent.handlers ? Array.from(agent.handlers.keys()) : [];
62
+
63
+ // Extract LLM configuration (without sensitive data)
64
+ const llm = llmConfig.provider ? {
65
+ provider: llmConfig.provider,
66
+ model: llmConfig.model || 'gpt-3.5-turbo',
67
+ temperature: llmConfig.temperature || 0.7,
68
+ maxTokens: llmConfig.maxTokens || 1000,
69
+ baseURL: llmConfig.baseURL || null
70
+ } : null;
71
+
72
+ // Collect ports that need to be opened
73
+ const ports = [];
74
+ if (promptingServer) {
75
+ ports.push({
76
+ port: promptingServer.port,
77
+ protocol: promptingServer.protocol,
78
+ description: 'Direct prompting server'
79
+ });
80
+ }
81
+ if (httpServer) {
82
+ ports.push({
83
+ port: httpServer.port,
84
+ protocol: 'http',
85
+ description: 'HTTP API server'
86
+ });
87
+ }
88
+
89
+ // Determine features enabled
90
+ const features = {
91
+ directPrompting: directPrompting.enabled || false,
92
+ httpApi: communication.httpApi?.enabled || httpConfig.enabled || false,
93
+ eventHandlers: communication.eventHandlers?.enabled || handlers.length > 0 || false
94
+ };
95
+
96
+ return {
97
+ name: agent.name,
98
+ imageName: imageName,
99
+ baseImage: {
100
+ full: baseImage,
101
+ tag: baseImageTag
102
+ },
103
+ buildOptions: {
104
+ registry: buildOptions.registry || null,
105
+ namespace: buildOptions.namespace || null,
106
+ tag: buildOptions.tag || 'latest',
107
+ tagByAgent: buildOptions.tagByAgent || false
108
+ },
109
+ promptingServer: promptingServer,
110
+ resources: resourcesConfig,
111
+ httpServer: httpServer,
112
+ ports: ports,
113
+ features: features,
114
+ llm: llm,
115
+ handlers: handlers,
116
+ hasPrompt: !!config.prompt,
117
+ environment: config.environment || {}
118
+ };
119
+ }
120
+
11
121
  async function productionBuildCommand(options) {
12
122
  // Track production build command
13
123
  await analytics.trackCommand('build:prod', true, {
@@ -35,6 +145,8 @@ async function productionBuildCommand(options) {
35
145
 
36
146
  // Build production images for each agent
37
147
  const buildResults = [];
148
+ const metadata = [];
149
+
38
150
  for (const agent of config.agents) {
39
151
  try {
40
152
  console.log(chalk.blue(`šŸ“¦ Building production image for agent: ${agent.name}`));
@@ -67,6 +179,10 @@ async function productionBuildCommand(options) {
67
179
  pushed: result.pushed || false
68
180
  });
69
181
 
182
+ // Extract deployment metadata for successfully built agents
183
+ const agentMetadata = extractAgentMetadata(agent, buildOptions, result.imageName);
184
+ metadata.push(agentMetadata);
185
+
70
186
  console.log(chalk.green(`āœ… Successfully built: ${result.imageName}`));
71
187
  if (result.pushed) {
72
188
  console.log(chalk.green(`šŸš€ Successfully pushed: ${result.imageName}`));
@@ -82,41 +198,71 @@ async function productionBuildCommand(options) {
82
198
  }
83
199
  }
84
200
 
85
- // Print summary
86
- console.log(chalk.yellow('\nšŸ“Š Build Summary:'));
87
- console.log(chalk.gray('================'));
88
-
89
- const successful = buildResults.filter(r => r.success);
90
- const failed = buildResults.filter(r => !r.success);
91
- const pushed = buildResults.filter(r => r.pushed);
92
-
93
- console.log(chalk.green(`āœ… Successful builds: ${successful.length}`));
94
- if (pushed.length > 0) {
95
- console.log(chalk.blue(`šŸš€ Pushed to registry: ${pushed.length}`));
96
- }
97
- if (failed.length > 0) {
98
- console.log(chalk.red(`āŒ Failed builds: ${failed.length}`));
201
+ // Output metadata file if requested
202
+ if (options.outputMetadata) {
203
+ const metadataPath = path.resolve(options.outputMetadata);
204
+ const metadataOutput = {
205
+ project: config.name,
206
+ buildTimestamp: new Date().toISOString(),
207
+ agents: metadata,
208
+ summary: {
209
+ total: metadata.length,
210
+ successful: buildResults.filter(r => r.success).length,
211
+ failed: buildResults.filter(r => !r.success).length,
212
+ pushed: buildResults.filter(r => r.pushed).length
213
+ }
214
+ };
215
+
216
+ await fs.writeJson(metadataPath, metadataOutput, { spaces: 2 });
217
+ console.log(chalk.cyan(`\nšŸ“„ Deployment metadata saved to: ${metadataPath}`));
99
218
  }
100
219
 
101
- // List built images
102
- if (successful.length > 0) {
103
- console.log(chalk.cyan('\nšŸ“¦ Built Images:'));
104
- successful.forEach(result => {
105
- console.log(chalk.gray(` - ${result.imageName}`));
106
- });
107
- }
220
+ // Output
221
+ if (options.json) {
222
+ const payload = {
223
+ success: buildResults.every(r => r.success),
224
+ results: buildResults
225
+ };
226
+ // Always print JSON to stdout for machine consumption
227
+ console.log(JSON.stringify(payload));
228
+ process.exit(payload.success ? 0 : 1);
229
+ } else {
230
+ // Human summary
231
+ console.log(chalk.yellow('\nšŸ“Š Build Summary:'));
232
+ console.log(chalk.gray('================'));
233
+
234
+ const successful = buildResults.filter(r => r.success);
235
+ const failed = buildResults.filter(r => !r.success);
236
+ const pushed = buildResults.filter(r => r.pushed);
108
237
 
109
- // List failed builds
110
- if (failed.length > 0) {
111
- console.log(chalk.red('\nāŒ Failed Builds:'));
112
- failed.forEach(result => {
113
- console.log(chalk.gray(` - ${result.agent}: ${result.error}`));
114
- });
115
- process.exit(1);
116
- }
238
+ console.log(chalk.green(`āœ… Successful builds: ${successful.length}`));
239
+ if (pushed.length > 0) {
240
+ console.log(chalk.blue(`šŸš€ Pushed to registry: ${pushed.length}`));
241
+ }
242
+ if (failed.length > 0) {
243
+ console.log(chalk.red(`āŒ Failed builds: ${failed.length}`));
244
+ }
245
+
246
+ // List built images
247
+ if (successful.length > 0) {
248
+ console.log(chalk.cyan('\nšŸ“¦ Built Images:'));
249
+ successful.forEach(result => {
250
+ console.log(chalk.gray(` - ${result.imageName}`));
251
+ });
252
+ }
117
253
 
118
- console.log(chalk.green('\nšŸŽ‰ Production build completed successfully!'));
119
- process.exit(0);
254
+ // List failed builds
255
+ if (failed.length > 0) {
256
+ console.log(chalk.red('\nāŒ Failed Builds:'));
257
+ failed.forEach(result => {
258
+ console.log(chalk.gray(` - ${result.agent}: ${result.error}`));
259
+ });
260
+ process.exit(1);
261
+ }
262
+
263
+ console.log(chalk.green('\nšŸŽ‰ Production build completed successfully!'));
264
+ process.exit(0);
265
+ }
120
266
 
121
267
  } catch (error) {
122
268
  console.error(chalk.red('āŒ Production build failed:'), error.message);
@@ -23,6 +23,26 @@ const analytics = require("../analytics");
23
23
  const execAsync = promisify(exec);
24
24
 
25
25
  class DockerManager {
26
+ /**
27
+ * Resolve docker executable path
28
+ */
29
+ async resolveDockerCommand() {
30
+ const dockerPaths = [
31
+ "/usr/local/bin/docker",
32
+ "/opt/homebrew/bin/docker",
33
+ "/usr/bin/docker",
34
+ "docker",
35
+ ];
36
+ for (const path of dockerPaths) {
37
+ try {
38
+ await execAsync(`${path} --version`);
39
+ return path;
40
+ } catch (_) {
41
+ // continue
42
+ }
43
+ }
44
+ throw new Error("Docker executable not found in expected locations");
45
+ }
26
46
  constructor(options = {}) {
27
47
  this.docker = new Docker(options.dockerOptions || {});
28
48
  this.logger =
@@ -1172,25 +1192,21 @@ class DockerManager {
1172
1192
  return sanitized;
1173
1193
  };
1174
1194
 
1175
- // Base repository name logic
1195
+ //construct full repo name
1176
1196
  let repoName;
1177
- if (tagByAgent) {
1178
- // Use a common repository only when no namespace is provided
1179
- // Otherwise, fall back to per-agent repository name
1180
- repoName = namespace ? agent.name.toLowerCase() : 'dank-agent';
1181
- } else {
1182
- // Default: per-agent repository name
1183
- repoName = agent.name.toLowerCase();
1197
+ if(!tagByAgent){
1198
+
1199
+ repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}${repoName}`;
1200
+ }else{
1201
+ repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}`;
1184
1202
  }
1203
+
1204
+ repoName = repoName.replace(/\/+$/, '');
1185
1205
 
1186
- // Compose full repository path
1187
- let fullRepo = repoName;
1188
- if (namespace) fullRepo = `${namespace}/${fullRepo}`;
1189
- if (registry) fullRepo = `${registry}/${fullRepo}`;
1190
1206
 
1191
1207
  // Final tag selection
1192
1208
  const finalTag = tagByAgent ? normalizeTag(agent.name) : tag;
1193
- const imageName = `${fullRepo}:${finalTag}`;
1209
+ const imageName = `${repoName}:${finalTag}`;
1194
1210
 
1195
1211
  this.logger.info(
1196
1212
  `Building production image for agent: ${agent.name} -> ${imageName}`
@@ -1217,19 +1233,29 @@ class DockerManager {
1217
1233
 
1218
1234
  let pushed = false;
1219
1235
 
1220
- // Push to registry if requested
1236
+ // Push to registry if requested (use docker CLI for reliability)
1221
1237
  if (push) {
1222
- try {
1223
- this.logger.info(`Pushing image to registry: ${imageName}`);
1224
- const pushStream = await this.docker.getImage(imageName).push();
1225
- await this.followBuildProgress(pushStream, `Push ${imageName}`);
1226
- this.logger.info(`Successfully pushed image: ${imageName}`);
1227
- pushed = true;
1228
- } catch (pushError) {
1229
- this.logger.warn(
1230
- `Failed to push image ${imageName}: ${pushError.message}`
1231
- );
1232
- // Don't fail the build if push fails
1238
+ const dockerCmd = await this.resolveDockerCommand();
1239
+ const maxAttempts = 3;
1240
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1241
+ try {
1242
+ this.logger.info(`Pushing image to registry (attempt ${attempt}/${maxAttempts}): ${imageName}`);
1243
+ await this.runCommand(`${dockerCmd} push ${imageName}`, `Docker push ${imageName}`);
1244
+ this.logger.info(`Successfully pushed image: ${imageName}`);
1245
+ pushed = true;
1246
+ break;
1247
+ } catch (pushError) {
1248
+ const msg = pushError?.message || '';
1249
+ this.logger.warn(`Push attempt ${attempt} failed: ${msg}`);
1250
+ if (msg.match(/denied|unauthorized|authentication required/i)) {
1251
+ this.logger.warn("Authentication issue detected. Please ensure you're logged in: docker login <registry>");
1252
+ break; // don't retry auth failures automatically
1253
+ }
1254
+ if (attempt < maxAttempts) {
1255
+ await this.sleep(2000 * attempt);
1256
+ continue;
1257
+ }
1258
+ }
1233
1259
  }
1234
1260
  }
1235
1261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dank-ai",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "Dank Agent Service - Docker-based AI agent orchestration platform",
5
5
  "main": "lib/index.js",
6
6
  "exports": {