dank-ai 1.0.31 → 1.0.33

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.
@@ -1140,13 +1140,35 @@ class DockerManager {
1140
1140
  }
1141
1141
  }
1142
1142
 
1143
+ /**
1144
+ * Normalize Docker image name/tag components
1145
+ * Docker allows: lowercase letters, digits, underscores, periods, and hyphens
1146
+ * Cannot start or end with period or hyphen, max 128 chars for tags
1147
+ */
1148
+ normalizeDockerName(name) {
1149
+ if (!name || typeof name !== 'string') return 'invalid';
1150
+ const lower = String(name).toLowerCase();
1151
+ let sanitized = lower.replace(/[^a-z0-9_.-]/g, '-');
1152
+ sanitized = sanitized.replace(/\.{2,}/g, '.'); // Replace multiple periods with single
1153
+ sanitized = sanitized.replace(/-{2,}/g, '-'); // Replace multiple hyphens with single
1154
+ sanitized = sanitized.replace(/^[.-]+/, '').replace(/[.-]+$/, ''); // Remove leading/trailing . or -
1155
+ if (!sanitized || !/^[a-z0-9]/.test(sanitized)) sanitized = `a${sanitized || ''}`;
1156
+ if (sanitized.length > 128) sanitized = sanitized.slice(0, 128);
1157
+ return sanitized;
1158
+ }
1159
+
1143
1160
  /**
1144
1161
  * Build agent-specific image
1145
1162
  */
