@lhi/n8m 0.3.3 → 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.
Files changed (48) hide show
  1. package/README.md +27 -9
  2. package/banner.txt +9 -0
  3. package/dist/agentic/graph.d.ts +11 -1
  4. package/dist/agentic/graph.js +2 -1
  5. package/dist/agentic/nodes/architect.js +3 -2
  6. package/dist/agentic/nodes/engineer.js +4 -2
  7. package/dist/agentic/state.d.ts +2 -0
  8. package/dist/agentic/state.js +4 -0
  9. package/dist/commands/create.js +15 -2
  10. package/dist/commands/deploy.d.ts +2 -0
  11. package/dist/commands/deploy.js +25 -8
  12. package/dist/commands/fixture.js +1 -0
  13. package/dist/commands/learn.js +1 -1
  14. package/dist/commands/modify.js +13 -1
  15. package/dist/commands/rollback.d.ts +31 -0
  16. package/dist/commands/rollback.js +201 -0
  17. package/dist/fixture-schema.json +162 -0
  18. package/dist/help.d.ts +6 -0
  19. package/dist/help.js +12 -0
  20. package/dist/resources/node-definitions-fallback.json +390 -0
  21. package/dist/resources/node-test-hints.json +188 -0
  22. package/dist/resources/workflow-test-fixtures.json +42 -0
  23. package/dist/services/ai.service.d.ts +9 -2
  24. package/dist/services/ai.service.js +27 -6
  25. package/dist/services/git.service.d.ts +52 -0
  26. package/dist/services/git.service.js +110 -0
  27. package/dist/services/mcp.service.d.ts +1 -0
  28. package/dist/services/mcp.service.js +201 -0
  29. package/dist/services/node-definitions.service.js +1 -1
  30. package/dist/utils/config.js +3 -1
  31. package/dist/utils/n8nClient.d.ts +11 -0
  32. package/dist/utils/n8nClient.js +39 -0
  33. package/docs/.nojekyll +0 -0
  34. package/docs/CNAME +1 -0
  35. package/docs/DEVELOPER_GUIDE.md +12 -2
  36. package/docs/apple-touch-icon.png +0 -0
  37. package/docs/favicon-16x16.png +0 -0
  38. package/docs/favicon-192x192.png +0 -0
  39. package/docs/favicon-32x32.png +0 -0
  40. package/docs/favicon.svg +4 -0
  41. package/docs/index.html +1577 -0
  42. package/docs/social-card.html +237 -0
  43. package/n8m-cover.png +0 -0
  44. package/n8m-logo-light.png +0 -0
  45. package/n8m-logo-mono.png +0 -0
  46. package/n8m-logo-v2.png +0 -0
  47. package/oclif.manifest.json +68 -1
  48. 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 prompt = `You are a Senior n8n Engineer.
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
+ }
@@ -5,5 +5,6 @@ export declare class MCPService {
5
5
  private server;
6
6
  constructor();
7
7
  private setupTools;
8
+ private getN8nClient;
8
9
  start(): Promise<void>;
9
10
  }
@@ -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*([^\-]+)-->/i);
169
+ const keywordsMatch = content.match(/<!--\s*keywords:\s*([^-]+)-->/i);
170
170
  if (!keywordsMatch)
171
171
  continue;
172
172
  seen.add(file);
@@ -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
  */
@@ -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
@@ -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
- └── N8N_NODE_REFERENCE.md # Human-readable node reference (for LLM context)
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
@@ -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>