dank-ai 1.0.24 ā 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 +91 -0
- package/bin/dank +2 -0
- package/lib/cli/production-build.js +177 -31
- package/lib/docker/manager.js +51 -24
- package/package.json +1 -1
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
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
//
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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);
|
package/lib/docker/manager.js
CHANGED
|
@@ -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,24 +1192,21 @@ class DockerManager {
|
|
|
1172
1192
|
return sanitized;
|
|
1173
1193
|
};
|
|
1174
1194
|
|
|
1175
|
-
//
|
|
1195
|
+
//construct full repo name
|
|
1176
1196
|
let repoName;
|
|
1177
|
-
if
|
|
1178
|
-
|
|
1179
|
-
repoName = '
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
repoName = agent.name.toLowerCase();
|
|
1197
|
+
if(!tagByAgent){
|
|
1198
|
+
|
|
1199
|
+
repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}${repoName}`;
|
|
1200
|
+
}else{
|
|
1201
|
+
repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}`;
|
|
1183
1202
|
}
|
|
1203
|
+
|
|
1204
|
+
repoName = repoName.replace(/\/+$/, '');
|
|
1184
1205
|
|
|
1185
|
-
// Compose full repository path
|
|
1186
|
-
let fullRepo = repoName;
|
|
1187
|
-
if (namespace) fullRepo = `${namespace}/${fullRepo}`;
|
|
1188
|
-
if (registry) fullRepo = `${registry}/${fullRepo}`;
|
|
1189
1206
|
|
|
1190
1207
|
// Final tag selection
|
|
1191
1208
|
const finalTag = tagByAgent ? normalizeTag(agent.name) : tag;
|
|
1192
|
-
const imageName = `${
|
|
1209
|
+
const imageName = `${repoName}:${finalTag}`;
|
|
1193
1210
|
|
|
1194
1211
|
this.logger.info(
|
|
1195
1212
|
`Building production image for agent: ${agent.name} -> ${imageName}`
|
|
@@ -1216,19 +1233,29 @@ class DockerManager {
|
|
|
1216
1233
|
|
|
1217
1234
|
let pushed = false;
|
|
1218
1235
|
|
|
1219
|
-
// Push to registry if requested
|
|
1236
|
+
// Push to registry if requested (use docker CLI for reliability)
|
|
1220
1237
|
if (push) {
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
)
|
|
1231
|
-
|
|
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
|
+
}
|
|
1232
1259
|
}
|
|
1233
1260
|
}
|
|
1234
1261
|
|