@lhi/n8m 0.2.3 → 0.3.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 +149 -11
- package/dist/agentic/graph.d.ts +16 -4
- package/dist/agentic/nodes/architect.d.ts +2 -2
- package/dist/agentic/nodes/architect.js +5 -1
- package/dist/agentic/nodes/engineer.d.ts +6 -0
- package/dist/agentic/nodes/engineer.js +39 -5
- package/dist/commands/create.js +43 -4
- package/dist/commands/deploy.d.ts +2 -1
- package/dist/commands/deploy.js +119 -19
- package/dist/commands/fixture.js +31 -8
- package/dist/commands/learn.d.ts +19 -0
- package/dist/commands/learn.js +277 -0
- package/dist/commands/modify.js +210 -68
- package/dist/commands/test.d.ts +4 -0
- package/dist/commands/test.js +118 -14
- package/dist/services/ai.service.d.ts +33 -0
- package/dist/services/ai.service.js +337 -2
- package/dist/services/node-definitions.service.d.ts +8 -0
- package/dist/services/node-definitions.service.js +45 -0
- package/dist/utils/fixtureManager.d.ts +10 -0
- package/dist/utils/fixtureManager.js +43 -4
- package/dist/utils/multilinePrompt.js +33 -47
- package/dist/utils/n8nClient.js +60 -11
- package/docs/DEVELOPER_GUIDE.md +598 -0
- package/docs/N8N_NODE_REFERENCE.md +369 -0
- package/docs/patterns/bigquery-via-http.md +110 -0
- package/oclif.manifest.json +82 -3
- package/package.json +3 -1
- package/dist/fixture-schema.json +0 -162
- package/dist/resources/node-definitions-fallback.json +0 -390
- package/dist/resources/node-test-hints.json +0 -188
- package/dist/resources/workflow-test-fixtures.json +0 -42
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
# n8m Developer Guide
|
|
2
|
+
|
|
3
|
+
> A deep-dive into the internals of `n8m` for contributors and developers who
|
|
4
|
+
> want to understand, extend, or build on the project.
|
|
5
|
+
|
|
6
|
+
## Table of Contents
|
|
7
|
+
|
|
8
|
+
- [Project Structure](#project-structure)
|
|
9
|
+
- [Architecture Overview](#architecture-overview)
|
|
10
|
+
- [The Agentic Graph](#the-agentic-graph)
|
|
11
|
+
- [TeamState](#teamstate)
|
|
12
|
+
- [Agent Nodes](#agent-nodes)
|
|
13
|
+
- [Graph Edges & Control Flow](#graph-edges--control-flow)
|
|
14
|
+
- [AI Service](#ai-service)
|
|
15
|
+
- [Node Definitions & RAG](#node-definitions--rag)
|
|
16
|
+
- [CLI Commands](#cli-commands)
|
|
17
|
+
- [Testing Infrastructure](#testing-infrastructure)
|
|
18
|
+
- [Extending n8m](#extending-n8m)
|
|
19
|
+
- [Environment Variables](#environment-variables)
|
|
20
|
+
- [Local Development](#local-development)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Project Structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
n8m/
|
|
28
|
+
├── bin/ # CLI entry points
|
|
29
|
+
├── docs/ # Documentation (you are here)
|
|
30
|
+
├── src/
|
|
31
|
+
│ ├── agentic/ # LangGraph multi-agent system
|
|
32
|
+
│ │ ├── graph.ts # Graph definition, edges, and exports
|
|
33
|
+
│ │ ├── state.ts # TeamState (shared agent memory)
|
|
34
|
+
│ │ ├── checkpointer.ts # SQLite persistence for sessions
|
|
35
|
+
│ │ └── nodes/ # Individual agent node implementations
|
|
36
|
+
│ │ ├── architect.ts # Blueprint designer
|
|
37
|
+
│ │ ├── engineer.ts # Workflow JSON generator
|
|
38
|
+
│ │ ├── reviewer.ts # Static structural validator
|
|
39
|
+
│ │ ├── supervisor.ts # Candidate selector (fan-in)
|
|
40
|
+
│ │ └── qa.ts # Live ephemeral tester
|
|
41
|
+
│ ├── commands/ # oclif CLI command handlers
|
|
42
|
+
│ │ ├── create.ts
|
|
43
|
+
│ │ ├── modify.ts
|
|
44
|
+
│ │ ├── test.ts
|
|
45
|
+
│ │ ├── deploy.ts
|
|
46
|
+
│ │ ├── doc.ts
|
|
47
|
+
│ │ ├── fixture.ts # capture/init sub-commands for offline fixtures
|
|
48
|
+
│ │ ├── learn.ts # extract pattern knowledge from validated workflows
|
|
49
|
+
│ │ ├── mcp.ts # MCP server entry point
|
|
50
|
+
│ │ ├── resume.ts
|
|
51
|
+
│ │ ├── prune.ts
|
|
52
|
+
│ │ └── config.ts
|
|
53
|
+
│ ├── services/ # Core business logic services
|
|
54
|
+
│ │ ├── ai.service.ts # LLM abstraction layer
|
|
55
|
+
│ │ ├── doc.service.ts # Documentation generation
|
|
56
|
+
│ │ ├── n8n.service.ts # n8n API helpers
|
|
57
|
+
│ │ ├── mcp.service.ts # MCP server integration
|
|
58
|
+
│ │ └── node-definitions.service.ts # RAG for n8n node schemas
|
|
59
|
+
│ ├── utils/
|
|
60
|
+
│ │ ├── n8nClient.ts # n8n REST API client
|
|
61
|
+
│ │ ├── config.ts # Config file management
|
|
62
|
+
│ │ ├── theme.ts # CLI formatting/theming
|
|
63
|
+
│ │ ├── fixtureManager.ts # Read/write .n8m/fixtures/ (single-file + directory)
|
|
64
|
+
│ │ └── sandbox.ts # Isolated script runner for custom QA tools
|
|
65
|
+
│ └── resources/
|
|
66
|
+
│ └── node-definitions-fallback.json # Static node schema fallback
|
|
67
|
+
├── docs/
|
|
68
|
+
│ └── N8N_NODE_REFERENCE.md # Human-readable node reference (for LLM context)
|
|
69
|
+
├── test/ # Mocha unit tests
|
|
70
|
+
└── workflows/ # Local workflow project folders
|
|
71
|
+
└── <slug>/
|
|
72
|
+
├── workflow.json
|
|
73
|
+
└── README.md
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Architecture Overview
|
|
79
|
+
|
|
80
|
+
`n8m` uses a **multi-agent LangGraph pipeline** to translate a natural-language
|
|
81
|
+
goal into a validated n8n workflow JSON. The pipeline is composed of several
|
|
82
|
+
specialized AI agents, each with a distinct role:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
Developer → n8m create "Send daily Slack digest"
|
|
86
|
+
│
|
|
87
|
+
▼
|
|
88
|
+
┌───────────────┐
|
|
89
|
+
│ Architect │ Generates 2 strategies (Primary + Alternative)
|
|
90
|
+
└───────┬───────┘
|
|
91
|
+
│ Send() fan-out
|
|
92
|
+
┌────────┴────────┐
|
|
93
|
+
▼ ▼
|
|
94
|
+
┌──────────┐ ┌──────────┐ (Parallel Engineers — each works on one strategy)
|
|
95
|
+
│ Engineer │ │ Engineer │
|
|
96
|
+
└────┬─────┘ └────┬─────┘
|
|
97
|
+
└────────┬────────┘
|
|
98
|
+
│ candidates[]
|
|
99
|
+
▼
|
|
100
|
+
┌──────────────┐
|
|
101
|
+
│ Supervisor │ AI picks the best candidate
|
|
102
|
+
└──────┬───────┘
|
|
103
|
+
│
|
|
104
|
+
▼
|
|
105
|
+
┌───────────┐
|
|
106
|
+
│ Reviewer │ Static structural validation (node types, orphans, connections)
|
|
107
|
+
└──────┬────┘
|
|
108
|
+
pass │ fail
|
|
109
|
+
┌─────┴─────┐
|
|
110
|
+
▼ ▼
|
|
111
|
+
┌─────┐ ┌──────────┐
|
|
112
|
+
│ QA │ │ Engineer │◄─ repair loop
|
|
113
|
+
└──┬──┘ └──────────┘
|
|
114
|
+
pass │ fail
|
|
115
|
+
│ ┌──────────┐
|
|
116
|
+
│ │ Engineer │◄─ self-correction loop
|
|
117
|
+
▼
|
|
118
|
+
END
|
|
119
|
+
./workflows/<slug>/
|
|
120
|
+
├── workflow.json
|
|
121
|
+
└── README.md
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The pipeline leverages [LangGraph](https://github.com/langchain-ai/langgraphjs)
|
|
125
|
+
for orchestration, with SQLite-backed checkpointing for session persistence and
|
|
126
|
+
HITL (Human-in-the-Loop) interrupts.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## The Agentic Graph
|
|
131
|
+
|
|
132
|
+
### TeamState
|
|
133
|
+
|
|
134
|
+
Defined in `src/agentic/state.ts`. This is the **shared memory** of the entire
|
|
135
|
+
pipeline — all agents read from and write to this object.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// src/agentic/state.ts
|
|
139
|
+
export const TeamState = Annotation.Root({
|
|
140
|
+
userGoal: Annotation<string>, // The original user prompt
|
|
141
|
+
spec: Annotation<any>, // Workflow spec from Architect
|
|
142
|
+
workflowJson: Annotation<any>, // The generated/fixed workflow
|
|
143
|
+
validationErrors: Annotation<string[]>, // Errors from Reviewer or QA
|
|
144
|
+
validationStatus: Annotation<"passed" | "failed">,
|
|
145
|
+
availableNodeTypes: Annotation<string[]>, // Node types from live n8n instance
|
|
146
|
+
revisionCount: Annotation<number>, // How many repair loops have run
|
|
147
|
+
strategies: Annotation<any[]>, // Multiple Architect strategies (for parallel Engineers)
|
|
148
|
+
candidates: Annotation<any[]>, // Each Engineer pushes here (fan-in reducer)
|
|
149
|
+
collaborationLog: Annotation<string[]>, // Agent audit trail
|
|
150
|
+
userFeedback: Annotation<string>, // HITL feedback from user
|
|
151
|
+
testScenarios: Annotation<any[]>, // AI-generated test input payloads
|
|
152
|
+
customTools: Annotation<Record<string, string>>, // Dynamic scripts for QA sandbox
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Key design note**: `candidates` uses a custom `reducer` that concatenates
|
|
157
|
+
incoming arrays. This is what enables the parallel fan-out pattern — each
|
|
158
|
+
Engineer pushes one candidate, and LangGraph merges them for the Supervisor to
|
|
159
|
+
evaluate.
|
|
160
|
+
|
|
161
|
+
### Agent Nodes
|
|
162
|
+
|
|
163
|
+
Each node is a plain `async function(state) => Partial<TeamState>`. They live in
|
|
164
|
+
`src/agentic/nodes/`.
|
|
165
|
+
|
|
166
|
+
| Node | File | Role |
|
|
167
|
+
| ------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
168
|
+
| `architect` | `architect.ts` | Calls `AIService.generateSpec()` twice (primary + alternative) to produce two strategies. Returns `{ strategies, spec }`. |
|
|
169
|
+
| `engineer` | `engineer.ts` | Receives one strategy via `Send()`. Performs RAG lookup for relevant node schemas, then calls `AIService` to generate full workflow JSON. Returns `{ candidates: [result] }` or repairs an existing `workflowJson`. |
|
|
170
|
+
| `supervisor` | `supervisor.ts` | Receives all candidates from Engineers. Calls `AIService.evaluateCandidates()` to have an AI pick the best one. Sets `workflowJson`. |
|
|
171
|
+
| `reviewer` | `reviewer.ts` | Performs **pure static validation** (no AI, no network). Detects hallucinated node types, orphaned nodes, and missing sub-workflow IDs. |
|
|
172
|
+
| `qa` | `qa.ts` | Deploys the workflow ephemerally to your n8n instance, runs test scenarios via webhook, verifies execution results, and cleans up. |
|
|
173
|
+
|
|
174
|
+
### Graph Edges & Control Flow
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// src/agentic/graph.ts (simplified)
|
|
178
|
+
workflow
|
|
179
|
+
.addEdge(START, "architect")
|
|
180
|
+
// Fan-out: One Engineer for each Architect strategy
|
|
181
|
+
.addConditionalEdges("architect", (state) => {
|
|
182
|
+
if (state.strategies?.length > 0) {
|
|
183
|
+
return state.strategies.map((s) =>
|
|
184
|
+
new Send("engineer", { spec: s })
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return "engineer"; // fallback
|
|
188
|
+
}, ["engineer"])
|
|
189
|
+
// Route: if repairing (errors present) → skip Supervisor → go to Reviewer
|
|
190
|
+
.addConditionalEdges("engineer", (state) => {
|
|
191
|
+
return state.validationErrors?.length > 0 ? "reviewer" : "supervisor";
|
|
192
|
+
}, ["supervisor", "reviewer"])
|
|
193
|
+
.addEdge("supervisor", "reviewer")
|
|
194
|
+
// Reviewer: pass → QA, fail → back to Engineer
|
|
195
|
+
.addConditionalEdges(
|
|
196
|
+
"reviewer",
|
|
197
|
+
(state) => state.validationStatus === "passed" ? "passed" : "failed",
|
|
198
|
+
{ passed: "qa", failed: "engineer" },
|
|
199
|
+
)
|
|
200
|
+
// QA: pass → END, fail → back to Engineer (self-correction loop)
|
|
201
|
+
.addConditionalEdges(
|
|
202
|
+
"qa",
|
|
203
|
+
(state) => state.validationStatus === "passed" ? "passed" : "failed",
|
|
204
|
+
{ passed: END, failed: "engineer" },
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// HITL interrupts fire before these nodes, pausing for user review
|
|
208
|
+
export const graph = workflow.compile({
|
|
209
|
+
checkpointer,
|
|
210
|
+
interruptBefore: ["engineer", "qa"],
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**HITL (Human-in-the-Loop)**: The graph pauses execution before `engineer` and
|
|
215
|
+
`qa`. The CLI commands (`create.ts`, `test.ts`) detect this pause by checking
|
|
216
|
+
`graph.getState()`, prompt the user for input, then call
|
|
217
|
+
`graph.stream(null, ...)` to resume.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## AI Service
|
|
222
|
+
|
|
223
|
+
`src/services/ai.service.ts` is the **single abstraction layer** for all LLM
|
|
224
|
+
calls. It supports OpenAI, Anthropic (Claude), Google Gemini, and any
|
|
225
|
+
OpenAI-compatible API (Ollama, Groq, etc.).
|
|
226
|
+
|
|
227
|
+
### Key Methods
|
|
228
|
+
|
|
229
|
+
| Method | Description |
|
|
230
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
231
|
+
| `generateContent(prompt, options?)` | Low-level LLM call with retry logic (3 attempts, exponential backoff). |
|
|
232
|
+
| `generateSpec(goal)` | Produces a `WorkflowSpec` (blueprint) from a user goal. |
|
|
233
|
+
| `generateAlternativeSpec(goal, primarySpec)` | Generates a second, different strategy — uses the "alternative model" for diversity. |
|
|
234
|
+
| `generateWorkflowFix(workflow, errors, model?)` | Sends a failing workflow + error list to the LLM for repair. |
|
|
235
|
+
| `evaluateCandidates(goal, candidates)` | AI picks the best candidate workflow from the list. |
|
|
236
|
+
| `generateTestScenarios(workflowJson, goal)` | Returns 3 test payloads: happy path, edge case, error case. |
|
|
237
|
+
| `evaluateTestError(error, nodes, failingNode)` | Classifies a live test failure and returns a `TestErrorEvaluation` describing what action to take. Used by the self-healing loop in `test.ts` and `qa.ts`. |
|
|
238
|
+
| `inferBinaryFieldName(predecessorNode)` | Given a Code node that produces binary output, reads its `jsCode` and asks the AI what the binary field name is. Returns `string \| null`. |
|
|
239
|
+
| `fixHallucinatedNodes(workflow)` | Corrects known-bad node type strings (e.g. `rssFeed` → `rssFeedRead`). |
|
|
240
|
+
| `validateAndShim(workflow, validNodeTypes)` | Replaces truly unknown node types with safe shims (`n8n-nodes-base.set`). |
|
|
241
|
+
|
|
242
|
+
### Provider Configuration
|
|
243
|
+
|
|
244
|
+
The service reads credentials from (in priority order):
|
|
245
|
+
|
|
246
|
+
1. Environment variables (`AI_API_KEY`, `AI_PROVIDER`, `AI_MODEL`,
|
|
247
|
+
`AI_BASE_URL`)
|
|
248
|
+
2. `~/.n8m/config.json` (written by `n8m config`)
|
|
249
|
+
|
|
250
|
+
Anthropic is called via its native `/messages` REST API because it doesn't fully
|
|
251
|
+
conform to the OpenAI SDK. All others use the OpenAI SDK with a custom
|
|
252
|
+
`baseURL`.
|
|
253
|
+
|
|
254
|
+
### Parallel Strategies & Model Diversity
|
|
255
|
+
|
|
256
|
+
The Architect generates a primary strategy using the default model, and an
|
|
257
|
+
alternative strategy using `getAlternativeModel()`. This method returns a
|
|
258
|
+
different model tier from the same provider (e.g., `claude-haiku` if using
|
|
259
|
+
`claude-sonnet`), ensuring genuine architectural diversity in the two candidates
|
|
260
|
+
before the Supervisor picks the winner.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Node Definitions & RAG
|
|
265
|
+
|
|
266
|
+
`src/services/node-definitions.service.ts` provides **Retrieval-Augmented
|
|
267
|
+
Generation** for n8n node schemas, helping the Engineer produce accurate node
|
|
268
|
+
configurations.
|
|
269
|
+
|
|
270
|
+
### Loading Strategy (with Fallback)
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
1. Fetch live node types from n8n instance via /nodes endpoint
|
|
274
|
+
↓ (on failure)
|
|
275
|
+
2. Load from src/resources/node-definitions-fallback.json (static snapshot)
|
|
276
|
+
↓ (on failure)
|
|
277
|
+
3. Empty — RAG disabled, Engineer uses base knowledge only
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### How RAG Works in the Engineer Node
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// src/agentic/nodes/engineer.ts
|
|
284
|
+
const nodeService = NodeDefinitionsService.getInstance();
|
|
285
|
+
await nodeService.loadDefinitions();
|
|
286
|
+
|
|
287
|
+
// Build a query from goal + spec description
|
|
288
|
+
const queryText = state.userGoal + " " + state.spec.suggestedName;
|
|
289
|
+
|
|
290
|
+
// Keyword search — returns up to 8 reduced definitions
|
|
291
|
+
const relevantDefs = nodeService.search(queryText, 8);
|
|
292
|
+
|
|
293
|
+
// Static markdown reference (loaded from docs/N8N_NODE_REFERENCE.md)
|
|
294
|
+
const staticRef = nodeService.getStaticReference();
|
|
295
|
+
|
|
296
|
+
// Both are injected into the Engineer's LLM prompt
|
|
297
|
+
const ragContext =
|
|
298
|
+
`[N8N NODE REFERENCE GUIDE]\n${staticRef}\n\n[AVAILABLE NODE SCHEMAS]\n${
|
|
299
|
+
nodeService.formatForLLM(relevantDefs)
|
|
300
|
+
}`;
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Updating the Fallback / Reference
|
|
304
|
+
|
|
305
|
+
- **`src/resources/node-definitions-fallback.json`**: A JSON snapshot of n8n
|
|
306
|
+
node type definitions. Update this periodically from a live n8n instance.
|
|
307
|
+
- **`docs/N8N_NODE_REFERENCE.md`**: A human-readable markdown reference injected
|
|
308
|
+
into the Architect and Engineer prompts. Editable manually. This is the
|
|
309
|
+
primary guide for the AI when choosing node types and parameters.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## CLI Commands
|
|
314
|
+
|
|
315
|
+
All commands are built with [oclif](https://oclif.io/) and live in
|
|
316
|
+
`src/commands/`. They handle user I/O and then delegate to the agentic graph or
|
|
317
|
+
services.
|
|
318
|
+
|
|
319
|
+
| Command | File | Description |
|
|
320
|
+
| --------- | ------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
321
|
+
| `create` | `create.ts` | Runs `runAgenticWorkflowStream()`, handles HITL prompts, organizes output into project folders, auto-generates docs. |
|
|
322
|
+
| `modify` | `modify.ts` | Loads an existing workflow, builds a modification goal, passes to `runAgenticWorkflow()`. |
|
|
323
|
+
| `test` | `test.ts` | Resolves sub-workflow dependencies, runs the agentic validator/repairer, handles ephemeral deploy/cleanup. Also drives the offline fixture replay loop. |
|
|
324
|
+
| `deploy` | `deploy.ts` | Directly pushes a local JSON to the n8n instance. |
|
|
325
|
+
| `doc` | `doc.ts` | Uses `DocService` to generate Mermaid diagrams + AI summaries, organizes loose files into project folders. |
|
|
326
|
+
| `fixture` | `fixture.ts` | Two sub-commands: `capture` (pull real execution data from n8n → named fixture) and `init` (scaffold empty template). Fixtures stored in `.n8m/fixtures/<workflowId>/<name>.json`. |
|
|
327
|
+
| `learn` | `learn.ts` | Extracts reusable patterns from validated workflow JSON and writes `.md` pattern files to `.n8m/patterns/`. Also supports `--github owner/repo` to import patterns from a public GitHub archive. |
|
|
328
|
+
| `mcp` | `mcp.ts` | Launches the MCP (Model Context Protocol) server over stdio, exposing `create_workflow` and `test_workflow` as tools for Claude Desktop and other MCP clients. |
|
|
329
|
+
| `resume` | `resume.ts` | Resumes a paused graph session by thread ID from the SQLite checkpointer. |
|
|
330
|
+
| `prune` | `prune.ts` | Deletes `[n8m:test:*]` prefixed workflows from the n8n instance. |
|
|
331
|
+
| `config` | `config.ts` | Reads/writes `~/.n8m/config.json`. |
|
|
332
|
+
|
|
333
|
+
### Project Folder Output
|
|
334
|
+
|
|
335
|
+
When a workflow is created or saved, it is organized into a slug-named folder:
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
./workflows/
|
|
339
|
+
└── send-daily-slack-digest/
|
|
340
|
+
├── workflow.json ← the n8n workflow
|
|
341
|
+
└── README.md ← AI-generated doc with Mermaid diagram
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The slug is generated by `DocService.generateSlug()`, which lowercases the name
|
|
345
|
+
and replaces non-alphanumeric characters with hyphens.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Testing Infrastructure
|
|
350
|
+
|
|
351
|
+
Tests live in `test/` and run with [Mocha](https://mochajs.org/) +
|
|
352
|
+
[Sinon](https://sinonjs.org/).
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# Run all tests
|
|
356
|
+
npm test
|
|
357
|
+
|
|
358
|
+
# Watch mode
|
|
359
|
+
npm run dev
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Key Testing Principles
|
|
363
|
+
|
|
364
|
+
- **No live AI calls in tests**: `process.env.NODE_ENV=test` is set by
|
|
365
|
+
`.mocharc.json`. The `AIService` and `N8nClient` must be fully mocked in test
|
|
366
|
+
files via `sinon.stub()` before calling any tested code. Importing them causes
|
|
367
|
+
the singleton to initialize — ensure stubs are applied first.
|
|
368
|
+
- **Ephemeral test workflows**: `n8m test` deploys workflows to n8n with a
|
|
369
|
+
`[n8m:test]` name prefix and deletes them in the `finally` block — even on
|
|
370
|
+
failure.
|
|
371
|
+
- **AI Scenario Generation**: Use `--ai-scenarios` flag to have the QA node
|
|
372
|
+
generate 3 diverse test payloads automatically (happy path, edge case, error).
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Extending n8m
|
|
377
|
+
|
|
378
|
+
### Adding a New Agent Node
|
|
379
|
+
|
|
380
|
+
1. Create a new file in `src/agentic/nodes/my-node.ts`:
|
|
381
|
+
```typescript
|
|
382
|
+
import { TeamState } from "../state.js";
|
|
383
|
+
|
|
384
|
+
export const myNode = async (state: typeof TeamState.State) => {
|
|
385
|
+
// Read from state, do work, return partial state
|
|
386
|
+
return {
|
|
387
|
+
collaborationLog: ["myNode: did something"],
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
2. Register it in `src/agentic/graph.ts`:
|
|
393
|
+
```typescript
|
|
394
|
+
import { myNode } from "./nodes/my-node.js";
|
|
395
|
+
|
|
396
|
+
const workflow = new StateGraph(TeamState)
|
|
397
|
+
.addNode("myNode", myNode);
|
|
398
|
+
// ... add edges
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
3. Add any new state fields to `src/agentic/state.ts`.
|
|
402
|
+
|
|
403
|
+
### Adding a New CLI Command
|
|
404
|
+
|
|
405
|
+
1. Create `src/commands/my-command.ts` extending `Command` from `@oclif/core`.
|
|
406
|
+
2. Register it in `package.json` under the `oclif.commands` field (or in the
|
|
407
|
+
commands directory manifest).
|
|
408
|
+
|
|
409
|
+
### Adding a New AI Provider
|
|
410
|
+
|
|
411
|
+
`AIService` wraps the OpenAI SDK with custom `baseURL`. For any
|
|
412
|
+
OpenAI-compatible API:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
n8m config --ai-base-url https://api.my-provider.com/v1 --ai-key <key> --ai-model my-model
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
For non-compatible APIs, implement a new private call method in `AIService`
|
|
419
|
+
(similar to `callAnthropicNative`) and route to it in `generateContent()`.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Self-Healing Test Loop
|
|
424
|
+
|
|
425
|
+
Both `src/commands/test.ts` (`testRemoteWorkflowDirectly`) and
|
|
426
|
+
`src/agentic/nodes/qa.ts` implement the same AI-powered repair cycle:
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
1. Fire webhook / execute workflow
|
|
430
|
+
2. Poll for execution result
|
|
431
|
+
3. On failure → call AIService.evaluateTestError(error, nodes, failingNodeName)
|
|
432
|
+
4. Dispatch on returned action:
|
|
433
|
+
├── fix_node/code_node_js → patch JS syntax in the Code node's jsCode
|
|
434
|
+
├── fix_node/execute_command → patch shell script in Execute Command node
|
|
435
|
+
├── fix_node/binary_field → correct a wrong binary field name (see below)
|
|
436
|
+
├── regenerate_payload → ask AI to produce a new test input payload
|
|
437
|
+
├── structural_pass → test environment limitation; mark as pass
|
|
438
|
+
└── escalate → fundamental design flaw; abort with message
|
|
439
|
+
5. Apply fix, redeploy, retry
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### `TestErrorEvaluation` interface
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// src/services/ai.service.ts
|
|
446
|
+
export interface TestErrorEvaluation {
|
|
447
|
+
action: 'fix_node' | 'regenerate_payload' | 'structural_pass' | 'escalate';
|
|
448
|
+
nodeFixType?: 'code_node_js' | 'execute_command' | 'binary_field';
|
|
449
|
+
targetNodeName?: string;
|
|
450
|
+
suggestedBinaryField?: string;
|
|
451
|
+
reason: string;
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Binary field fix flow
|
|
456
|
+
|
|
457
|
+
When `nodeFixType === 'binary_field'` (error: `"has no binary field 'X'"`):
|
|
458
|
+
|
|
459
|
+
1. Find the predecessor node via the workflow's `connections` map.
|
|
460
|
+
2. If predecessor is an **HTTP Request** node → use `'data'` (always correct).
|
|
461
|
+
3. If predecessor is a **Code node** → call `AIService.inferBinaryFieldName(node)`,
|
|
462
|
+
which reads `jsCode` and asks the AI what the binary field is called.
|
|
463
|
+
4. Any other predecessor type → `structural_pass` (can't determine field).
|
|
464
|
+
5. After **any** `binary_field` fix attempt, a subsequent binary error on the
|
|
465
|
+
same run → `structural_pass` (avoids infinite loops in test environments
|
|
466
|
+
that don't support binary pin injection).
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Fixture Infrastructure
|
|
471
|
+
|
|
472
|
+
Fixtures are managed by `src/utils/fixtureManager.ts` (`FixtureManager` class).
|
|
473
|
+
|
|
474
|
+
### Storage format
|
|
475
|
+
|
|
476
|
+
```
|
|
477
|
+
.n8m/fixtures/
|
|
478
|
+
<workflowId>/ ← new multi-fixture directory (one file per scenario)
|
|
479
|
+
happy-path.json
|
|
480
|
+
error-case.json
|
|
481
|
+
<workflowId>.json ← legacy single-file format (still supported for reads)
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
`FixtureManager.loadAll(workflowId)` prefers the directory format, falling back
|
|
485
|
+
to the single legacy file if no directory exists.
|
|
486
|
+
|
|
487
|
+
### `WorkflowFixture` schema
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
interface WorkflowFixture {
|
|
491
|
+
version: '1.0';
|
|
492
|
+
capturedAt: string; // ISO timestamp
|
|
493
|
+
workflowId: string;
|
|
494
|
+
workflowName: string;
|
|
495
|
+
description?: string; // human label, e.g. "happy-path"
|
|
496
|
+
expectedOutcome?: 'pass' | 'fail'; // default: 'pass'
|
|
497
|
+
workflow: any; // full workflow JSON
|
|
498
|
+
execution: {
|
|
499
|
+
id?: string;
|
|
500
|
+
status: string;
|
|
501
|
+
startedAt?: string;
|
|
502
|
+
data: {
|
|
503
|
+
resultData: {
|
|
504
|
+
error?: any;
|
|
505
|
+
runData: Record<string, any[]>; // keyed by exact node name
|
|
506
|
+
};
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Key methods
|
|
513
|
+
|
|
514
|
+
| Method | Description |
|
|
515
|
+
|---|---|
|
|
516
|
+
| `exists(workflowId)` | Returns `true` if any fixture (directory or legacy file) exists for the workflow. |
|
|
517
|
+
| `loadAll(workflowId)` | Returns all fixtures for a workflow as an array; used by `test.ts` to run every scenario. |
|
|
518
|
+
| `load(workflowId)` | Legacy: loads the single `.n8m/fixtures/<workflowId>.json` file. |
|
|
519
|
+
| `loadFromPath(filePath)` | Loads a fixture from an explicit path (used with `--fixture` flag). |
|
|
520
|
+
| `saveNamed(fixture, name)` | Saves to the per-workflow directory (new multi-fixture format). |
|
|
521
|
+
| `save(fixture)` | Legacy single-file save; used by `offerSaveFixture` after live test runs. |
|
|
522
|
+
| `getCapturedDate(workflowId)` | Returns the most recent `capturedAt` date across all fixtures. |
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## MCP Server
|
|
527
|
+
|
|
528
|
+
`src/services/mcp.service.ts` implements an MCP server using the
|
|
529
|
+
`@modelcontextprotocol/sdk` package with a **stdio transport**.
|
|
530
|
+
|
|
531
|
+
```
|
|
532
|
+
Claude Desktop / Cursor / other MCP client
|
|
533
|
+
│ stdio
|
|
534
|
+
▼
|
|
535
|
+
MCPService (n8m-agent)
|
|
536
|
+
├── create_workflow(goal) → runAgenticWorkflow(goal)
|
|
537
|
+
└── test_workflow(workflowJson, goal) → deploys + validates ephemerally
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
The server runs as a long-lived process started by `n8m mcp`. It does not use
|
|
541
|
+
the HITL interrupt mechanism (no interactive prompts), so workflow generation
|
|
542
|
+
runs fully autonomously.
|
|
543
|
+
|
|
544
|
+
To integrate with Claude Desktop, add to `claude_desktop_config.json`:
|
|
545
|
+
|
|
546
|
+
```json
|
|
547
|
+
{
|
|
548
|
+
"mcpServers": {
|
|
549
|
+
"n8m": { "command": "npx", "args": ["n8m", "mcp"] }
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## Environment Variables
|
|
557
|
+
|
|
558
|
+
| Variable | Description | Priority |
|
|
559
|
+
| ---------------- | ------------------------------------------ | ----------------- |
|
|
560
|
+
| `AI_API_KEY` | API key for the AI provider | Env > Config file |
|
|
561
|
+
| `AI_PROVIDER` | `openai`, `anthropic`, or `gemini` | Env > Config file |
|
|
562
|
+
| `AI_MODEL` | Override the default model | Env > Config file |
|
|
563
|
+
| `AI_BASE_URL` | Base URL for any OpenAI-compatible API | Env > Config file |
|
|
564
|
+
| `N8N_API_URL` | URL of your n8n instance | Env > Config file |
|
|
565
|
+
| `N8N_API_KEY` | n8n API key | Env > Config file |
|
|
566
|
+
| `GEMINI_API_KEY` | Alias for `AI_API_KEY` when using Gemini | Env only |
|
|
567
|
+
| `NODE_ENV` | Set to `test` to prevent live AI/n8n calls | Env only |
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## Local Development
|
|
572
|
+
|
|
573
|
+
```bash
|
|
574
|
+
git clone https://github.com/lcanady/n8m.git
|
|
575
|
+
cd n8m
|
|
576
|
+
npm install
|
|
577
|
+
|
|
578
|
+
# Watch mode (recompiles on change)
|
|
579
|
+
npm run dev
|
|
580
|
+
|
|
581
|
+
# Run the CLI directly from source
|
|
582
|
+
./bin/run.js help
|
|
583
|
+
./bin/run.js create "Send a Slack message every morning"
|
|
584
|
+
|
|
585
|
+
# Run tests
|
|
586
|
+
npm test
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Build
|
|
590
|
+
|
|
591
|
+
```bash
|
|
592
|
+
npm run build # Compiles TypeScript to dist/
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
The project uses `tsconfig.json` with `"module": "NodeNext"` and
|
|
596
|
+
`"moduleResolution": "NodeNext"`. All imports in source files must include the
|
|
597
|
+
`.js` extension, even for TypeScript files (this is resolved to `.ts` by the
|
|
598
|
+
TypeScript compiler at build time but must be explicit).
|