@lhi/n8m 0.1.2 → 0.2.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.
- package/README.md +84 -21
- package/bin/dev.js +8 -0
- package/bin/run.js +7 -1
- package/dist/agentic/graph.d.ts +116 -8
- package/dist/agentic/graph.js +1 -1
- package/dist/agentic/nodes/architect.d.ts +15 -3
- package/dist/agentic/nodes/architect.js +11 -2
- package/dist/agentic/nodes/engineer.d.ts +1 -1
- package/dist/agentic/nodes/engineer.js +19 -91
- package/dist/agentic/nodes/qa.js +66 -55
- package/dist/agentic/nodes/reviewer.js +5 -3
- package/dist/agentic/state.d.ts +10 -0
- package/dist/agentic/state.js +2 -0
- package/dist/commands/create.js +115 -48
- package/dist/commands/doc.d.ts +11 -0
- package/dist/commands/doc.js +136 -0
- package/dist/commands/mcp.d.ts +6 -0
- package/dist/commands/mcp.js +25 -0
- package/dist/commands/modify.js +1 -1
- package/dist/commands/resume.js +40 -1
- package/dist/commands/test.d.ts +1 -0
- package/dist/commands/test.js +36 -4
- package/dist/resources/node-definitions-fallback.json +213 -0
- package/dist/services/ai.service.d.ts +38 -47
- package/dist/services/ai.service.js +302 -350
- package/dist/services/doc.service.d.ts +26 -0
- package/dist/services/doc.service.js +92 -0
- package/dist/services/mcp.service.d.ts +9 -0
- package/dist/services/mcp.service.js +110 -0
- package/dist/services/node-definitions.service.d.ts +5 -0
- package/dist/services/node-definitions.service.js +65 -9
- package/dist/utils/n8nClient.d.ts +10 -10
- package/dist/utils/n8nClient.js +80 -145
- package/oclif.manifest.json +67 -3
- package/package.json +7 -4
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service for generating workflow documentation and diagrams.
|
|
3
|
+
*/
|
|
4
|
+
export declare class DocService {
|
|
5
|
+
private static instance;
|
|
6
|
+
private aiService;
|
|
7
|
+
private constructor();
|
|
8
|
+
static getInstance(): DocService;
|
|
9
|
+
/**
|
|
10
|
+
* Generates a Mermaid.js flowchart diagram from an n8n workflow JSON.
|
|
11
|
+
*/
|
|
12
|
+
generateMermaid(workflowJson: any): string;
|
|
13
|
+
/**
|
|
14
|
+
* Generates an AI-driven README/Summary for the workflow.
|
|
15
|
+
*/
|
|
16
|
+
generateReadme(workflowJson: any): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Generates a folder-safe slug from a name.
|
|
19
|
+
*/
|
|
20
|
+
generateSlug(name: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Uses AI to suggest a concise, professional project title for the workflow.
|
|
23
|
+
*/
|
|
24
|
+
generateProjectTitle(workflowJson: any): Promise<string>;
|
|
25
|
+
private toID;
|
|
26
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { AIService } from "./ai.service.js";
|
|
2
|
+
/**
|
|
3
|
+
* Service for generating workflow documentation and diagrams.
|
|
4
|
+
*/
|
|
5
|
+
export class DocService {
|
|
6
|
+
static instance;
|
|
7
|
+
aiService;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.aiService = AIService.getInstance();
|
|
10
|
+
}
|
|
11
|
+
static getInstance() {
|
|
12
|
+
if (!DocService.instance) {
|
|
13
|
+
DocService.instance = new DocService();
|
|
14
|
+
}
|
|
15
|
+
return DocService.instance;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generates a Mermaid.js flowchart diagram from an n8n workflow JSON.
|
|
19
|
+
*/
|
|
20
|
+
generateMermaid(workflowJson) {
|
|
21
|
+
const nodes = workflowJson.nodes || [];
|
|
22
|
+
const connections = workflowJson.connections || {};
|
|
23
|
+
let mermaid = "graph TD\n";
|
|
24
|
+
// 1. Define Nodes
|
|
25
|
+
nodes.forEach((node) => {
|
|
26
|
+
// Escape node names for Mermaid
|
|
27
|
+
const safeName = node.name.replace(/"/g, "'");
|
|
28
|
+
// Use different shapes/styles based on node type if desired
|
|
29
|
+
// Simple box for now: nodeName["Display Text"]
|
|
30
|
+
mermaid += ` ${this.toID(node.name)}["${safeName}"]\n`;
|
|
31
|
+
});
|
|
32
|
+
// 2. Define Connections
|
|
33
|
+
for (const [sourceName, sourceConns] of Object.entries(connections)) {
|
|
34
|
+
if (sourceConns && sourceConns.main) {
|
|
35
|
+
sourceConns.main.forEach((targets) => {
|
|
36
|
+
targets.forEach((target) => {
|
|
37
|
+
mermaid += ` ${this.toID(sourceName)} --> ${this.toID(target.node)}\n`;
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return mermaid;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Generates an AI-driven README/Summary for the workflow.
|
|
46
|
+
*/
|
|
47
|
+
async generateReadme(workflowJson) {
|
|
48
|
+
const prompt = `You are a technical writer for n8n.
|
|
49
|
+
Generate a concise, professional README for the following n8n workflow.
|
|
50
|
+
|
|
51
|
+
Workflow JSON:
|
|
52
|
+
${JSON.stringify(workflowJson, null, 2)}
|
|
53
|
+
|
|
54
|
+
The README should include:
|
|
55
|
+
1. A clear Title.
|
|
56
|
+
2. A brief 1-2 sentence Summary of what the workflow does.
|
|
57
|
+
3. A "Nodes Used" section listing the key nodes.
|
|
58
|
+
4. An "Execution Flow" section explaining the logic.
|
|
59
|
+
|
|
60
|
+
Output in Markdown format.
|
|
61
|
+
`;
|
|
62
|
+
return await this.aiService.generateContent(prompt) || "Failed to generate documentation.";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generates a folder-safe slug from a name.
|
|
66
|
+
*/
|
|
67
|
+
generateSlug(name) {
|
|
68
|
+
return name
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
71
|
+
.replace(/^-+|-+$/g, '');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Uses AI to suggest a concise, professional project title for the workflow.
|
|
75
|
+
*/
|
|
76
|
+
async generateProjectTitle(workflowJson) {
|
|
77
|
+
const prompt = `Based on the following n8n workflow JSON, suggest a concise, professional project title (3-5 words).
|
|
78
|
+
|
|
79
|
+
Workflow JSON Snippet:
|
|
80
|
+
${JSON.stringify({
|
|
81
|
+
name: workflowJson.name,
|
|
82
|
+
nodes: (workflowJson.nodes || []).map((n) => ({ name: n.name, type: n.type }))
|
|
83
|
+
}, null, 2)}
|
|
84
|
+
|
|
85
|
+
Output ONLY the title string. No quotes. No commentary.`;
|
|
86
|
+
const title = await this.aiService.generateContent(prompt);
|
|
87
|
+
return title?.trim() || workflowJson.name || 'Untitled Workflow';
|
|
88
|
+
}
|
|
89
|
+
toID(name) {
|
|
90
|
+
return name.replace(/[^a-zA-Z0-9]/g, "_");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { runAgenticWorkflow } from "../agentic/graph.js";
|
|
5
|
+
import { theme } from "../utils/theme.js";
|
|
6
|
+
/**
|
|
7
|
+
* MCP Service for exposing n8m agentic capabilities as tools.
|
|
8
|
+
*/
|
|
9
|
+
export class MCPService {
|
|
10
|
+
server;
|
|
11
|
+
constructor() {
|
|
12
|
+
this.server = new Server({
|
|
13
|
+
name: "n8m-agent",
|
|
14
|
+
version: "0.1.0",
|
|
15
|
+
}, {
|
|
16
|
+
capabilities: {
|
|
17
|
+
tools: {},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
this.setupTools();
|
|
21
|
+
}
|
|
22
|
+
setupTools() {
|
|
23
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
24
|
+
return {
|
|
25
|
+
tools: [
|
|
26
|
+
{
|
|
27
|
+
name: "create_workflow",
|
|
28
|
+
description: "Generate an n8n workflow from a natural language description.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
goal: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Natural language description of the workflow goals",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["goal"],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "test_workflow",
|
|
42
|
+
description: "Validate and repair a workflow JSON by deploying it ephemerally to n8n.",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
workflowJson: {
|
|
47
|
+
type: "object",
|
|
48
|
+
description: "The workflow JSON to test",
|
|
49
|
+
},
|
|
50
|
+
goal: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "The original goal or context for testing",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ["workflowJson", "goal"],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
62
|
+
const { name, arguments: args } = request.params;
|
|
63
|
+
try {
|
|
64
|
+
if (name === "create_workflow") {
|
|
65
|
+
const goal = String(args.goal);
|
|
66
|
+
// Run agentic workflow without interactive approval for MCP
|
|
67
|
+
const result = await runAgenticWorkflow(goal);
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: JSON.stringify(result.workflowJson || result, null, 2),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else if (name === "test_workflow") {
|
|
78
|
+
const workflowJson = args.workflowJson;
|
|
79
|
+
const goal = String(args.goal);
|
|
80
|
+
const result = await runAgenticWorkflow(goal, { workflowJson });
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: JSON.stringify(result, null, 2),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`Tool not found: ${name}`);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return {
|
|
94
|
+
content: [
|
|
95
|
+
{
|
|
96
|
+
type: "text",
|
|
97
|
+
text: `Error: ${error.message}`,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
isError: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async start() {
|
|
106
|
+
const transport = new StdioServerTransport();
|
|
107
|
+
await this.server.connect(transport);
|
|
108
|
+
console.error(theme.done("n8m MCP Server started (stdio transport)"));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -15,6 +15,11 @@ export declare class NodeDefinitionsService {
|
|
|
15
15
|
* In a real app, we might cache this to a file.
|
|
16
16
|
*/
|
|
17
17
|
loadDefinitions(): Promise<void>;
|
|
18
|
+
private loadFallback;
|
|
19
|
+
/**
|
|
20
|
+
* Get the human-readable static reference document
|
|
21
|
+
*/
|
|
22
|
+
getStaticReference(): string;
|
|
18
23
|
/**
|
|
19
24
|
* Search for nodes relevant to the query.
|
|
20
25
|
* Simple keyword matching for now.
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { N8nClient } from '../utils/n8nClient.js';
|
|
2
2
|
import { ConfigManager } from '../utils/config.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
3
8
|
export class NodeDefinitionsService {
|
|
4
9
|
static instance;
|
|
5
10
|
definitions = [];
|
|
6
11
|
client;
|
|
7
12
|
constructor() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
this.client = new N8nClient({ apiUrl: n8nUrl, apiKey: n8nKey });
|
|
13
|
+
// Will be overridden in loadDefinitions() once config is available
|
|
14
|
+
this.client = new N8nClient();
|
|
11
15
|
}
|
|
12
16
|
static getInstance() {
|
|
13
17
|
if (!NodeDefinitionsService.instance) {
|
|
@@ -26,18 +30,70 @@ export class NodeDefinitionsService {
|
|
|
26
30
|
try {
|
|
27
31
|
// Re-initialize client if env vars changed (e.g. after config load)
|
|
28
32
|
const config = await ConfigManager.load();
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
// Env vars take priority over stored config
|
|
34
|
+
const apiUrl = process.env.N8N_API_URL || config.n8nUrl;
|
|
35
|
+
const apiKey = process.env.N8N_API_KEY || config.n8nKey;
|
|
36
|
+
if (apiUrl && apiKey) {
|
|
37
|
+
this.client = new N8nClient({ apiUrl, apiKey });
|
|
31
38
|
}
|
|
32
39
|
this.definitions = await this.client.getNodeTypes();
|
|
33
|
-
|
|
40
|
+
if (this.definitions.length === 0) {
|
|
41
|
+
console.warn("No node definitions returned from n8n instance. Attempting fallback...");
|
|
42
|
+
this.loadFallback();
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log(`Loaded ${this.definitions.length} node definitions.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
console.error("Failed to load node definitions from n8n instance (fetch failed).");
|
|
50
|
+
this.loadFallback();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
loadFallback() {
|
|
54
|
+
try {
|
|
55
|
+
// Check multiple potential locations (dist vs src)
|
|
56
|
+
const paths = [
|
|
57
|
+
path.join(__dirname, '..', 'resources', 'node-definitions-fallback.json'), // dist
|
|
58
|
+
path.join(__dirname, '..', '..', 'src', 'resources', 'node-definitions-fallback.json') // src (dev)
|
|
59
|
+
];
|
|
60
|
+
let fallbackPath = '';
|
|
61
|
+
for (const p of paths) {
|
|
62
|
+
if (fs.existsSync(p)) {
|
|
63
|
+
fallbackPath = p;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (fallbackPath) {
|
|
68
|
+
const fallbackData = fs.readFileSync(fallbackPath, 'utf8');
|
|
69
|
+
this.definitions = JSON.parse(fallbackData);
|
|
70
|
+
console.log(`Loaded ${this.definitions.length} node definitions (from fallback at ${path.basename(path.dirname(fallbackPath))}).`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.warn("Fallback node definitions file not found in searched locations.");
|
|
74
|
+
this.definitions = [];
|
|
75
|
+
}
|
|
34
76
|
}
|
|
35
|
-
catch (
|
|
36
|
-
console.error("Failed to load node definitions:",
|
|
37
|
-
// Fallback to empty to allow process to continue without RAG
|
|
77
|
+
catch (fallbackError) {
|
|
78
|
+
console.error("Failed to load fallback node definitions:", fallbackError);
|
|
38
79
|
this.definitions = [];
|
|
39
80
|
}
|
|
40
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Get the human-readable static reference document
|
|
84
|
+
*/
|
|
85
|
+
getStaticReference() {
|
|
86
|
+
try {
|
|
87
|
+
const docPath = path.join(__dirname, '..', '..', 'docs', 'N8N_NODE_REFERENCE.md');
|
|
88
|
+
if (fs.existsSync(docPath)) {
|
|
89
|
+
return fs.readFileSync(docPath, 'utf8');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
console.error("Failed to read N8N_NODE_REFERENCE.md", e);
|
|
94
|
+
}
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
41
97
|
/**
|
|
42
98
|
* Search for nodes relevant to the query.
|
|
43
99
|
* Simple keyword matching for now.
|
|
@@ -25,6 +25,10 @@ export declare class N8nClient {
|
|
|
25
25
|
private apiKey;
|
|
26
26
|
private headers;
|
|
27
27
|
constructor(config?: N8nClientConfig);
|
|
28
|
+
/**
|
|
29
|
+
* Assert that an API response is OK, throwing an actionable error for 401/403.
|
|
30
|
+
*/
|
|
31
|
+
private assertOk;
|
|
28
32
|
/**
|
|
29
33
|
* Activate a workflow
|
|
30
34
|
*/
|
|
@@ -53,6 +57,10 @@ export declare class N8nClient {
|
|
|
53
57
|
* Get workflow by ID
|
|
54
58
|
*/
|
|
55
59
|
getWorkflow(workflowId: string): Promise<unknown>;
|
|
60
|
+
/**
|
|
61
|
+
* Strip invalid timezone from workflow settings so n8n activation never 400s.
|
|
62
|
+
*/
|
|
63
|
+
private sanitizeSettings;
|
|
56
64
|
/**
|
|
57
65
|
* Create a new workflow
|
|
58
66
|
*/
|
|
@@ -60,16 +68,8 @@ export declare class N8nClient {
|
|
|
60
68
|
id: string;
|
|
61
69
|
}>;
|
|
62
70
|
/**
|
|
63
|
-
* Get all installed node types
|
|
64
|
-
*
|
|
65
|
-
* Strategy:
|
|
66
|
-
* 1. Create a workflow with Webhook -> HTTP Request (Internal API)
|
|
67
|
-
* 2. Activate it
|
|
68
|
-
* 3. Call the webhook -> Returns the node types
|
|
69
|
-
*/
|
|
70
|
-
/**
|
|
71
|
-
* Get all installed node types via Probe Workflow (Webhook)
|
|
72
|
-
* Returns full node type objects including parameters, not just names.
|
|
71
|
+
* Get all installed node types directly from the n8n REST API.
|
|
72
|
+
* Handles paginated responses and returns the full node type objects.
|
|
73
73
|
*/
|
|
74
74
|
getNodeTypes(): Promise<any[]>;
|
|
75
75
|
/**
|