@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AIService } from "../../services/ai.service.js";
|
|
2
2
|
import { NodeDefinitionsService } from "../../services/node-definitions.service.js";
|
|
3
|
+
import { jsonrepair } from "jsonrepair";
|
|
3
4
|
export const engineerNode = async (state) => {
|
|
4
5
|
const aiService = AIService.getInstance();
|
|
5
6
|
// RAG: Load and Search Node Definitions
|
|
@@ -9,8 +10,9 @@ export const engineerNode = async (state) => {
|
|
|
9
10
|
const queryText = (state.userGoal + (state.spec ? ` ${state.spec.suggestedName} ${state.spec.description}` : "")).replace(/\n/g, " ");
|
|
10
11
|
// Search for relevant nodes (limit 8 to save context)
|
|
11
12
|
const relevantDefs = nodeService.search(queryText, 8);
|
|
12
|
-
const
|
|
13
|
-
|
|
13
|
+
const staticRef = nodeService.getStaticReference();
|
|
14
|
+
const ragContext = (relevantDefs.length > 0 || staticRef)
|
|
15
|
+
? `\n\n[N8N NODE REFERENCE GUIDE]\n${staticRef}\n\n[AVAILABLE NODE SCHEMAS - USE THESE EXACT PARAMETERS]\n${nodeService.formatForLLM(relevantDefs)}`
|
|
14
16
|
: "";
|
|
15
17
|
if (relevantDefs.length > 0) {
|
|
16
18
|
console.log(`[Engineer] RAG: Found ${relevantDefs.length} relevant node schemas.`);
|
|
@@ -22,7 +24,8 @@ export const engineerNode = async (state) => {
|
|
|
22
24
|
// We pass the entire list of errors as context
|
|
23
25
|
const errorContext = state.validationErrors.join('\n');
|
|
24
26
|
// Use the robust fix logic from AIService
|
|
25
|
-
const fixedWorkflow = await aiService.generateWorkflowFix(state.workflowJson, errorContext,
|
|
27
|
+
const fixedWorkflow = await aiService.generateWorkflowFix(state.workflowJson, errorContext, state.spec?.aiModel, // Pass model if available
|
|
28
|
+
false, state.availableNodeTypes || []);
|
|
26
29
|
return {
|
|
27
30
|
workflowJson: fixedWorkflow,
|
|
28
31
|
// validationErrors will be overwritten by next QA run
|
|
@@ -49,6 +52,7 @@ export const engineerNode = async (state) => {
|
|
|
49
52
|
Specification:
|
|
50
53
|
${JSON.stringify(state.spec, null, 2)}
|
|
51
54
|
${ragContext}
|
|
55
|
+
${state.userFeedback ? `\n\nUSER FEEDBACK / REFINEMENTS:\n${state.userFeedback}\n(Incorporate this feedback into the generation process)` : ""}
|
|
52
56
|
|
|
53
57
|
IMPORTANT:
|
|
54
58
|
1. Desciptive Naming: Name nodes descriptively (e.g. "Fetch Bitcoin Price" instead of "HTTP Request").
|
|
@@ -74,23 +78,27 @@ export const engineerNode = async (state) => {
|
|
|
74
78
|
Output ONLY valid JSON. No commentary. No markdown.
|
|
75
79
|
`;
|
|
76
80
|
// Using AIService just for the LLM call to keep auth logic dry
|
|
77
|
-
const response = await aiService.generateContent(prompt
|
|
81
|
+
const response = await aiService.generateContent(prompt, {
|
|
82
|
+
provider: state.spec.aiProvider,
|
|
83
|
+
model: state.spec.aiModel
|
|
84
|
+
});
|
|
78
85
|
let cleanJson = response || "{}";
|
|
79
86
|
cleanJson = cleanJson.replace(/```json\n?|\n?```/g, "").trim();
|
|
80
87
|
let result;
|
|
81
88
|
try {
|
|
82
|
-
result = JSON.parse(cleanJson);
|
|
89
|
+
result = JSON.parse(jsonrepair(cleanJson));
|
|
83
90
|
}
|
|
84
|
-
catch (
|
|
85
|
-
console.error("Failed to parse workflow JSON from spec",
|
|
91
|
+
catch (e2) {
|
|
92
|
+
console.error("Failed to parse workflow JSON from spec", e2);
|
|
86
93
|
throw new Error("AI generated invalid JSON for workflow from spec");
|
|
87
94
|
}
|
|
88
95
|
if (result.workflows && Array.isArray(result.workflows)) {
|
|
89
|
-
result.workflows = result.workflows.map((wf) => fixHallucinatedNodes(wf));
|
|
96
|
+
result.workflows = result.workflows.map((wf) => aiService.fixHallucinatedNodes(wf));
|
|
90
97
|
}
|
|
91
98
|
return {
|
|
92
|
-
workflowJson
|
|
93
|
-
//
|
|
99
|
+
// Only push to candidates — the Supervisor sets workflowJson after fan-in.
|
|
100
|
+
// Writing workflowJson here would cause a LastValue conflict when two
|
|
101
|
+
// Engineers run in parallel via Send().
|
|
94
102
|
candidates: [result],
|
|
95
103
|
};
|
|
96
104
|
}
|
|
@@ -99,84 +107,4 @@ export const engineerNode = async (state) => {
|
|
|
99
107
|
throw error;
|
|
100
108
|
}
|
|
101
109
|
};
|
|
102
|
-
|
|
103
|
-
* Auto-correct common n8n node type hallucinations
|
|
104
|
-
*/
|
|
105
|
-
function fixHallucinatedNodes(workflow) {
|
|
106
|
-
if (!workflow.nodes || !Array.isArray(workflow.nodes))
|
|
107
|
-
return workflow;
|
|
108
|
-
const corrections = {
|
|
109
|
-
"n8n-nodes-base.rssFeed": "n8n-nodes-base.rssFeedRead",
|
|
110
|
-
"rssFeed": "n8n-nodes-base.rssFeedRead",
|
|
111
|
-
"n8n-nodes-base.gpt": "n8n-nodes-base.openAi",
|
|
112
|
-
"n8n-nodes-base.openai": "n8n-nodes-base.openAi",
|
|
113
|
-
"openai": "n8n-nodes-base.openAi",
|
|
114
|
-
"n8n-nodes-base.openAiChat": "n8n-nodes-base.openAi",
|
|
115
|
-
"n8n-nodes-base.openAIChat": "n8n-nodes-base.openAi",
|
|
116
|
-
"n8n-nodes-base.openaiChat": "n8n-nodes-base.openAi",
|
|
117
|
-
"n8n-nodes-base.gemini": "n8n-nodes-base.googleGemini",
|
|
118
|
-
"n8n-nodes-base.cheerioHtml": "n8n-nodes-base.htmlExtract",
|
|
119
|
-
"cheerioHtml": "n8n-nodes-base.htmlExtract",
|
|
120
|
-
"n8n-nodes-base.schedule": "n8n-nodes-base.scheduleTrigger",
|
|
121
|
-
"schedule": "n8n-nodes-base.scheduleTrigger",
|
|
122
|
-
"n8n-nodes-base.cron": "n8n-nodes-base.scheduleTrigger",
|
|
123
|
-
"n8n-nodes-base.googleCustomSearch": "n8n-nodes-base.googleGemini",
|
|
124
|
-
"googleCustomSearch": "n8n-nodes-base.googleGemini"
|
|
125
|
-
};
|
|
126
|
-
workflow.nodes = workflow.nodes.map((node) => {
|
|
127
|
-
if (node.type && corrections[node.type]) {
|
|
128
|
-
node.type = corrections[node.type];
|
|
129
|
-
}
|
|
130
|
-
// Ensure base prefix if missing
|
|
131
|
-
if (node.type && !node.type.startsWith('n8n-nodes-base.') && !node.type.includes('.')) {
|
|
132
|
-
node.type = `n8n-nodes-base.${node.type}`;
|
|
133
|
-
}
|
|
134
|
-
return node;
|
|
135
|
-
});
|
|
136
|
-
return fixN8nConnections(workflow);
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Force-fix connection structure to prevent "object is not iterable" errors
|
|
140
|
-
*/
|
|
141
|
-
function fixN8nConnections(workflow) {
|
|
142
|
-
if (!workflow.connections || typeof workflow.connections !== 'object')
|
|
143
|
-
return workflow;
|
|
144
|
-
const fixedConnections = {};
|
|
145
|
-
for (const [sourceNode, targets] of Object.entries(workflow.connections)) {
|
|
146
|
-
if (!targets || typeof targets !== 'object')
|
|
147
|
-
continue;
|
|
148
|
-
const targetObj = targets;
|
|
149
|
-
// 2. Ensure "main" exists and is an array
|
|
150
|
-
if (targetObj.main) {
|
|
151
|
-
let mainArr = targetObj.main;
|
|
152
|
-
if (!Array.isArray(mainArr))
|
|
153
|
-
mainArr = [[{ node: String(mainArr), type: 'main', index: 0 }]];
|
|
154
|
-
const fixedMain = mainArr.map((segment) => {
|
|
155
|
-
if (!segment)
|
|
156
|
-
return [];
|
|
157
|
-
if (!Array.isArray(segment)) {
|
|
158
|
-
// Wrap in array if it's a single object
|
|
159
|
-
return [segment];
|
|
160
|
-
}
|
|
161
|
-
return segment.map((conn) => {
|
|
162
|
-
if (!conn)
|
|
163
|
-
return { node: 'Unknown', type: 'main', index: 0 };
|
|
164
|
-
if (typeof conn === 'string')
|
|
165
|
-
return { node: conn, type: 'main', index: 0 };
|
|
166
|
-
return {
|
|
167
|
-
node: String(conn.node || 'Unknown'),
|
|
168
|
-
type: conn.type || 'main',
|
|
169
|
-
index: conn.index || 0
|
|
170
|
-
};
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
fixedConnections[sourceNode] = { main: fixedMain };
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
// If it's just raw data like { "Source": { "node": "Target" } }, wrap it
|
|
177
|
-
fixedConnections[sourceNode] = targetObj;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
workflow.connections = fixedConnections;
|
|
181
|
-
return workflow;
|
|
182
|
-
}
|
|
110
|
+
// Local helpers removed, using AIService methods instead.
|
package/dist/agentic/nodes/qa.js
CHANGED
|
@@ -10,8 +10,9 @@ export const qaNode = async (state) => {
|
|
|
10
10
|
}
|
|
11
11
|
// 1. Load Credentials
|
|
12
12
|
const config = await ConfigManager.load();
|
|
13
|
-
|
|
14
|
-
const
|
|
13
|
+
// Env vars take priority over stored config so a fresh key in .env is always used
|
|
14
|
+
const n8nUrl = process.env.N8N_API_URL || config.n8nUrl;
|
|
15
|
+
const n8nKey = process.env.N8N_API_KEY || config.n8nKey;
|
|
15
16
|
if (!n8nUrl || !n8nKey) {
|
|
16
17
|
throw new Error('Credentials missing. Configure environment via \'n8m config\'.');
|
|
17
18
|
}
|
|
@@ -28,10 +29,21 @@ export const qaNode = async (state) => {
|
|
|
28
29
|
targetWorkflow = workflowJson.workflows[0];
|
|
29
30
|
}
|
|
30
31
|
const workflowName = targetWorkflow.name || 'Agentic_Test_Workflow';
|
|
32
|
+
// Drop timezone — sanitizeSettings in N8nClient strips it unconditionally
|
|
33
|
+
const rawSettings = { ...(targetWorkflow.settings || {}) };
|
|
34
|
+
delete rawSettings.timezone;
|
|
35
|
+
// Strip credentials from all nodes — n8n 2.x refuses to activate ("publish")
|
|
36
|
+
// a workflow that references credentials that don't exist on the instance.
|
|
37
|
+
// Structural validation can still run; only live execution of credentialed
|
|
38
|
+
// nodes will be skipped/fail, which is expected for an ephemeral test.
|
|
39
|
+
const strippedNodes = targetWorkflow.nodes.map((node) => {
|
|
40
|
+
const { credentials: _creds, ...rest } = node;
|
|
41
|
+
return rest;
|
|
42
|
+
});
|
|
31
43
|
const rootPayload = {
|
|
32
|
-
nodes:
|
|
44
|
+
nodes: strippedNodes,
|
|
33
45
|
connections: targetWorkflow.connections,
|
|
34
|
-
settings:
|
|
46
|
+
settings: rawSettings,
|
|
35
47
|
staticData: targetWorkflow.staticData || {},
|
|
36
48
|
name: `[n8m:test] ${workflowName}`,
|
|
37
49
|
};
|
|
@@ -49,70 +61,69 @@ export const qaNode = async (state) => {
|
|
|
49
61
|
console.log(theme.agent(`Deploying ephemeral root: ${rootPayload.name}...`));
|
|
50
62
|
const result = await client.createWorkflow(rootPayload.name, rootPayload);
|
|
51
63
|
createdWorkflowId = result.id;
|
|
52
|
-
// 4.
|
|
64
|
+
// 4. Determine Test Scenarios
|
|
65
|
+
let scenarios = state.testScenarios;
|
|
66
|
+
if (!scenarios || scenarios.length === 0) {
|
|
67
|
+
// Fallback to generating a single mock payload for efficiency if no scenarios provided
|
|
68
|
+
const nodeNames = targetWorkflow.nodes.map((n) => n.name).join(', ');
|
|
69
|
+
const context = `Workflow Name: "${targetWorkflow.name}"
|
|
70
|
+
Nodes: ${nodeNames}
|
|
71
|
+
Goal: "${state.userGoal}"
|
|
72
|
+
Generate a SINGLE JSON object payload that effectively tests this workflow.`;
|
|
73
|
+
const mockPayload = await aiService.generateMockData(context);
|
|
74
|
+
scenarios = [{ name: "Default Test", payload: mockPayload }];
|
|
75
|
+
}
|
|
53
76
|
const webhookNode = rootPayload.nodes.find((n) => n.type === 'n8n-nodes-base.webhook');
|
|
54
|
-
let triggerSuccess = false;
|
|
55
77
|
if (webhookNode) {
|
|
56
78
|
const path = webhookNode.parameters?.path;
|
|
57
79
|
if (path) {
|
|
58
80
|
// Activate for webhook testing
|
|
59
81
|
await client.activateWorkflow(createdWorkflowId);
|
|
60
|
-
const nodeNames = targetWorkflow.nodes.map((n) => n.name).join(', ');
|
|
61
|
-
const context = `Workflow Name: "${targetWorkflow.name}"
|
|
62
|
-
Nodes: ${nodeNames}
|
|
63
|
-
Goal: "${state.userGoal}"
|
|
64
|
-
Generate a SINGLE JSON object payload that effectively tests this workflow.`;
|
|
65
|
-
const mockPayload = await aiService.generateMockData(context);
|
|
66
82
|
const baseUrl = new URL(n8nUrl).origin;
|
|
67
83
|
const webhookUrl = `${baseUrl}/webhook/${path}`;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
for (const scenario of scenarios) {
|
|
85
|
+
console.log(theme.info(`🧪 Running Scenario: ${theme.value(scenario.name)}...`));
|
|
86
|
+
const response = await fetch(webhookUrl, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify(scenario.payload)
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
validationErrors.push(`Scenario "${scenario.name}" failed to trigger: ${response.status}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// 5. Verify Execution for this scenario
|
|
96
|
+
const executionStartTime = Date.now();
|
|
97
|
+
let executionFound = false;
|
|
98
|
+
const maxPoll = 15;
|
|
99
|
+
for (let i = 0; i < maxPoll; i++) {
|
|
100
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
101
|
+
const executions = await client.getWorkflowExecutions(createdWorkflowId);
|
|
102
|
+
const recentExec = executions.find((e) => new Date(e.startedAt).getTime() > (executionStartTime - 5000));
|
|
103
|
+
if (recentExec) {
|
|
104
|
+
executionFound = true;
|
|
105
|
+
const fullExec = await client.getExecution(recentExec.id);
|
|
106
|
+
if (fullExec.status === 'success') {
|
|
107
|
+
console.log(theme.success(` ✔ Passed`));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const errorMsg = fullExec.data?.resultData?.error?.message || "Unknown flow failure";
|
|
111
|
+
validationErrors.push(`Scenario "${scenario.name}" Failed: ${errorMsg}`);
|
|
112
|
+
console.log(theme.error(` ✘ Failed: ${errorMsg}`));
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!executionFound) {
|
|
118
|
+
validationErrors.push(`Scenario "${scenario.name}": No execution detected after trigger.`);
|
|
119
|
+
console.log(theme.warn(` ⚠ No execution detected.`));
|
|
120
|
+
}
|
|
78
121
|
}
|
|
79
122
|
}
|
|
80
123
|
}
|
|
81
124
|
else {
|
|
82
125
|
// Just execute if no webhook (manual trigger)
|
|
83
126
|
await client.executeWorkflow(createdWorkflowId);
|
|
84
|
-
triggerSuccess = true;
|
|
85
|
-
}
|
|
86
|
-
// 5. Verify Execution
|
|
87
|
-
// Wait for execution to appear
|
|
88
|
-
if (triggerSuccess) {
|
|
89
|
-
const executionStartTime = Date.now();
|
|
90
|
-
let executionFound = false;
|
|
91
|
-
const maxPoll = 20; // shorter poll for agent
|
|
92
|
-
for (let i = 0; i < maxPoll; i++) {
|
|
93
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
94
|
-
const executions = await client.getWorkflowExecutions(createdWorkflowId);
|
|
95
|
-
const recentExec = executions.find((e) => new Date(e.startedAt).getTime() > (executionStartTime - 5000));
|
|
96
|
-
if (recentExec) {
|
|
97
|
-
executionFound = true;
|
|
98
|
-
const fullExec = await client.getExecution(recentExec.id);
|
|
99
|
-
if (fullExec.status === 'success') {
|
|
100
|
-
return {
|
|
101
|
-
validationStatus: 'passed',
|
|
102
|
-
validationErrors: [],
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
const errorMsg = fullExec.data?.resultData?.error?.message || "Unknown flow failure";
|
|
107
|
-
validationErrors.push(`Execution Failed: ${errorMsg}`);
|
|
108
|
-
console.log(theme.error(`Execution Failed: ${errorMsg}`));
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (!executionFound) {
|
|
114
|
-
validationErrors.push("No execution detected after trigger.");
|
|
115
|
-
}
|
|
116
127
|
}
|
|
117
128
|
// 6. Dynamic Tool Execution (Sandbox)
|
|
118
129
|
// If the Agent has defined a custom validation script, run it now.
|
|
@@ -145,7 +156,7 @@ export const qaNode = async (state) => {
|
|
|
145
156
|
}
|
|
146
157
|
}
|
|
147
158
|
return {
|
|
148
|
-
validationStatus: 'failed',
|
|
159
|
+
validationStatus: validationErrors.length === 0 ? 'passed' : 'failed',
|
|
149
160
|
validationErrors,
|
|
150
161
|
};
|
|
151
162
|
};
|
|
@@ -66,6 +66,8 @@ export const reviewerNode = async (state) => {
|
|
|
66
66
|
// Triggers usually have no input.
|
|
67
67
|
// We use a broader check: any node with "trigger" or "webhook" in the name, plus generic start types.
|
|
68
68
|
const isTrigger = (type) => {
|
|
69
|
+
if (!type)
|
|
70
|
+
return false;
|
|
69
71
|
const lower = type.toLowerCase();
|
|
70
72
|
return lower.includes('trigger') ||
|
|
71
73
|
lower.includes('webhook') ||
|
|
@@ -79,9 +81,9 @@ export const reviewerNode = async (state) => {
|
|
|
79
81
|
// Sticky notes and Merge nodes can be tricky, but generally Merge needs input.
|
|
80
82
|
if (!node.type.includes('StickyNote')) {
|
|
81
83
|
// Double check for "On Execution" (custom trigger name sometimes used)
|
|
82
|
-
if (!node.name.toLowerCase().includes('trigger') && !node.name.toLowerCase().includes('webhook')) {
|
|
83
|
-
console.log(theme.warn(`[Reviewer] Validated disconnection: Node "${node.name}" has no incoming connections.`));
|
|
84
|
-
validationErrors.push(`Node "${node.name}" (${node.type}) is disconnected (orphaned). Connect it or remove it.`);
|
|
84
|
+
if (!node.name || (!node.name.toLowerCase().includes('trigger') && !node.name.toLowerCase().includes('webhook'))) {
|
|
85
|
+
console.log(theme.warn(`[Reviewer] Validated disconnection: Node "${node.name || 'Unnamed'}" has no incoming connections.`));
|
|
86
|
+
validationErrors.push(`Node "${node.name || 'Unnamed'}" (${node.type || 'unknown type'}) is disconnected (orphaned). Connect it or remove it.`);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
}
|
package/dist/agentic/state.d.ts
CHANGED
|
@@ -49,4 +49,14 @@ export declare const TeamState: import("@langchain/langgraph").AnnotationRoot<{
|
|
|
49
49
|
candidates: import("@langchain/langgraph").BinaryOperatorAggregate<any[], any[]>;
|
|
50
50
|
customTools: import("@langchain/langgraph").BinaryOperatorAggregate<Record<string, string>, Record<string, string>>;
|
|
51
51
|
collaborationLog: import("@langchain/langgraph").BinaryOperatorAggregate<string[], string[]>;
|
|
52
|
+
userFeedback: {
|
|
53
|
+
(): import("@langchain/langgraph").LastValue<string>;
|
|
54
|
+
(annotation: import("@langchain/langgraph").SingleReducer<string, string>): import("@langchain/langgraph").BinaryOperatorAggregate<string, string>;
|
|
55
|
+
Root: <S extends import("@langchain/langgraph").StateDefinition>(sd: S) => import("@langchain/langgraph").AnnotationRoot<S>;
|
|
56
|
+
};
|
|
57
|
+
testScenarios: {
|
|
58
|
+
(): import("@langchain/langgraph").LastValue<any[]>;
|
|
59
|
+
(annotation: import("@langchain/langgraph").SingleReducer<any[], any[]>): import("@langchain/langgraph").BinaryOperatorAggregate<any[], any[]>;
|
|
60
|
+
Root: <S extends import("@langchain/langgraph").StateDefinition>(sd: S) => import("@langchain/langgraph").AnnotationRoot<S>;
|
|
61
|
+
};
|
|
52
62
|
}>;
|
package/dist/agentic/state.js
CHANGED
package/dist/commands/create.js
CHANGED
|
@@ -3,11 +3,11 @@ import { theme } from '../utils/theme.js';
|
|
|
3
3
|
import { runAgenticWorkflowStream } from '../agentic/graph.js';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import * as fs from 'node:fs/promises';
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
6
|
import inquirer from 'inquirer';
|
|
8
7
|
import { randomUUID } from 'node:crypto';
|
|
9
8
|
import { graph, resumeAgenticWorkflow } from '../agentic/graph.js';
|
|
10
9
|
import { promptMultiline } from '../utils/multilinePrompt.js';
|
|
10
|
+
import { DocService } from '../services/doc.service.js';
|
|
11
11
|
export default class Create extends Command {
|
|
12
12
|
static args = {
|
|
13
13
|
description: Args.string({
|
|
@@ -15,7 +15,7 @@ export default class Create extends Command {
|
|
|
15
15
|
required: false,
|
|
16
16
|
}),
|
|
17
17
|
};
|
|
18
|
-
static description = 'Generate n8n workflows from natural language using
|
|
18
|
+
static description = 'Generate n8n workflows from natural language using an AI Agent';
|
|
19
19
|
static examples = [
|
|
20
20
|
'<%= config.bin %> <%= command.id %> "Send a telegram alert when I receive an email"',
|
|
21
21
|
'echo "Slack to Discord sync" | <%= config.bin %> <%= command.id %>',
|
|
@@ -85,9 +85,17 @@ export default class Create extends Command {
|
|
|
85
85
|
const stateUpdate = event[nodeName];
|
|
86
86
|
if (nodeName === 'architect') {
|
|
87
87
|
this.log(theme.agent(`🏗️ Architect: Blueprint designed.`));
|
|
88
|
-
if (stateUpdate.
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
if (stateUpdate.strategies && stateUpdate.strategies.length > 0) {
|
|
89
|
+
lastSpec = stateUpdate.strategies[0]; // Default to primary
|
|
90
|
+
this.log(theme.header('\nPROPOSED STRATEGIES:'));
|
|
91
|
+
stateUpdate.strategies.forEach((s, i) => {
|
|
92
|
+
this.log(`${i === 0 ? theme.success(' [Primary]') : theme.info(' [Alternative]')} ${theme.value(s.suggestedName)}`);
|
|
93
|
+
this.log(` Description: ${s.description}`);
|
|
94
|
+
if (s.nodes && s.nodes.length > 0) {
|
|
95
|
+
this.log(` Proposed Nodes: ${s.nodes.map((n) => n.type.split('.').pop()).join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
this.log('');
|
|
98
|
+
});
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
101
|
else if (nodeName === 'engineer') {
|
|
@@ -111,36 +119,98 @@ export default class Create extends Command {
|
|
|
111
119
|
}
|
|
112
120
|
}
|
|
113
121
|
// Check for interrupt/pause
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
122
|
+
let snapshot = await graph.getState({ configurable: { thread_id: threadId } });
|
|
123
|
+
while (snapshot.next.length > 0) {
|
|
124
|
+
const nextNode = snapshot.next[0];
|
|
125
|
+
this.log(theme.warn(`\n⏸️ Workflow Paused at step: ${nextNode}`));
|
|
126
|
+
if (nextNode === 'engineer') {
|
|
127
|
+
const { action } = await inquirer.prompt([{
|
|
128
|
+
type: 'list',
|
|
129
|
+
name: 'action',
|
|
130
|
+
message: 'How would you like to proceed with the Blueprint?',
|
|
131
|
+
choices: [
|
|
132
|
+
{ name: 'Approve and Generate Workflow', value: 'approve' },
|
|
133
|
+
{ name: 'Provide Feedback / Refine Strategy', value: 'feedback' },
|
|
134
|
+
{ name: 'Exit and Resume Later', value: 'exit' }
|
|
135
|
+
]
|
|
136
|
+
}]);
|
|
137
|
+
if (action === 'approve') {
|
|
138
|
+
this.log(theme.agent("Approve! Proceeding to engineering..."));
|
|
139
|
+
await graph.updateState({ configurable: { thread_id: threadId } }, { userFeedback: undefined }, nextNode);
|
|
140
|
+
const stream = await graph.stream(null, { configurable: { thread_id: threadId } });
|
|
141
|
+
for await (const event of stream) {
|
|
142
|
+
const nodeName = Object.keys(event)[0];
|
|
143
|
+
const stateUpdate = event[nodeName];
|
|
144
|
+
if (nodeName === 'engineer') {
|
|
145
|
+
this.log(theme.agent(`⚙️ Engineer: Workflow code generated/updated.`));
|
|
146
|
+
if (stateUpdate.workflowJson)
|
|
147
|
+
lastWorkflowJson = stateUpdate.workflowJson;
|
|
148
|
+
}
|
|
149
|
+
else if (nodeName === 'qa') {
|
|
150
|
+
const status = stateUpdate.validationStatus;
|
|
151
|
+
if (status === 'passed')
|
|
152
|
+
this.log(theme.success(`🧪 QA: Validation Passed!`));
|
|
153
|
+
else
|
|
154
|
+
this.log(theme.fail(`🧪 QA: Validation Failed.`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (action === 'feedback') {
|
|
159
|
+
const { feedback } = await inquirer.prompt([{
|
|
160
|
+
type: 'input',
|
|
161
|
+
name: 'feedback',
|
|
162
|
+
message: 'Enter your feedback/instructions:',
|
|
163
|
+
}]);
|
|
164
|
+
this.log(theme.agent("Updating strategy with your feedback..."));
|
|
165
|
+
// In a real implementation, we'd loop back to Architect or update the goal.
|
|
166
|
+
// For now, let's update userFeedback and resume.
|
|
167
|
+
// To actually RE-ARCHITECT, we might need to jump back.
|
|
168
|
+
// LangGraph can handle this by updating state and using a conditional edge.
|
|
169
|
+
await graph.updateState({ configurable: { thread_id: threadId } }, { userFeedback: feedback }, nextNode);
|
|
170
|
+
// For now, just resume and let Engineer see the feedback.
|
|
171
|
+
const stream = await graph.stream(null, { configurable: { thread_id: threadId } });
|
|
172
|
+
for await (const event of stream) {
|
|
173
|
+
const nodeName = Object.keys(event)[0];
|
|
174
|
+
const stateUpdate = event[nodeName];
|
|
175
|
+
if (nodeName === 'engineer') {
|
|
176
|
+
this.log(theme.agent(`⚙️ Engineer: Workflow code generated/updated (Feedback incorporated).`));
|
|
177
|
+
if (stateUpdate.workflowJson)
|
|
178
|
+
lastWorkflowJson = stateUpdate.workflowJson;
|
|
179
|
+
}
|
|
180
|
+
else if (nodeName === 'qa') {
|
|
181
|
+
if (stateUpdate.validationStatus === 'passed')
|
|
182
|
+
this.log(theme.success(`🧪 QA: Validation Passed!`));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
135
185
|
}
|
|
136
186
|
else {
|
|
137
|
-
this.log(theme.
|
|
187
|
+
this.log(theme.info(`Session persisted. Resume later with: n8m resume ${threadId}`));
|
|
188
|
+
return;
|
|
138
189
|
}
|
|
139
190
|
}
|
|
140
191
|
else {
|
|
141
|
-
|
|
142
|
-
|
|
192
|
+
// Handle other interrupts (like QA)
|
|
193
|
+
const { resume } = await inquirer.prompt([{
|
|
194
|
+
type: 'confirm',
|
|
195
|
+
name: 'resume',
|
|
196
|
+
message: `Review completed for ${nextNode}. Resume workflow execution?`,
|
|
197
|
+
default: true
|
|
198
|
+
}]);
|
|
199
|
+
if (resume) {
|
|
200
|
+
this.log(theme.agent("Resuming..."));
|
|
201
|
+
const result = await resumeAgenticWorkflow(threadId);
|
|
202
|
+
if (result.validationStatus === 'passed') {
|
|
203
|
+
this.log(theme.success(`🧪 QA (Resumed): Validation Passed!`));
|
|
204
|
+
if (result.workflowJson)
|
|
205
|
+
lastWorkflowJson = result.workflowJson;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
this.log(theme.info(`Session persisted. Resume later with: n8m resume ${threadId}`));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
143
212
|
}
|
|
213
|
+
snapshot = await graph.getState({ configurable: { thread_id: threadId } });
|
|
144
214
|
}
|
|
145
215
|
}
|
|
146
216
|
catch (error) {
|
|
@@ -153,28 +223,25 @@ export default class Create extends Command {
|
|
|
153
223
|
// Normalize to array
|
|
154
224
|
const workflows = lastWorkflowJson.workflows || [lastWorkflowJson];
|
|
155
225
|
const savedResources = [];
|
|
226
|
+
const docService = DocService.getInstance();
|
|
156
227
|
for (const workflow of workflows) {
|
|
157
228
|
const workflowName = workflow.name || (lastSpec && lastSpec.suggestedName) || 'generated-workflow';
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// If specific file given but we have multiple, suffix it
|
|
166
|
-
targetFile = targetFile.replace('.json', `-${sanitizedName}.json`);
|
|
167
|
-
}
|
|
168
|
-
else if (!targetFile) {
|
|
169
|
-
const targetDir = path.join(process.cwd(), 'workflows');
|
|
170
|
-
if (!existsSync(targetDir)) {
|
|
171
|
-
await fs.mkdir(targetDir, { recursive: true });
|
|
172
|
-
}
|
|
173
|
-
targetFile = path.join(targetDir, `${sanitizedName}.json`);
|
|
174
|
-
}
|
|
229
|
+
const projectTitle = await docService.generateProjectTitle(workflow);
|
|
230
|
+
workflow.name = projectTitle; // Standardize name
|
|
231
|
+
const slug = docService.generateSlug(projectTitle);
|
|
232
|
+
const workflowsDir = path.join(process.cwd(), 'workflows');
|
|
233
|
+
const targetDir = path.join(workflowsDir, slug);
|
|
234
|
+
const targetFile = path.join(targetDir, 'workflow.json');
|
|
235
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
175
236
|
await fs.writeFile(targetFile, JSON.stringify(workflow, null, 2));
|
|
176
|
-
|
|
177
|
-
|
|
237
|
+
this.log(theme.success(`\nWorkflow organized at: ${targetDir}`));
|
|
238
|
+
// Auto-Generate Documentation
|
|
239
|
+
this.log(theme.agent("Generating initial documentation..."));
|
|
240
|
+
const mermaid = docService.generateMermaid(workflow);
|
|
241
|
+
const readmeContent = await docService.generateReadme(workflow);
|
|
242
|
+
const fullDoc = `# ${projectTitle}\n\n## Visual Flow\n\n\`\`\`mermaid\n${mermaid}\`\`\`\n\n${readmeContent}`;
|
|
243
|
+
await fs.writeFile(path.join(targetDir, 'README.md'), fullDoc);
|
|
244
|
+
savedResources.push({ path: targetFile, name: projectTitle, original: workflow });
|
|
178
245
|
}
|
|
179
246
|
this.log(theme.done('Agentic Workflow Complete.'));
|
|
180
247
|
process.exit(0);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Doc extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
workflow: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static flags: {
|
|
8
|
+
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|