diff --git a/n8n-workflow.json b/n8n-workflow.json
index 1ed2a1f..b4f11b8 100644
--- a/n8n-workflow.json
+++ b/n8n-workflow.json
@@ -299,7 +299,7 @@
},
{
"parameters": {
- "jsCode": "// Get Docker API response and user input\nconst dockerOutput = $input.item.json.stdout;\nconst userMessage = $('Telegram Trigger').item.json.message.text.toLowerCase().trim();\nconst chatId = $('Telegram Trigger').item.json.message.chat.id;\n\n// Parse JSON response - only error if we can't parse valid JSON from stdout\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: \"Can't reach Docker - check if n8n has socket access\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Extract container name from user query\n// Remove common query words to get the container name\nconst queryWords = ['status', 'show', 'check', 'container', 'docker', 'what', 'is', 'the', 'of', 'for'];\nconst words = userMessage.split(/\\s+/).filter(word => !queryWords.includes(word));\nconst requestedName = words.join(' ').trim();\n\n// If no container name specified, return summary\nif (!requestedName || requestedName === '') {\n const counts = containers.reduce((acc, c) => {\n acc[c.State] = (acc[c.State] || 0) + 1;\n return acc;\n }, {});\n\n const parts = [];\n if (counts.running) parts.push(`${counts.running} running`);\n if (counts.exited) parts.push(`${counts.exited} stopped`);\n if (counts.paused) parts.push(`${counts.paused} paused`);\n if (counts.restarting) parts.push(`${counts.restarting} restarting`);\n\n const summary = parts.length > 0 ? parts.join(', ') : 'No containers found';\n\n return [{\n json: {\n chatId: chatId,\n summary: true,\n containers: containers,\n text: `Container summary: ${summary}`\n }\n }];\n}\n\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = requestedName.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\n});\n\n// Handle no matches\nif (matches.length === 0) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: `No container found matching \"${requestedName}\".\\n\\nTry \"status\" to see all containers.`\n }\n }];\n}\n\n// Handle multiple matches\nif (matches.length > 1) {\n const names = matches.map(c => c.Names[0].replace(/^\\//, '')).join('\\n- ');\n return [{\n json: {\n chatId: chatId,\n multipleMatches: true,\n matches: matches,\n text: `Found ${matches.length} matches:\\n\\n- ${names}\\n\\nPlease be more specific.`\n }\n }];\n}\n\n// Single match - return container details\nconst container = matches[0];\nreturn [{\n json: {\n chatId: chatId,\n singleMatch: true,\n container: {\n id: container.Id,\n name: container.Names[0].replace(/^\\//, ''),\n state: container.State,\n status: container.Status,\n image: container.Image\n }\n }\n}];"
+ "jsCode": "// Get Docker API response and intent data\nconst dockerOutput = $input.item.json.stdout;\nconst intent = $('Parse Intent').item.json;\nconst chatId = intent.original_message.chat.id;\n\n// Parse JSON response - only error if we can't parse valid JSON from stdout\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: \"Can't reach Docker - check if n8n has socket access\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Check if user requested a specific container or general list\nconst requestedName = intent.container;\n\n// If no container name specified, return summary\nif (!requestedName || requestedName === '' || intent.action === 'list_containers') {\n const counts = containers.reduce((acc, c) => {\n acc[c.State] = (acc[c.State] || 0) + 1;\n return acc;\n }, {});\n \n const total = containers.length;\n const running = counts['running'] || 0;\n const stopped = counts['exited'] || 0;\n const other = total - running - stopped;\n \n let summary = `Docker Containers\\n\\n`;\n summary += `Running: ${running}/${total}\\n`;\n if (stopped > 0) summary += `Stopped: ${stopped}\\n`;\n if (other > 0) summary += `Other: ${other}\\n`;\n \n // List running containers\n const runningContainers = containers\n .filter(c => c.State === 'running')\n .map(c => {\n const name = normalizeName(c.Names[0]);\n const uptime = c.Status;\n return ` \u2022 ${name} - ${uptime}`;\n });\n \n if (runningContainers.length > 0) {\n summary += `\\nRunning:\\n${runningContainers.join('\\n')}`;\n }\n \n return [{\n json: {\n chatId: chatId,\n text: summary\n }\n }];\n}\n\n// Find matching containers\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(requestedName);\n});\n\nif (matches.length === 0) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: `No container found matching \"${requestedName}\"`\n }\n }];\n}\n\nif (matches.length === 1) {\n const container = matches[0];\n const name = normalizeName(container.Names[0]);\n const state = container.State;\n const status = container.Status;\n const image = container.Image;\n \n let text = `${name}\\n`;\n text += `State: ${state}\\n`;\n text += `Status: ${status}\\n`;\n text += `Image: ${image}`;\n \n return [{\n json: {\n chatId: chatId,\n text: text\n }\n }];\n}\n\n// Multiple matches - list them\nconst matchList = matches.map(c => {\n const name = normalizeName(c.Names[0]);\n return ` \u2022 ${name} (${c.State})`;\n}).join('\\n');\n\nreturn [{\n json: {\n chatId: chatId,\n text: `Multiple matches for \"${requestedName}\":\\n${matchList}`\n }\n}];"
},
"id": "code-parse-match",
"name": "Parse and Match",
@@ -388,7 +388,7 @@
},
{
"parameters": {
- "jsCode": "// Parse action command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match action pattern: start/stop/restart followed by container name\nconst match = text.match(/^(start|stop|restart)\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid action format. Use: start/stop/restart ',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: match[1].toLowerCase(),\n containerQuery: match[2].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};"
+ "jsCode": "// Parse action from intent\n// Intent structure: { action: \"container_action\", container: \"name\", parameters: { action: \"restart\" } }\nconst text = $json.original_message.text.toLowerCase().trim();\nconst chatId = $json.original_message.chat.id;\nconst messageId = $json.original_message.message_id;\n\n// If intent provided an action, use it\nlet requestedAction = $json.parameters.action || null;\nlet containerQuery = $json.container;\n\n// If no action in parameters, try to infer from the original text\nif (!requestedAction) {\n if (text.includes('start') && !text.includes('restart')) {\n requestedAction = 'start';\n } else if (text.includes('stop')) {\n requestedAction = 'stop';\n } else if (text.includes('restart')) {\n requestedAction = 'restart';\n } else if (text.includes('update')) {\n requestedAction = 'update';\n }\n}\n\nif (!requestedAction || !containerQuery) {\n return {\n json: {\n error: true,\n errorMessage: 'Please specify an action (start/stop/restart/update) and container name.',\n chatId: chatId\n }\n };\n}\n\n// Map action names\nconst actionMap = {\n 'start': 'start',\n 'stop': 'stop',\n 'restart': 'restart',\n 'update': 'update'\n};\n\nconst action = actionMap[requestedAction.toLowerCase()];\n\nif (!action) {\n return {\n json: {\n error: true,\n errorMessage: 'Unknown action. Valid actions: start, stop, restart, update',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n messageId: messageId\n }\n};"
},
"id": "code-parse-action",
"name": "Parse Action",
@@ -1805,7 +1805,7 @@
},
{
"parameters": {
- "jsCode": "// Parse logs command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match logs pattern: (show )logs (optional line count)\n// Examples:\n// - \"logs plex\" -> { container: \"plex\", lines: 50 }\n// - \"show logs sonarr\" -> { container: \"sonarr\", lines: 50 }\n// - \"logs nginx 100\" -> { container: \"nginx\", lines: 100 }\n// - \"show logs radarr last 200\" -> { container: \"radarr\", lines: 200 }\n\nconst match = text.match(/^(?:show\\s+)?logs\\s+(.+?)(?:\\s+(?:last\\s+)?(\\d+))?$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid logs format. Use: logs [line-count]',\n chatId: chatId\n }\n };\n}\n\nconst containerQuery = match[1].trim();\nconst lines = match[2] ? parseInt(match[2], 10) : 50;\n\n// Validate line count (reasonable limits)\nconst validatedLines = Math.min(Math.max(lines, 1), 1000);\n\nreturn {\n json: {\n containerQuery: containerQuery,\n lines: validatedLines,\n chatId: chatId,\n messageId: messageId\n }\n};"
+ "jsCode": "// Parse logs command from intent\n// Intent structure: { action: \"view_logs\", container: \"name\", parameters: { lines: 50 } }\nconst chatId = $json.original_message.chat.id;\nconst messageId = $json.original_message.message_id;\nconst containerQuery = $json.container;\nconst lines = $json.parameters.lines || 50;\n\nif (!containerQuery) {\n return {\n json: {\n error: true,\n errorMessage: 'Please specify which container logs you want to see.',\n chatId: chatId\n }\n };\n}\n\n// Validate line count (reasonable limits)\nconst validatedLines = Math.min(Math.max(lines, 1), 1000);\n\nreturn {\n json: {\n containerQuery: containerQuery,\n lines: validatedLines,\n chatId: chatId,\n messageId: messageId\n }\n};"
},
"id": "code-parse-logs",
"name": "Parse Logs Command",
@@ -2067,6 +2067,316 @@
1780,
600
]
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "https://api.anthropic.com/v1/messages",
+ "authentication": "genericCredentialType",
+ "genericAuthType": "httpHeaderAuth",
+ "sendHeaders": true,
+ "headerParameters": {
+ "parameters": [
+ {
+ "name": "anthropic-version",
+ "value": "2023-06-01"
+ }
+ ]
+ },
+ "sendBody": true,
+ "contentType": "json",
+ "jsonBody": "={{ JSON.stringify({\n \"model\": \"claude-sonnet-4-5-20250929\",\n \"max_tokens\": 256,\n \"system\": [{\n \"type\": \"text\",\n \"text\": \"You are a Docker container management assistant. Parse user requests and return ONLY valid JSON.\\n\\nValid actions:\\n- view_logs: User wants to see container logs\\n- query_stats: User asks about resource usage (memory, CPU)\\n- container_action: User wants to start/stop/restart/update a container\\n- container_status: User asks about container status\\n- list_containers: User wants to see all containers\\n- unknown: Cannot determine intent\\n\\nRespond with JSON: {\\\"action\\\": \\\"\\\", \\\"container\\\": \\\"\\\", \\\"parameters\\\": {}}\\n\\nExamples:\\n- \\\"show me plex logs\\\" -> {\\\"action\\\": \\\"view_logs\\\", \\\"container\\\": \\\"plex\\\", \\\"parameters\\\": {\\\"lines\\\": 50}}\\n- \\\"what's using the most memory?\\\" -> {\\\"action\\\": \\\"query_stats\\\", \\\"container\\\": null, \\\"parameters\\\": {\\\"metric\\\": \\\"memory\\\", \\\"sort\\\": \\\"desc\\\"}}\\n- \\\"restart nginx\\\" -> {\\\"action\\\": \\\"container_action\\\", \\\"container\\\": \\\"nginx\\\", \\\"parameters\\\": {\\\"action\\\": \\\"restart\\\"}}\\n- \\\"how's sonarr doing?\\\" -> {\\\"action\\\": \\\"container_status\\\", \\\"container\\\": \\\"sonarr\\\", \\\"parameters\\\": {}}\\n- \\\"hello\\\" -> {\\\"action\\\": \\\"unknown\\\", \\\"container\\\": null, \\\"parameters\\\": {\\\"message\\\": \\\"I can help with Docker containers. Try: 'show logs', 'restart plex', or 'what's using memory?'\\\"}}\",\n \"cache_control\": {\"type\": \"ephemeral\"}\n }],\n \"messages\": [{\n \"role\": \"user\",\n \"content\": $json.message.text\n }]\n}) }}",
+ "options": {
+ "timeout": 30000,
+ "retry": {
+ "enabled": true,
+ "maxTries": 3
+ }
+ }
+ },
+ "id": "http-claude-intent",
+ "name": "Claude Intent Parser",
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.2,
+ "position": [
+ 900,
+ 300
+ ],
+ "credentials": {
+ "httpHeaderAuth": {
+ "id": "anthropic-api-key",
+ "name": "Anthropic API Key"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "jsCode": "// Parse and validate Claude's intent response\nconst response = $input.item.json;\n\n// Claude response structure: { content: [{ type: \"text\", text: \"...\" }] }\nlet intentText = '';\ntry {\n intentText = response.content[0].text;\n} catch (e) {\n return {\n action: 'error',\n error: 'Invalid Claude response structure',\n raw: JSON.stringify(response)\n };\n}\n\n// Parse JSON from Claude's response\nlet intent;\ntry {\n // Claude might wrap JSON in markdown code blocks, strip them\n const cleaned = intentText.replace(/```json\\n?/g, '').replace(/```\\n?/g, '').trim();\n intent = JSON.parse(cleaned);\n} catch (e) {\n return {\n action: 'error',\n error: 'Could not parse intent JSON',\n raw: intentText\n };\n}\n\n// Validate required fields\nconst validActions = ['view_logs', 'query_stats', 'container_action', 'container_status', 'list_containers', 'unknown'];\nif (!intent.action || !validActions.includes(intent.action)) {\n return {\n action: 'unknown',\n error: 'Invalid or missing action',\n parameters: { message: 'I didn\\'t understand that. Try: \"show logs plex\" or \"restart nginx\"' }\n };\n}\n\n// Normalize container name if present\nif (intent.container) {\n intent.container = intent.container.toLowerCase().trim();\n}\n\n// Set defaults for parameters\nintent.parameters = intent.parameters || {};\n\n// Preserve original message for fallback\nintent.original_message = $input.item.json.message || {};\n\nreturn intent;"
+ },
+ "id": "code-parse-intent",
+ "name": "Parse Intent",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 1120,
+ 300
+ ]
+ },
+ {
+ "parameters": {
+ "rules": {
+ "values": [
+ {
+ "id": "route-view-logs",
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict"
+ },
+ "conditions": [
+ {
+ "id": "is-view-logs",
+ "leftValue": "={{ $json.action }}",
+ "rightValue": "view_logs",
+ "operator": {
+ "type": "string",
+ "operation": "equals"
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "renameOutput": true,
+ "outputKey": "view_logs"
+ },
+ {
+ "id": "route-query-stats",
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict"
+ },
+ "conditions": [
+ {
+ "id": "is-query-stats",
+ "leftValue": "={{ $json.action }}",
+ "rightValue": "query_stats",
+ "operator": {
+ "type": "string",
+ "operation": "equals"
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "renameOutput": true,
+ "outputKey": "query_stats"
+ },
+ {
+ "id": "route-container-action",
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict"
+ },
+ "conditions": [
+ {
+ "id": "is-container-action",
+ "leftValue": "={{ $json.action }}",
+ "rightValue": "container_action",
+ "operator": {
+ "type": "string",
+ "operation": "equals"
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "renameOutput": true,
+ "outputKey": "container_action"
+ },
+ {
+ "id": "route-container-status",
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict"
+ },
+ "conditions": [
+ {
+ "id": "is-container-status",
+ "leftValue": "={{ $json.action }}",
+ "rightValue": "container_status",
+ "operator": {
+ "type": "string",
+ "operation": "equals"
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "renameOutput": true,
+ "outputKey": "container_status"
+ },
+ {
+ "id": "route-list-containers",
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict"
+ },
+ "conditions": [
+ {
+ "id": "is-list-containers",
+ "leftValue": "={{ $json.action }}",
+ "rightValue": "list_containers",
+ "operator": {
+ "type": "string",
+ "operation": "equals"
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "renameOutput": true,
+ "outputKey": "list_containers"
+ },
+ {
+ "id": "route-unknown",
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict"
+ },
+ "conditions": [
+ {
+ "id": "is-unknown",
+ "leftValue": "={{ $json.action }}",
+ "rightValue": "unknown",
+ "operator": {
+ "type": "string",
+ "operation": "equals"
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "renameOutput": true,
+ "outputKey": "unknown"
+ },
+ {
+ "id": "route-error",
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict"
+ },
+ "conditions": [
+ {
+ "id": "is-error",
+ "leftValue": "={{ $json.action }}",
+ "rightValue": "error",
+ "operator": {
+ "type": "string",
+ "operation": "equals"
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "renameOutput": true,
+ "outputKey": "error"
+ }
+ ]
+ },
+ "options": {
+ "fallbackOutput": "none"
+ }
+ },
+ "id": "switch-intent-router",
+ "name": "Intent Router",
+ "type": "n8n-nodes-base.switch",
+ "typeVersion": 3.2,
+ "position": [
+ 1340,
+ 300
+ ]
+ },
+ {
+ "parameters": {
+ "chatId": "={{ $json.original_message.chat.id }}",
+ "text": "={{ $json.parameters.message || \"I can help manage your Docker containers. Try:\\n- 'show logs plex'\\n- 'restart sonarr'\\n- 'what containers are running?'\\n- 'what's using the most memory?'\" }}",
+ "additionalFields": {
+ "parse_mode": "HTML"
+ }
+ },
+ "id": "telegram-send-unknown",
+ "name": "Send Unknown Intent",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 1560,
+ 100
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "telegram-credential",
+ "name": "Telegram API"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "chatId": "={{ $json.original_message.chat.id }}",
+ "text": "Stats queries coming soon! For now, try 'status' to see running containers.",
+ "additionalFields": {
+ "parse_mode": "HTML"
+ }
+ },
+ "id": "telegram-send-stats-placeholder",
+ "name": "Send Stats Placeholder",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 1560,
+ 200
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "telegram-credential",
+ "name": "Telegram API"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "chatId": "={{ $json.original_message.chat.id }}",
+ "text": "Sorry, I encountered an error understanding your request. Please try again.",
+ "additionalFields": {
+ "parse_mode": "HTML"
+ }
+ },
+ "id": "telegram-send-intent-error",
+ "name": "Send Intent Error",
+ "type": "n8n-nodes-base.telegram",
+ "typeVersion": 1.2,
+ "position": [
+ 1560,
+ 500
+ ],
+ "credentials": {
+ "telegramApi": {
+ "id": "telegram-credential",
+ "name": "Telegram API"
+ }
+ }
}
],
"connections": {
@@ -2103,7 +2413,7 @@
"main": [
[
{
- "node": "Route Message",
+ "node": "Claude Intent Parser",
"type": "main",
"index": 0
}
@@ -2996,6 +3306,81 @@
}
]
]
+ },
+ "Claude Intent Parser": {
+ "main": [
+ [
+ {
+ "node": "Parse Intent",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Parse Intent": {
+ "main": [
+ [
+ {
+ "node": "Intent Router",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Intent Router": {
+ "main": [
+ [
+ {
+ "node": "Parse Logs Command",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Send Stats Placeholder",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Docker List for Action",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Docker List Containers",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Docker List Containers",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Send Unknown Intent",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Send Intent Error",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
}
},
"pinData": {},