1146
1163
  async buildAgentImage(agent, options = {}) {
1147
- const imageName = `dank-agent-${agent.name.toLowerCase()}`;
1164
+ const normalizedName = this.normalizeDockerName(agent.name);
1165
+ const imageName = `dank-agent-${normalizedName}`;
1148
1166
  this.logger.info(`Building image for agent: ${agent.name}`);
1149
1167
 
1168
+ // Finalize agent configuration before building
1169
+ // This ensures ports and other configs are properly set
1170
+ agent.finalize();
1171
+
1150
1172
  try {
1151
1173
  const buildContext = await this.createAgentBuildContext(agent);
1152
1174
  const dockerCmd = await this.resolveDockerCommand();
@@ -1178,6 +1200,17 @@ class DockerManager {
1178
1200
 
1179
1201
  /**
1180
1202
  * Build production image with custom naming and tagging
1203
+ *
1204
+ * @param {Object} agent - The agent to build
1205
+ * @param {Object} options - Build options
1206
+ * @param {string} [options.tag="latest"] - Image tag
1207
+ * @param {string} [options.registry] - Docker registry
1208
+ * @param {string} [options.namespace] - Docker namespace
1209
+ * @param {boolean} [options.tagByAgent=false] - Use agent name as tag
1210
+ * @param {boolean} [options.force=false] - Force rebuild without cache
1211
+ * @param {boolean} [options.push=false] - Push to registry after build
1212
+ * @param {string} [options.baseImageOverride=null] - Production-only: Override base image for all agents (replaces agent's configured base image)
1213
+ * @returns {Promise<Object>} Build result with imageName and pushed status
1181
1214
  */
1182
1215
  async buildProductionImage(agent, options = {}) {
1183
1216
  const {
@@ -1187,42 +1220,47 @@ class DockerManager {
1187
1220
  tagByAgent = false,
1188
1221
  force = false,
1189
1222
  push = false,
1223
+ baseImageOverride = null, // Production-only: override base image for all agents
1190
1224
  } = options;
1191
1225
 
1192
- // Normalize a Docker tag from agent name when tagByAgent is enabled
1193
- const normalizeTag = (name) => {
1194
- const lower = String(name || '').toLowerCase();
1195
- let sanitized = lower.replace(/[^a-z0-9_.-]/g, '-');
1196
- sanitized = sanitized.replace(/^-+/, '').replace(/-+$/, '');
1197
- if (!sanitized || !/^[a-z0-9]/.test(sanitized)) sanitized = `a${sanitized || ''}`;
1198
- if (sanitized.length > 128) sanitized = sanitized.slice(0, 128);
1199
- return sanitized;
1200
- };
1226
+ // Normalize all components
1227
+ const normalizedAgentName = this.normalizeDockerName(agent.name);
1228
+ const normalizedTag = this.normalizeDockerName(tag);
1201
1229
 
1202
1230
  //construct full repo name
1203
1231
  let repoName;
1204
1232
  if(!tagByAgent){
1205
1233
  // Per-agent repository: {registry}/{namespace}/{agent-name}
1206
- const agentRepoName = agent.name.toLowerCase().replace(/[^a-z0-9_.-]/g, '-');
1207
- repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}${agentRepoName}`;
1234
+ repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}${normalizedAgentName}`;
1208
1235
  }else{
1209
- // Common repository: {registry}/{namespace}:agent-name
1236
+ // Common repository: {registry}/{namespace}
1210
1237
  repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}`;
1211
1238
  }
1212
1239
 
1213
1240
  repoName = repoName.replace(/\/+$/, '');
1214
1241
 
1215
-
1216
- // Final tag selection
1217
- const finalTag = tagByAgent ? normalizeTag(agent.name) : tag;
1242
+ // Final tag selection - normalize both agent name tags and user-provided tags
1243
+ const finalTag = tagByAgent ? normalizedAgentName : normalizedTag;
1218
1244
  const imageName = `${repoName}:${finalTag}`;
1219
1245
 
1220
1246
  this.logger.info(
1221
1247
  `Building production image for agent: ${agent.name} -> ${imageName}`
1222
1248
  );
1223
1249
 
1250
+ // Finalize agent configuration before building
1251
+ // This ensures ports and other configs are properly set
1252
+ agent.finalize();
1253
+
1254
+ // Log base image override if provided
1255
+ if (baseImageOverride) {
1256
+ this.logger.info(`🔧 Production build: Using custom base image override: ${baseImageOverride}`);
1257
+ }
1258
+
1224
1259
  try {
1225
- const buildContext = await this.createAgentBuildContext(agent);
1260
+ const buildContext = await this.createAgentBuildContext(agent, {
1261
+ isProductionBuild: true,
1262
+ baseImageOverride: baseImageOverride
1263
+ });
1226
1264
  const dockerCmd = await this.resolveDockerCommand();
1227
1265
 
1228
1266
  const buildCommand = [
@@ -1299,6 +1337,53 @@ class DockerManager {
1299
1337
  return `{\n${handlersEntries}\n }`;
1300
1338
  }
1301
1339
 
1340
+ /**
1341
+ * Generate routes code from agent configuration
1342
+ */
1343
+ generateRoutesCode(agent) {
1344
+ const routes = {};
1345
+
1346
+ // Add routes from agent HTTP configuration
1347
+ if (agent.config?.http?.routes && agent.config.http.routes.size > 0) {
1348
+ for (const [routeKey, routeList] of agent.config.http.routes) {
1349
+ const [method, path] = routeKey.split(':');
1350
+ if (!routes[path]) {
1351
+ routes[path] = {};
1352
+ }
1353
+
1354
+ // Convert route handlers to string representations
1355
+ routeList.forEach((routeObj) => {
1356
+ if (routeObj && typeof routeObj.handler === "function") {
1357
+ const handlerStr = routeObj.handler.toString();
1358
+ routes[path][method.toLowerCase()] = handlerStr;
1359
+ }
1360
+ });
1361
+ }
1362
+ }
1363
+
1364
+ // Generate the JavaScript object code
1365
+ if (Object.keys(routes).length === 0) {
1366
+ return '{}';
1367
+ }
1368
+
1369
+ const routesEntries = Object.entries(routes)
1370
+ .map(([path, methods]) => {
1371
+ const methodsEntries = Object.entries(methods)
1372
+ .map(([method, handler]) => {
1373
+ return ` ${method}: ${handler}`;
1374
+ })
1375
+ .join(",\n");
1376
+ // Quote paths that contain special characters
1377
+ const quotedPath = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(path)
1378
+ ? path
1379
+ : JSON.stringify(path);
1380
+ return ` ${quotedPath}: {\n${methodsEntries}\n }`;
1381
+ })
1382
+ .join(",\n");
1383
+
1384
+ return `{\n${routesEntries}\n }`;
1385
+ }
1386
+
1302
1387
  /**
1303
1388
  * Start agent container
1304
1389
  */
@@ -1341,12 +1426,12 @@ class DockerManager {
1341
1426
  name: containerName,
1342
1427
  Env: this.prepareEnvironmentVariables(agent),
1343
1428
  HostConfig: {
1344
- Memory: AgentConfig.parseMemory(agent.config.resources.memory),
1345
- CpuQuota: Math.floor(agent.config.resources.cpu * 100000),
1429
+ Memory: AgentConfig.parseMemory(AgentConfig.getResourcesFromInstanceType(agent.config.instanceType).memory),
1430
+ CpuQuota: Math.floor(AgentConfig.getResourcesFromInstanceType(agent.config.instanceType).cpu * 100000),
1346
1431
  CpuPeriod: 100000,
1347
1432
  RestartPolicy: {
1348
1433
  Name: "on-failure",
1349
- MaximumRetryCount: agent.config.resources.maxRestarts || 3,
1434
+ MaximumRetryCount: 3, // Default max restarts
1350
1435
  },
1351
1436
  NetworkMode: this.networkName,
1352
1437
  ...this.preparePortConfiguration(agent),
@@ -1475,10 +1560,7 @@ class DockerManager {
1475
1560
  uptime: Date.now() - startTime.getTime(),
1476
1561
  health: containerData.State.Health?.Status || "unknown",
1477
1562
  restartCount: containerData.RestartCount,
1478
- resources: {
1479
- memory: agent.config.resources.memory,
1480
- cpu: agent.config.resources.cpu,
1481
- },
1563
+ resources: AgentConfig.getResourcesFromInstanceType(agent.config.instanceType),
1482
1564
  };
1483
1565
  } catch (error) {
1484
1566
  return { status: "error", error: error.message };
@@ -1538,7 +1620,7 @@ class DockerManager {
1538
1620
  /**
1539
1621
  * Create build context for agent
1540
1622
  */
1541
- async createAgentBuildContext(agent) {
1623
+ async createAgentBuildContext(agent, options = {}) {
1542
1624
  const contextDir = path.join(
1543
1625
  __dirname,
1544
1626
  `../../.build-context-${agent.name}`
@@ -1546,12 +1628,44 @@ class DockerManager {
1546
1628
  await fs.ensureDir(contextDir);
1547
1629
 
1548
1630
  // Get the base image for this agent
1549
- const baseImageName =
1550
- agent.config.docker?.baseImage || this.defaultBaseImageName;
1631
+ // Production builds can override the base image via baseImageOverride option
1632
+ let baseImageName;
1633
+ if (options.isProductionBuild && options.baseImageOverride) {
1634
+ baseImageName = options.baseImageOverride;
1635
+ this.logger.info(`🔧 Using production base image override: ${baseImageName} (instead of ${agent.config.docker?.baseImage || this.defaultBaseImageName})`);
1636
+ } else {
1637
+ baseImageName = agent.config.docker?.baseImage || this.defaultBaseImageName;
1638
+ }
1639
+
1640
+ // Generate environment variables
1641
+ // For production builds, these need to be embedded in the image
1642
+ // For normal builds, they're injected at container creation time via startAgent()
1643
+ const env = AgentConfig.generateContainerEnv(agent);
1644
+
1645
+ // Embed env vars in Dockerfile for production builds
1646
+ // For normal builds, env vars are injected at container creation time
1647
+ const isProductionBuild = options.isProductionBuild || false;
1648
+
1649
+ let envStatements = '';
1650
+ if (isProductionBuild) {
1651
+ // Embed environment variables in Dockerfile for production builds
1652
+ // This ensures they're available when the image is deployed elsewhere
1653
+ envStatements = Object.entries(env)
1654
+ .map(([key, value]) => {
1655
+ // Escape special characters in values for Dockerfile ENV
1656
+ const escapedValue = String(value).replace(/\$/g, '$$$$').replace(/"/g, '\\"');
1657
+ return `ENV ${key}="${escapedValue}"`;
1658
+ })
1659
+ .join('\n');
1660
+
1661
+ this.logger.info(`🔌 Production build: Embedding environment variables in Dockerfile`);
1662
+ this.logger.info(` - DOCKER_PORT: ${env.DOCKER_PORT || 'not set'}`);
1663
+ }
1551
1664
 
1552
1665
  // Create Dockerfile for agent
1553
1666
  const dockerfile = `FROM ${baseImageName}
1554
1667
  COPY agent-code/ /app/agent-code/
1668
+ ${envStatements}
1555
1669
  USER dankuser
1556
1670
  `;
1557
1671
 
@@ -1564,6 +1678,7 @@ USER dankuser
1564
1678
  // Create basic agent code structure
1565
1679
  // Generate handlers from agent configuration
1566
1680
  const handlersCode = this.generateHandlersCode(agent);
1681
+ const routesCode = this.generateRoutesCode(agent);
1567
1682
 
1568
1683
  const agentCode = `
1569
1684
  // Agent: ${agent.name}
@@ -1605,7 +1720,8 @@ module.exports = {
1605
1720
  }, 10000);
1606
1721
  },
1607
1722
 
1608
- handlers: ${handlersCode}
1723
+ handlers: ${handlersCode},
1724
+ routes: ${routesCode}
1609
1725
  };
1610
1726
  `;
1611
1727
 
@@ -1632,19 +1748,21 @@ module.exports = {
1632
1748
 
1633
1749
  // Always bind the main agent port
1634
1750
  const mainPort = agent.config.docker?.port || DOCKER_CONFIG.defaultPort;
1751
+ this.logger.info(`🔌 Binding main agent port: ${mainPort} (from docker.port: ${agent.config.docker?.port || 'default'})`);
1635
1752
  portBindings[`${mainPort}/tcp`] = [{ HostPort: mainPort.toString() }];
1636
1753
 
1637
- // Also bind HTTP port if HTTP is enabled and different from main port
1638
- if (agent.config.http && agent.config.http.enabled) {
1639
- const httpPort = agent.config.http.port;
1640
- if (httpPort !== mainPort) {
1754
+ // Only bind HTTP port if HTTP is explicitly enabled AND different from main port
1755
+ // Check both http.enabled and communication.httpApi.enabled to be safe
1756
+ const httpEnabled = agent.config.http?.enabled === true;
1757
+ const httpApiEnabled = agent.config.communication?.httpApi?.enabled === true;
1758
+ if (httpEnabled || httpApiEnabled) {
1759
+ const httpPort = agent.config.http?.port;
1760
+ if (httpPort && httpPort !== mainPort) {
1641
1761
  portBindings[`${httpPort}/tcp`] = [{ HostPort: httpPort.toString() }];
1642
1762
  }
1643
1763
  }
1644
1764
 
1645
- // Always bind health check port
1646
- const healthPort = DOCKER_CONFIG.healthCheckPort;
1647
- portBindings[`${healthPort}/tcp`] = [{ HostPort: healthPort.toString() }];
1765
+ // Health check uses the same port as the main agent port, so no separate binding needed
1648
1766
 
1649
1767
  portConfig.PortBindings = portBindings;
1650
1768
  return portConfig;
@@ -1660,17 +1778,18 @@ module.exports = {
1660
1778
  const mainPort = agent.config.docker?.port || DOCKER_CONFIG.defaultPort;
1661
1779
  exposedPorts[`${mainPort}/tcp`] = {};
1662
1780
 
1663
- // Also expose HTTP port if HTTP is enabled and different from main port
1664
- if (agent.config.http && agent.config.http.enabled) {
1665
- const httpPort = agent.config.http.port;
1666
- if (httpPort !== mainPort) {
1781
+ // Only expose HTTP port if HTTP is explicitly enabled AND different from main port
1782
+ // Check both http.enabled and communication.httpApi.enabled to be safe
1783
+ const httpEnabled = agent.config.http?.enabled === true;
1784
+ const httpApiEnabled = agent.config.communication?.httpApi?.enabled === true;
1785
+ if (httpEnabled || httpApiEnabled) {
1786
+ const httpPort = agent.config.http?.port;
1787
+ if (httpPort && httpPort !== mainPort) {
1667
1788
  exposedPorts[`${httpPort}/tcp`] = {};
1668
1789
  }
1669
1790
  }
1670
1791
 
1671
- // Always expose health check port
1672
- const healthPort = DOCKER_CONFIG.healthCheckPort;
1673
- exposedPorts[`${healthPort}/tcp`] = {};
1792
+ // Health check uses the same port as the main agent port, so no separate exposure needed
1674
1793
 
1675
1794
  return { ExposedPorts: exposedPorts };
1676
1795
  }
package/lib/project.js CHANGED
@@ -85,10 +85,7 @@ module.exports = {
85
85
  protocol: 'http',
86
86
  port: 3000
87
87
  })
88
- .setResources({
89
- memory: '512m',
90
- cpu: 1
91
- })
88
+ .setInstanceType('small')
92
89
  // Event handlers for prompt modification and response enhancement
93
90
  .addHandler('request_output:start', (data) => {
94
91
  console.log('[Prompt Agent] Processing prompt:', data.conversationId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dank-ai",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "description": "Dank Agent Service - Docker-based AI agent orchestration platform",
5
5
  "main": "lib/index.js",
6
6
  "exports": {