@lhi/n8m 0.3.2 → 1.0.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 +50 -10
- package/banner.txt +9 -0
- package/dist/agentic/graph.d.ts +11 -1
- package/dist/agentic/graph.js +2 -1
- package/dist/agentic/nodes/architect.js +3 -2
- package/dist/agentic/nodes/engineer.js +4 -2
- package/dist/agentic/state.d.ts +2 -0
- package/dist/agentic/state.js +4 -0
- package/dist/commands/create.js +15 -2
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.js +25 -8
- package/dist/commands/fixture.js +1 -0
- package/dist/commands/learn.js +1 -1
- package/dist/commands/modify.js +13 -1
- package/dist/commands/rollback.d.ts +31 -0
- package/dist/commands/rollback.js +201 -0
- package/dist/fixture-schema.json +162 -0
- package/dist/help.d.ts +6 -0
- package/dist/help.js +12 -0
- package/dist/resources/node-definitions-fallback.json +390 -0
- package/dist/resources/node-test-hints.json +188 -0
- package/dist/resources/workflow-test-fixtures.json +42 -0
- package/dist/services/ai.service.d.ts +9 -2
- package/dist/services/ai.service.js +27 -6
- package/dist/services/git.service.d.ts +52 -0
- package/dist/services/git.service.js +110 -0
- package/dist/services/mcp.service.d.ts +1 -0
- package/dist/services/mcp.service.js +201 -0
- package/dist/services/node-definitions.service.js +1 -1
- package/dist/utils/config.js +3 -1
- package/dist/utils/n8nClient.d.ts +11 -0
- package/dist/utils/n8nClient.js +39 -0
- package/docs/.nojekyll +0 -0
- package/docs/CNAME +1 -0
- package/docs/DEVELOPER_GUIDE.md +12 -2
- package/docs/apple-touch-icon.png +0 -0
- package/docs/favicon-16x16.png +0 -0
- package/docs/favicon-192x192.png +0 -0
- package/docs/favicon-32x32.png +0 -0
- package/docs/favicon.svg +4 -0
- package/docs/index.html +1577 -0
- package/docs/social-card.html +237 -0
- package/n8m-cover.png +0 -0
- package/n8m-logo-light.png +0 -0
- package/n8m-logo-mono.png +0 -0
- package/n8m-logo-v2.png +0 -0
- package/oclif.manifest.json +68 -1
- package/package.json +11 -1
|
@@ -6,6 +6,23 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import { jsonrepair } from 'jsonrepair';
|
|
7
7
|
import { NodeDefinitionsService } from './node-definitions.service.js';
|
|
8
8
|
import { Spinner } from '../utils/spinner.js';
|
|
9
|
+
/**
|
|
10
|
+
* Build a credential-awareness section for AI prompts.
|
|
11
|
+
* Returns an empty string when no credentials are provided so
|
|
12
|
+
* offline / unconfigured usage is completely unaffected.
|
|
13
|
+
*/
|
|
14
|
+
export function buildCredentialContext(credentials) {
|
|
15
|
+
if (!credentials || credentials.length === 0)
|
|
16
|
+
return '';
|
|
17
|
+
const list = credentials.map(c => `- "${c.name}" (type: ${c.type})`).join('\n');
|
|
18
|
+
return `
|
|
19
|
+
|
|
20
|
+
AVAILABLE CREDENTIALS ON TARGET INSTANCE:
|
|
21
|
+
${list}
|
|
22
|
+
|
|
23
|
+
IMPORTANT: Only generate nodes whose credential type matches one of the types listed above.
|
|
24
|
+
If the goal requires a service not in this list, use the HTTP Request node with generic authentication instead of a dedicated service node.`;
|
|
25
|
+
}
|
|
9
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
27
|
const __dirname = path.dirname(__filename);
|
|
11
28
|
export const PROVIDER_PRESETS = {
|
|
@@ -171,14 +188,16 @@ export class AIService {
|
|
|
171
188
|
}
|
|
172
189
|
getDefaultModel() { return this.model; }
|
|
173
190
|
getDefaultProvider() { return this.defaultProvider; }
|
|
174
|
-
async generateSpec(goal) {
|
|
191
|
+
async generateSpec(goal, availableCredentials = []) {
|
|
175
192
|
const nodeService = NodeDefinitionsService.getInstance();
|
|
176
193
|
const staticRef = nodeService.getStaticReference();
|
|
194
|
+
const credentialContext = buildCredentialContext(availableCredentials);
|
|
177
195
|
const prompt = `You are an n8n Solution Architect.
|
|
178
196
|
Create a technical specification for an n8n workflow that fulfills the following goal: "${goal}".
|
|
179
|
-
|
|
197
|
+
|
|
180
198
|
[N8N NODE REFERENCE GUIDE]
|
|
181
199
|
${staticRef}
|
|
200
|
+
${credentialContext}
|
|
182
201
|
|
|
183
202
|
Your output must be a JSON object with this structure:
|
|
184
203
|
{
|
|
@@ -189,7 +208,7 @@ export class AIService {
|
|
|
189
208
|
],
|
|
190
209
|
"questions": ["Any clarification questions for the user"]
|
|
191
210
|
}
|
|
192
|
-
|
|
211
|
+
|
|
193
212
|
Use ONLY standard n8n node types (e.g. n8n-nodes-base.httpRequest, n8n-nodes-base.slack).
|
|
194
213
|
Output ONLY the JSON object. No commentary.`;
|
|
195
214
|
const response = await this.generateContent(prompt);
|
|
@@ -251,15 +270,17 @@ Output ONLY valid JSON. No markdown.`;
|
|
|
251
270
|
throw new Error(`invalid JSON: ${cleanJson}`);
|
|
252
271
|
}
|
|
253
272
|
}
|
|
254
|
-
async generateAlternativeSpec(goal, primarySpec) {
|
|
273
|
+
async generateAlternativeSpec(goal, primarySpec, availableCredentials = []) {
|
|
255
274
|
const nodeService = NodeDefinitionsService.getInstance();
|
|
256
275
|
const staticRef = nodeService.getStaticReference();
|
|
257
|
-
const
|
|
276
|
+
const credentialContext = buildCredentialContext(availableCredentials);
|
|
277
|
+
const prompt = `You are a Senior n8n Engineer.
|
|
258
278
|
Given the goal: "${goal}" and a primary strategy: ${JSON.stringify(primarySpec)},
|
|
259
279
|
design an ALTERNATIVE strategy (different approach or set of nodes) that achieves the same goal.
|
|
260
|
-
|
|
280
|
+
|
|
261
281
|
[N8N NODE REFERENCE GUIDE]
|
|
262
282
|
${staticRef}
|
|
283
|
+
${credentialContext}
|
|
263
284
|
|
|
264
285
|
Your output must be a JSON object with the same WorkflowSpec structure.
|
|
265
286
|
Output ONLY the JSON object. No commentary.`;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface GitCommit {
|
|
2
|
+
hash: string;
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
5
|
+
export interface WorkflowDiff {
|
|
6
|
+
added: string[];
|
|
7
|
+
removed: string[];
|
|
8
|
+
unchanged: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse the output of `git log --oneline` into structured commits.
|
|
12
|
+
* Pure function — safe to unit test without I/O.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseGitLog(raw: string): GitCommit[];
|
|
15
|
+
/**
|
|
16
|
+
* Format a commit for display in an interactive select list.
|
|
17
|
+
* Shows the 7-char short hash followed by the commit message.
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatCommitChoice(commit: GitCommit): string;
|
|
20
|
+
/**
|
|
21
|
+
* Diff two workflow JSON objects by node names.
|
|
22
|
+
* Returns which node names were added, removed, and how many are unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export declare function diffWorkflowNodes(oldJson: any, newJson: any): WorkflowDiff;
|
|
25
|
+
export declare class GitService {
|
|
26
|
+
private cwd;
|
|
27
|
+
constructor(cwd?: string);
|
|
28
|
+
/**
|
|
29
|
+
* Parse the raw stdout of `git log --oneline` into GitCommit objects.
|
|
30
|
+
* Static so it can be called without instantiation in tests.
|
|
31
|
+
*/
|
|
32
|
+
static parseGitLog(raw: string): GitCommit[];
|
|
33
|
+
/** Returns true if `cwd` is inside a git repository. */
|
|
34
|
+
isGitRepo(): Promise<boolean>;
|
|
35
|
+
/** Returns the absolute path to the repository root. */
|
|
36
|
+
getRepoRoot(): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Converts an absolute path to a path relative to the repo root.
|
|
39
|
+
* Returns null if the path is outside the repository.
|
|
40
|
+
*/
|
|
41
|
+
getRelativePath(absolutePath: string): Promise<string | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Returns git commit history for a file (newest first).
|
|
44
|
+
* Returns [] if the file is untracked or has no commits.
|
|
45
|
+
*/
|
|
46
|
+
getFileHistory(relativePath: string): Promise<GitCommit[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Retrieves the content of a file at a specific git commit.
|
|
49
|
+
* Throws if the hash is invalid or the file did not exist at that commit.
|
|
50
|
+
*/
|
|
51
|
+
getFileAtCommit(hash: string, relativePath: string): Promise<string>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
/**
|
|
6
|
+
* Parse the output of `git log --oneline` into structured commits.
|
|
7
|
+
* Pure function — safe to unit test without I/O.
|
|
8
|
+
*/
|
|
9
|
+
export function parseGitLog(raw) {
|
|
10
|
+
return GitService.parseGitLog(raw);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Format a commit for display in an interactive select list.
|
|
14
|
+
* Shows the 7-char short hash followed by the commit message.
|
|
15
|
+
*/
|
|
16
|
+
export function formatCommitChoice(commit) {
|
|
17
|
+
return `${commit.hash.slice(0, 7)} ${commit.message}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Diff two workflow JSON objects by node names.
|
|
21
|
+
* Returns which node names were added, removed, and how many are unchanged.
|
|
22
|
+
*/
|
|
23
|
+
export function diffWorkflowNodes(oldJson, newJson) {
|
|
24
|
+
const oldNodes = (oldJson?.nodes ?? []).map((n) => n.name);
|
|
25
|
+
const newNodes = (newJson?.nodes ?? []).map((n) => n.name);
|
|
26
|
+
const oldSet = new Set(oldNodes);
|
|
27
|
+
const newSet = new Set(newNodes);
|
|
28
|
+
const added = newNodes.filter(n => !oldSet.has(n));
|
|
29
|
+
const removed = oldNodes.filter(n => !newSet.has(n));
|
|
30
|
+
const unchanged = newNodes.filter(n => oldSet.has(n)).length;
|
|
31
|
+
return { added, removed, unchanged };
|
|
32
|
+
}
|
|
33
|
+
export class GitService {
|
|
34
|
+
cwd;
|
|
35
|
+
constructor(cwd = process.cwd()) {
|
|
36
|
+
this.cwd = cwd;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse the raw stdout of `git log --oneline` into GitCommit objects.
|
|
40
|
+
* Static so it can be called without instantiation in tests.
|
|
41
|
+
*/
|
|
42
|
+
static parseGitLog(raw) {
|
|
43
|
+
return raw
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map(line => line.trim())
|
|
46
|
+
.filter(line => line.length > 0)
|
|
47
|
+
.flatMap(line => {
|
|
48
|
+
const spaceIdx = line.indexOf(' ');
|
|
49
|
+
if (spaceIdx === -1)
|
|
50
|
+
return [];
|
|
51
|
+
const hash = line.slice(0, spaceIdx).trim();
|
|
52
|
+
const message = line.slice(spaceIdx + 1).trim();
|
|
53
|
+
if (!hash || !message)
|
|
54
|
+
return [];
|
|
55
|
+
return [{ hash, message }];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Returns true if `cwd` is inside a git repository. */
|
|
59
|
+
async isGitRepo() {
|
|
60
|
+
try {
|
|
61
|
+
await execFileAsync('git', ['rev-parse', '--git-dir'], { cwd: this.cwd });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Returns the absolute path to the repository root. */
|
|
69
|
+
async getRepoRoot() {
|
|
70
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], { cwd: this.cwd });
|
|
71
|
+
return stdout.trim();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Converts an absolute path to a path relative to the repo root.
|
|
75
|
+
* Returns null if the path is outside the repository.
|
|
76
|
+
*/
|
|
77
|
+
async getRelativePath(absolutePath) {
|
|
78
|
+
try {
|
|
79
|
+
const root = await this.getRepoRoot();
|
|
80
|
+
const rel = path.relative(root, absolutePath);
|
|
81
|
+
if (rel.startsWith('..'))
|
|
82
|
+
return null;
|
|
83
|
+
return rel;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns git commit history for a file (newest first).
|
|
91
|
+
* Returns [] if the file is untracked or has no commits.
|
|
92
|
+
*/
|
|
93
|
+
async getFileHistory(relativePath) {
|
|
94
|
+
try {
|
|
95
|
+
const { stdout } = await execFileAsync('git', ['log', '--oneline', '--', relativePath], { cwd: this.cwd });
|
|
96
|
+
return GitService.parseGitLog(stdout);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Retrieves the content of a file at a specific git commit.
|
|
104
|
+
* Throws if the hash is invalid or the file did not exist at that commit.
|
|
105
|
+
*/
|
|
106
|
+
async getFileAtCommit(hash, relativePath) {
|
|
107
|
+
const { stdout } = await execFileAsync('git', ['show', `${hash}:${relativePath}`], { cwd: this.cwd });
|
|
108
|
+
return stdout;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -3,6 +3,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { runAgenticWorkflow } from "../agentic/graph.js";
|
|
5
5
|
import { theme } from "../utils/theme.js";
|
|
6
|
+
import { N8nClient } from "../utils/n8nClient.js";
|
|
7
|
+
import { ConfigManager } from "../utils/config.js";
|
|
8
|
+
import { DocService } from "../services/doc.service.js";
|
|
6
9
|
/**
|
|
7
10
|
* MCP Service for exposing n8m agentic capabilities as tools.
|
|
8
11
|
*/
|
|
@@ -55,6 +58,93 @@ export class MCPService {
|
|
|
55
58
|
required: ["workflowJson", "goal"],
|
|
56
59
|
},
|
|
57
60
|
},
|
|
61
|
+
{
|
|
62
|
+
name: "modify_workflow",
|
|
63
|
+
description: "Modify an existing n8n workflow JSON based on natural language instructions using the AI agent.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
workflowJson: {
|
|
68
|
+
type: "object",
|
|
69
|
+
description: "The existing workflow JSON to modify",
|
|
70
|
+
},
|
|
71
|
+
instruction: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Natural language description of the modifications to apply",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ["workflowJson", "instruction"],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "deploy_workflow",
|
|
81
|
+
description: "Deploy a workflow JSON to the configured n8n instance. Creates a new workflow or updates an existing one if the workflow has an ID.",
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
workflowJson: {
|
|
86
|
+
type: "object",
|
|
87
|
+
description: "The workflow JSON to deploy",
|
|
88
|
+
},
|
|
89
|
+
forceCreate: {
|
|
90
|
+
type: "boolean",
|
|
91
|
+
description: "Always create as a new workflow, ignoring any existing ID (default: false)",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
required: ["workflowJson"],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "get_workflow",
|
|
99
|
+
description: "Fetch a workflow from the configured n8n instance by its ID.",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
workflowId: {
|
|
104
|
+
type: "string",
|
|
105
|
+
description: "The n8n workflow ID to fetch",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
required: ["workflowId"],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "list_workflows",
|
|
113
|
+
description: "List all workflows on the configured n8n instance.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {},
|
|
117
|
+
required: [],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "delete_workflow",
|
|
122
|
+
description: "Delete a workflow from the configured n8n instance by its ID.",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
workflowId: {
|
|
127
|
+
type: "string",
|
|
128
|
+
description: "The n8n workflow ID to delete",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
required: ["workflowId"],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "generate_docs",
|
|
136
|
+
description: "Generate a Mermaid diagram and README documentation for a workflow JSON.",
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
workflowJson: {
|
|
141
|
+
type: "object",
|
|
142
|
+
description: "The workflow JSON to document",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
required: ["workflowJson"],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
58
148
|
],
|
|
59
149
|
};
|
|
60
150
|
});
|
|
@@ -87,6 +177,108 @@ export class MCPService {
|
|
|
87
177
|
],
|
|
88
178
|
};
|
|
89
179
|
}
|
|
180
|
+
else if (name === "modify_workflow") {
|
|
181
|
+
const workflowJson = args.workflowJson;
|
|
182
|
+
const instruction = String(args.instruction);
|
|
183
|
+
const goal = `Modify the provided workflow based on these instructions: ${instruction}`;
|
|
184
|
+
const result = await runAgenticWorkflow(goal, { workflowJson });
|
|
185
|
+
return {
|
|
186
|
+
content: [
|
|
187
|
+
{
|
|
188
|
+
type: "text",
|
|
189
|
+
text: JSON.stringify(result.workflowJson || result, null, 2),
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
else if (name === "deploy_workflow") {
|
|
195
|
+
const workflowJson = args.workflowJson;
|
|
196
|
+
const forceCreate = Boolean(args.forceCreate ?? false);
|
|
197
|
+
const client = await this.getN8nClient();
|
|
198
|
+
let deployedId;
|
|
199
|
+
if (workflowJson.id && !forceCreate) {
|
|
200
|
+
let exists = false;
|
|
201
|
+
try {
|
|
202
|
+
await client.getWorkflow(workflowJson.id);
|
|
203
|
+
exists = true;
|
|
204
|
+
}
|
|
205
|
+
catch { /* not found */ }
|
|
206
|
+
if (exists) {
|
|
207
|
+
await client.updateWorkflow(workflowJson.id, workflowJson);
|
|
208
|
+
deployedId = workflowJson.id;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const r = await client.createWorkflow(workflowJson.name || "n8m-workflow", workflowJson);
|
|
212
|
+
deployedId = r.id;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const r = await client.createWorkflow(workflowJson.name || "n8m-workflow", workflowJson);
|
|
217
|
+
deployedId = r.id;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
content: [
|
|
221
|
+
{
|
|
222
|
+
type: "text",
|
|
223
|
+
text: JSON.stringify({ id: deployedId, link: client.getWorkflowLink(deployedId) }),
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
else if (name === "get_workflow") {
|
|
229
|
+
const workflowId = String(args.workflowId);
|
|
230
|
+
const client = await this.getN8nClient();
|
|
231
|
+
const workflow = await client.getWorkflow(workflowId);
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: "text",
|
|
236
|
+
text: JSON.stringify(workflow, null, 2),
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
else if (name === "list_workflows") {
|
|
242
|
+
const client = await this.getN8nClient();
|
|
243
|
+
const workflows = await client.getWorkflows();
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: "text",
|
|
248
|
+
text: JSON.stringify(workflows, null, 2),
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
else if (name === "delete_workflow") {
|
|
254
|
+
const workflowId = String(args.workflowId);
|
|
255
|
+
const client = await this.getN8nClient();
|
|
256
|
+
await client.deleteWorkflow(workflowId);
|
|
257
|
+
return {
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: JSON.stringify({ deleted: true, workflowId }),
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
else if (name === "generate_docs") {
|
|
267
|
+
const workflowJson = args.workflowJson;
|
|
268
|
+
const docService = DocService.getInstance();
|
|
269
|
+
const mermaid = docService.generateMermaid(workflowJson);
|
|
270
|
+
const readme = await docService.generateReadme(workflowJson);
|
|
271
|
+
const workflowName = workflowJson.name || "Workflow";
|
|
272
|
+
const fullDoc = `# ${workflowName}\n\n## Visual Flow\n\n\`\`\`mermaid\n${mermaid}\`\`\`\n\n${readme}`;
|
|
273
|
+
return {
|
|
274
|
+
content: [
|
|
275
|
+
{
|
|
276
|
+
type: "text",
|
|
277
|
+
text: fullDoc,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
90
282
|
throw new Error(`Tool not found: ${name}`);
|
|
91
283
|
}
|
|
92
284
|
catch (error) {
|
|
@@ -102,6 +294,15 @@ export class MCPService {
|
|
|
102
294
|
}
|
|
103
295
|
});
|
|
104
296
|
}
|
|
297
|
+
async getN8nClient() {
|
|
298
|
+
const config = await ConfigManager.load();
|
|
299
|
+
const n8nUrl = config.n8nUrl || process.env.N8N_API_URL;
|
|
300
|
+
const n8nKey = config.n8nKey || process.env.N8N_API_KEY;
|
|
301
|
+
if (!n8nUrl || !n8nKey) {
|
|
302
|
+
throw new Error("Missing n8n credentials. Run 'n8m config' to set them.");
|
|
303
|
+
}
|
|
304
|
+
return new N8nClient({ apiUrl: n8nUrl, apiKey: n8nKey });
|
|
305
|
+
}
|
|
105
306
|
async start() {
|
|
106
307
|
const transport = new StdioServerTransport();
|
|
107
308
|
await this.server.connect(transport);
|
|
@@ -166,7 +166,7 @@ export class NodeDefinitionsService {
|
|
|
166
166
|
if (seen.has(file))
|
|
167
167
|
continue; // user pattern overrides built-in of same name
|
|
168
168
|
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
169
|
-
const keywordsMatch = content.match(/<!--\s*keywords:\s*([
|
|
169
|
+
const keywordsMatch = content.match(/<!--\s*keywords:\s*([^-]+)-->/i);
|
|
170
170
|
if (!keywordsMatch)
|
|
171
171
|
continue;
|
|
172
172
|
seen.add(file);
|
package/dist/utils/config.js
CHANGED
|
@@ -2,11 +2,13 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
|
+
// Load .env once at module initialisation. Dotenv skips vars already present
|
|
6
|
+
// in the environment, so this never overwrites values set by the shell or CI.
|
|
7
|
+
dotenv.config({ quiet: true });
|
|
5
8
|
export class ConfigManager {
|
|
6
9
|
static configDir = path.join(os.homedir(), '.n8m');
|
|
7
10
|
static configFile = path.join(os.homedir(), '.n8m', 'config.json');
|
|
8
11
|
static async load() {
|
|
9
|
-
dotenv.config({ quiet: true }); // Load .env from cwd if present (no-op if already loaded or file missing)
|
|
10
12
|
try {
|
|
11
13
|
const data = await fs.readFile(this.configFile, 'utf-8');
|
|
12
14
|
return JSON.parse(data);
|
|
@@ -2,6 +2,11 @@ export interface N8nClientConfig {
|
|
|
2
2
|
apiUrl?: string;
|
|
3
3
|
apiKey?: string;
|
|
4
4
|
}
|
|
5
|
+
export interface N8nCredential {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
}
|
|
5
10
|
export interface WorkflowExecutionResult {
|
|
6
11
|
executionId: string;
|
|
7
12
|
finished: boolean;
|
|
@@ -72,6 +77,12 @@ export declare class N8nClient {
|
|
|
72
77
|
* Handles paginated responses and returns the full node type objects.
|
|
73
78
|
*/
|
|
74
79
|
getNodeTypes(): Promise<any[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Get all credentials configured on the n8n instance.
|
|
82
|
+
* Returns [] gracefully on 401/403/network errors so missing permissions
|
|
83
|
+
* never block workflow generation.
|
|
84
|
+
*/
|
|
85
|
+
getCredentials(): Promise<N8nCredential[]>;
|
|
75
86
|
/**
|
|
76
87
|
* Get all workflows
|
|
77
88
|
*/
|
package/dist/utils/n8nClient.js
CHANGED
|
@@ -283,6 +283,45 @@ export class N8nClient {
|
|
|
283
283
|
return [];
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Get all credentials configured on the n8n instance.
|
|
288
|
+
* Returns [] gracefully on 401/403/network errors so missing permissions
|
|
289
|
+
* never block workflow generation.
|
|
290
|
+
*/
|
|
291
|
+
async getCredentials() {
|
|
292
|
+
try {
|
|
293
|
+
let all = [];
|
|
294
|
+
let cursor = undefined;
|
|
295
|
+
do {
|
|
296
|
+
const url = new URL(`${this.apiUrl}/credentials`);
|
|
297
|
+
if (cursor)
|
|
298
|
+
url.searchParams.set('cursor', cursor);
|
|
299
|
+
const response = await fetch(url.toString(), {
|
|
300
|
+
headers: this.headers,
|
|
301
|
+
method: 'GET',
|
|
302
|
+
});
|
|
303
|
+
if (!response.ok) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
const result = await response.json();
|
|
307
|
+
if (Array.isArray(result)) {
|
|
308
|
+
all = [...all, ...result];
|
|
309
|
+
cursor = undefined;
|
|
310
|
+
}
|
|
311
|
+
else if (result.data && Array.isArray(result.data)) {
|
|
312
|
+
all = [...all, ...result.data];
|
|
313
|
+
cursor = result.nextCursor ?? undefined;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
} while (cursor);
|
|
319
|
+
return all.map((c) => ({ id: String(c.id), name: String(c.name), type: String(c.type) }));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
286
325
|
/**
|
|
287
326
|
* Get all workflows
|
|
288
327
|
*/
|
package/docs/.nojekyll
ADDED
|
File without changes
|
package/docs/CNAME
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
n8m.run
|
package/docs/DEVELOPER_GUIDE.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="../n8m-logo-v2.png" alt="n8m" width="160" />
|
|
3
|
+
</div>
|
|
4
|
+
|
|
1
5
|
# n8m Developer Guide
|
|
2
6
|
|
|
3
7
|
> A deep-dive into the internals of `n8m` for contributors and developers who
|
|
@@ -47,25 +51,31 @@ n8m/
|
|
|
47
51
|
│ │ ├── fixture.ts # capture/init sub-commands for offline fixtures
|
|
48
52
|
│ │ ├── learn.ts # extract pattern knowledge from validated workflows
|
|
49
53
|
│ │ ├── mcp.ts # MCP server entry point
|
|
54
|
+
│ │ ├── rollback.ts # restore workflow to a previous git-tracked version
|
|
50
55
|
│ │ ├── resume.ts
|
|
51
56
|
│ │ ├── prune.ts
|
|
52
57
|
│ │ └── config.ts
|
|
53
58
|
│ ├── services/ # Core business logic services
|
|
54
59
|
│ │ ├── ai.service.ts # LLM abstraction layer
|
|
55
60
|
│ │ ├── doc.service.ts # Documentation generation
|
|
61
|
+
│ │ ├── git.service.ts # Git operations (history, diff, file-at-commit)
|
|
56
62
|
│ │ ├── n8n.service.ts # n8n API helpers
|
|
57
63
|
│ │ ├── mcp.service.ts # MCP server integration
|
|
58
64
|
│ │ └── node-definitions.service.ts # RAG for n8n node schemas
|
|
59
65
|
│ ├── utils/
|
|
60
66
|
│ │ ├── n8nClient.ts # n8n REST API client
|
|
61
|
-
│ │ ├── config.ts # Config file management
|
|
67
|
+
│ │ ├── config.ts # Config file management (~/.n8m/config.json)
|
|
62
68
|
│ │ ├── theme.ts # CLI formatting/theming
|
|
69
|
+
│ │ ├── spinner.ts # Ora spinner wrapper
|
|
70
|
+
│ │ ├── multilinePrompt.tsx # Ink-based multiline input component
|
|
63
71
|
│ │ ├── fixtureManager.ts # Read/write .n8m/fixtures/ (single-file + directory)
|
|
64
72
|
│ │ └── sandbox.ts # Isolated script runner for custom QA tools
|
|
65
73
|
│ └── resources/
|
|
66
74
|
│ └── node-definitions-fallback.json # Static node schema fallback
|
|
67
75
|
├── docs/
|
|
68
|
-
│
|
|
76
|
+
│ ├── DEVELOPER_GUIDE.md # This file
|
|
77
|
+
│ ├── N8N_NODE_REFERENCE.md # Human-readable node reference (for LLM context)
|
|
78
|
+
│ └── patterns/ # AI-generated reusable workflow patterns
|
|
69
79
|
├── test/ # Mocha unit tests
|
|
70
80
|
└── workflows/ # Local workflow project folders
|
|
71
81
|
└── <slug>/
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/favicon.svg
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<rect width="32" height="32" rx="6" fill="#0b0d14"/>
|
|
3
|
+
<text x="16" y="23" font-family="'JetBrains Mono', 'Courier New', monospace" font-size="22" font-weight="700" fill="#22c55e" text-anchor="middle">8</text>
|
|
4
|
+
</svg>
|