@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.
@@ -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 ragContext = relevantDefs.length > 0
13
- ? `\n\n[AVAILABLE NODE SCHEMAS - USE THESE EXACT PARAMETERS]\n${nodeService.formatForLLM(relevantDefs)}`
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, undefined, false, state.availableNodeTypes || []);
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 (e) {
85
- console.error("Failed to parse workflow JSON from spec", e);
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: result,
93
- // For parallel execution, push to candidates
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.
@@ -10,8 +10,9 @@ export const qaNode = async (state) => {
10
10
  }
11
11
  // 1. Load Credentials
12
12
  const config = await ConfigManager.load();
13
- const n8nUrl = config.n8nUrl || process.env.N8N_API_URL;
14
- const n8nKey = config.n8nKey || process.env.N8N_API_KEY;
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: targetWorkflow.nodes,
44
+ nodes: strippedNodes,
33
45
  connections: targetWorkflow.connections,
34
- settings: targetWorkflow.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. Generate Mock Data
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 response = await fetch(webhookUrl, {
69
- method: 'POST',
70
- headers: { 'Content-Type': 'application/json' },
71
- body: JSON.stringify(mockPayload)
72
- });
73
- if (response.ok) {
74
- triggerSuccess = true;
75
- }
76
- else {
77
- throw new Error(`Webhook trigger failed with status ${response.status}`);
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
  }
@@ -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
  }>;
@@ -28,4 +28,6 @@ export const TeamState = Annotation.Root({
28
28
  reducer: (x, y) => x.concat(y),
29
29
  default: () => [],
30
30
  }),
31
+ userFeedback: (Annotation),
32
+ testScenarios: (Annotation),
31
33
  });
@@ -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 Gemini AI Agent';
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.spec?.suggestedName) {
89
- this.log(` Goal: ${theme.value(stateUpdate.spec.suggestedName)}`);
90
- lastSpec = stateUpdate.spec;
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
- const snapshot = await graph.getState({ configurable: { thread_id: threadId } });
115
- if (snapshot.next.length > 0) {
116
- this.log(theme.warn(`\n⏸️ Workflow Paused at step: ${snapshot.next.join(', ')}`));
117
- const { resume } = await inquirer.prompt([{
118
- type: 'confirm',
119
- name: 'resume',
120
- message: 'Review completed. Resume workflow execution?',
121
- default: true
122
- }]);
123
- if (resume) {
124
- this.log(theme.agent("Resuming..."));
125
- // Resume recursively/iteratively?
126
- // For now, simple resume call. ideally we'd stream again.
127
- // But wait, resumeAgenticWorkflow returns the FINAL result, not a stream.
128
- // We should probably loop if we want to stream again, but let's just create a simple resume handling here.
129
- // Or we can just call resumeAgenticWorkflow and print the final result.
130
- const result = await resumeAgenticWorkflow(threadId);
131
- if (result.validationStatus === 'passed') {
132
- this.log(theme.success(`🧪 QA (Resumed): Validation Passed!`));
133
- if (result.workflowJson)
134
- lastWorkflowJson = result.workflowJson;
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.fail(`🧪 QA (Resumed): Final Status: ${result.validationStatus}`));
187
+ this.log(theme.info(`Session persisted. Resume later with: n8m resume ${threadId}`));
188
+ return;
138
189
  }
139
190
  }
140
191
  else {
141
- this.log(theme.info(`Session persisted. Resume later with: n8m resume ${threadId}`));
142
- return;
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 sanitizedName = workflowName.replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase();
159
- let targetFile = flags.output;
160
- // If multiple workflows and output provided, append name to avoid overwrite, unless it's a directory
161
- if (workflows.length > 1 && targetFile && !targetFile.endsWith('.json')) {
162
- targetFile = path.join(targetFile, `${sanitizedName}.json`);
163
- }
164
- else if (workflows.length > 1 && targetFile) {
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
- savedResources.push({ path: targetFile, name: workflowName, original: workflow });
177
- this.log(theme.success(`\nWorkflow saved to: ${targetFile}`));
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
+ }