@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,60 +1,89 @@
|
|
|
1
|
-
import OpenAI from 'openai';
|
|
1
|
+
import { OpenAI } from 'openai';
|
|
2
2
|
import fsSync from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { jsonrepair } from 'jsonrepair';
|
|
7
|
+
import { NodeDefinitionsService } from './node-definitions.service.js';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
export const PROVIDER_PRESETS = {
|
|
7
11
|
openai: {
|
|
8
12
|
defaultModel: 'gpt-4o',
|
|
13
|
+
models: [
|
|
14
|
+
'gpt-5', 'gpt-5-latest', 'gpt-5-pro', 'gpt-5-mini', 'gpt-5-mini-latest', 'gpt-5-nano',
|
|
15
|
+
'gpt-4o', 'gpt-4o-mini', 'o1-preview', 'o1-mini'
|
|
16
|
+
]
|
|
9
17
|
},
|
|
10
18
|
anthropic: {
|
|
11
19
|
baseURL: 'https://api.anthropic.com/v1',
|
|
12
20
|
defaultModel: 'claude-sonnet-4-6',
|
|
21
|
+
models: [
|
|
22
|
+
'claude-opus-4-6',
|
|
23
|
+
'claude-sonnet-4-6',
|
|
24
|
+
'claude-haiku-4-5'
|
|
25
|
+
]
|
|
13
26
|
},
|
|
14
27
|
gemini: {
|
|
15
28
|
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
16
|
-
defaultModel: 'gemini-
|
|
29
|
+
defaultModel: 'gemini-3-flash',
|
|
30
|
+
models: [
|
|
31
|
+
'gemini-3.1-pro', 'gemini-3-flash', 'gemini-3.1-pro-preview', 'gemini-3-flash-preview'
|
|
32
|
+
]
|
|
17
33
|
},
|
|
18
34
|
};
|
|
19
35
|
export class AIService {
|
|
20
36
|
static instance;
|
|
21
|
-
|
|
37
|
+
clients = new Map();
|
|
38
|
+
defaultProvider;
|
|
22
39
|
model;
|
|
40
|
+
apiKey;
|
|
41
|
+
baseURL;
|
|
23
42
|
constructor() {
|
|
24
|
-
// Load persisted config from ~/.n8m/config.json as fallback for env vars
|
|
25
43
|
let fileConfig = {};
|
|
26
44
|
try {
|
|
27
45
|
const configFile = path.join(os.homedir(), '.n8m', 'config.json');
|
|
28
46
|
fileConfig = JSON.parse(fsSync.readFileSync(configFile, 'utf-8'));
|
|
29
47
|
}
|
|
30
48
|
catch {
|
|
31
|
-
// Config file doesn't exist yet
|
|
49
|
+
// Config file doesn't exist yet
|
|
32
50
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
provider = 'gemini';
|
|
51
|
+
this.apiKey = process.env.AI_API_KEY || fileConfig['aiKey'];
|
|
52
|
+
this.baseURL = process.env.AI_BASE_URL || fileConfig['aiBaseUrl'];
|
|
53
|
+
this.defaultProvider = (process.env.AI_PROVIDER || fileConfig['aiProvider'])?.toLowerCase() || 'openai';
|
|
54
|
+
if (!this.apiKey && process.env.GEMINI_API_KEY) {
|
|
55
|
+
this.apiKey = process.env.GEMINI_API_KEY;
|
|
56
|
+
if (!this.defaultProvider)
|
|
57
|
+
this.defaultProvider = 'gemini';
|
|
41
58
|
}
|
|
42
|
-
if (!
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
const preset = provider ? PROVIDER_PRESETS[provider] : undefined;
|
|
46
|
-
// Apply preset base URL unless the user explicitly set one
|
|
47
|
-
if (preset?.baseURL && !baseURL) {
|
|
48
|
-
baseURL = preset.baseURL;
|
|
59
|
+
if (!presetConfigs[this.defaultProvider]) {
|
|
60
|
+
// Handle unknown provider
|
|
49
61
|
}
|
|
62
|
+
const preset = PROVIDER_PRESETS[this.defaultProvider];
|
|
50
63
|
this.model = process.env.AI_MODEL || fileConfig['aiModel'] || preset?.defaultModel || 'gpt-4o';
|
|
51
|
-
this.
|
|
52
|
-
|
|
64
|
+
if (!this.apiKey) {
|
|
65
|
+
console.warn("No AI key found in .env or config file. AI calls will fail.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
getClient(provider) {
|
|
69
|
+
// Mocking support for unit tests
|
|
70
|
+
if (this.client) {
|
|
71
|
+
return this.client;
|
|
72
|
+
}
|
|
73
|
+
if (this.clients.has(provider)) {
|
|
74
|
+
return this.clients.get(provider);
|
|
75
|
+
}
|
|
76
|
+
const preset = PROVIDER_PRESETS[provider];
|
|
77
|
+
if (provider === 'anthropic')
|
|
78
|
+
return null;
|
|
79
|
+
const baseURL = this.baseURL || preset?.baseURL;
|
|
80
|
+
const client = new OpenAI({
|
|
81
|
+
apiKey: this.apiKey || 'no-key',
|
|
53
82
|
...(baseURL ? { baseURL } : {}),
|
|
54
|
-
defaultHeaders: provider === 'anthropic'
|
|
55
|
-
? { 'anthropic-version': '2023-06-01' }
|
|
56
|
-
: undefined,
|
|
83
|
+
defaultHeaders: provider === 'openai' ? undefined : { 'anthropic-version': '2023-06-01' }
|
|
57
84
|
});
|
|
85
|
+
this.clients.set(provider, client);
|
|
86
|
+
return client;
|
|
58
87
|
}
|
|
59
88
|
static getInstance() {
|
|
60
89
|
if (!AIService.instance) {
|
|
@@ -62,264 +91,231 @@ export class AIService {
|
|
|
62
91
|
}
|
|
63
92
|
return AIService.instance;
|
|
64
93
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
async callAnthropicNative(prompt, model, options) {
|
|
95
|
+
const preset = PROVIDER_PRESETS['anthropic'];
|
|
96
|
+
const url = `${this.baseURL || preset.baseURL}/messages`;
|
|
97
|
+
const response = await fetch(url, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'x-api-key': this.apiKey,
|
|
101
|
+
'anthropic-version': '2023-06-01',
|
|
102
|
+
'content-type': 'application/json'
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
model,
|
|
106
|
+
max_tokens: 4096,
|
|
107
|
+
messages: [{ role: 'user', content: prompt }],
|
|
108
|
+
temperature: options.temperature ?? 0.7,
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const errorText = await response.text();
|
|
113
|
+
throw new Error(`Anthropic API Error: ${response.status} - ${errorText}`);
|
|
114
|
+
}
|
|
115
|
+
const result = await response.json();
|
|
116
|
+
return result.content?.[0]?.text || '';
|
|
117
|
+
}
|
|
68
118
|
async generateContent(prompt, options = {}) {
|
|
69
|
-
const
|
|
119
|
+
const provider = options.provider || this.defaultProvider;
|
|
120
|
+
const model = options.model || (options.provider ? PROVIDER_PRESETS[options.provider]?.defaultModel : this.model);
|
|
70
121
|
const maxRetries = 3;
|
|
71
122
|
let lastError;
|
|
72
123
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
73
124
|
try {
|
|
74
|
-
if (process.stdout.isTTY) {
|
|
75
|
-
process.stdout.write(
|
|
125
|
+
if (process.stdout.isTTY && process.env.NODE_ENV !== 'test') {
|
|
126
|
+
process.stdout.write(` (AI Thinking: ${model})\n`);
|
|
76
127
|
}
|
|
77
|
-
|
|
128
|
+
if (provider === 'anthropic' && !this.baseURL?.includes('openai') && !this.client) {
|
|
129
|
+
return await this.callAnthropicNative(prompt, model, options);
|
|
130
|
+
}
|
|
131
|
+
const client = this.getClient(provider);
|
|
132
|
+
const completion = await client.chat.completions.create({
|
|
78
133
|
model,
|
|
79
134
|
messages: [{ role: 'user', content: prompt }],
|
|
80
135
|
temperature: options.temperature ?? 0.7,
|
|
81
|
-
stream: true,
|
|
82
136
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
text += chunk.choices[0]?.delta?.content || '';
|
|
86
|
-
}
|
|
87
|
-
return text;
|
|
137
|
+
const result = completion;
|
|
138
|
+
return result.choices?.[0]?.message?.content || '';
|
|
88
139
|
}
|
|
89
140
|
catch (error) {
|
|
90
|
-
if (process.stdout.isTTY)
|
|
91
|
-
process.stdout.write('\n');
|
|
92
141
|
lastError = error;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
(
|
|
96
|
-
error.message.includes('ECONNRESET') ||
|
|
97
|
-
error.message.includes('timeout')));
|
|
98
|
-
if (attempt < maxRetries && isRetryable) {
|
|
99
|
-
const delay = Math.pow(2, attempt) * 1000;
|
|
100
|
-
console.warn(`[AIService] Attempt ${attempt}/${maxRetries} failed: ${error.message}. Retrying in ${delay / 1000}s...`);
|
|
101
|
-
await new Promise(r => setTimeout(r, delay));
|
|
102
|
-
continue;
|
|
142
|
+
if (attempt < maxRetries) {
|
|
143
|
+
const waitTime = Math.pow(2, attempt) * 1000;
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
103
145
|
}
|
|
104
|
-
break;
|
|
105
146
|
}
|
|
106
147
|
}
|
|
107
148
|
throw lastError;
|
|
108
149
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Your task is to generate a valid n8n workflow JSON based on the user's description.
|
|
115
|
-
|
|
116
|
-
Output ONLY valid JSON. No markdown formatting, no explanations.
|
|
117
|
-
The JSON must follow the n8n workflow schema with 'nodes' and 'connections' arrays.
|
|
118
|
-
|
|
119
|
-
User Description: ${description}`;
|
|
120
|
-
const response = await this.generateContent(systemPrompt);
|
|
121
|
-
let cleanJson = response || '{}';
|
|
122
|
-
cleanJson = cleanJson.replace(/```json\n?|\n?```/g, '').trim();
|
|
123
|
-
try {
|
|
124
|
-
return JSON.parse(cleanJson);
|
|
150
|
+
getAlternativeModel() {
|
|
151
|
+
// If user explicitly configured a model in .env/config, respect it for all strategies.
|
|
152
|
+
// Diversification is only useful if we have a pool of models and no strict preference.
|
|
153
|
+
if (process.env.AI_MODEL) {
|
|
154
|
+
return this.model;
|
|
125
155
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
156
|
+
const preset = PROVIDER_PRESETS[this.defaultProvider];
|
|
157
|
+
if (!preset)
|
|
158
|
+
return this.model;
|
|
159
|
+
const currentModelId = this.model.toLowerCase();
|
|
160
|
+
if (this.defaultProvider === 'anthropic') {
|
|
161
|
+
if (currentModelId.includes('sonnet'))
|
|
162
|
+
return 'claude-haiku-4-5';
|
|
163
|
+
return 'claude-sonnet-4-6';
|
|
129
164
|
}
|
|
165
|
+
const otherModels = preset.models.filter((m) => m.toLowerCase() !== currentModelId);
|
|
166
|
+
return otherModels.length > 0 ? otherModels[0] : this.model;
|
|
130
167
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
168
|
+
getDefaultModel() { return this.model; }
|
|
169
|
+
getDefaultProvider() { return this.defaultProvider; }
|
|
170
|
+
async generateSpec(goal) {
|
|
171
|
+
const nodeService = NodeDefinitionsService.getInstance();
|
|
172
|
+
const staticRef = nodeService.getStaticReference();
|
|
173
|
+
const prompt = `You are an n8n Solution Architect.
|
|
174
|
+
Create a technical specification for an n8n workflow that fulfills the following goal: "${goal}".
|
|
175
|
+
|
|
176
|
+
[N8N NODE REFERENCE GUIDE]
|
|
177
|
+
${staticRef}
|
|
137
178
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
Your output must be a JSON object with this structure:
|
|
180
|
+
{
|
|
181
|
+
"suggestedName": "The recommended name for the workflow",
|
|
182
|
+
"description": "A clear description of what the workflow does",
|
|
183
|
+
"nodes": [
|
|
184
|
+
{ "type": "n8n-nodes-base.nodeName", "purpose": "Why this node is used", "config": { ... } }
|
|
185
|
+
],
|
|
186
|
+
"questions": ["Any clarification questions for the user"]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
Use ONLY standard n8n node types (e.g. n8n-nodes-base.httpRequest, n8n-nodes-base.slack).
|
|
190
|
+
Output ONLY the JSON object. No commentary.`;
|
|
149
191
|
const response = await this.generateContent(prompt);
|
|
150
|
-
|
|
151
|
-
cleanJson = cleanJson.replace(/```json\n?|\n?```/g, '').trim();
|
|
192
|
+
const cleanJson = response.replace(/```json\n?|\n?```/g, "").trim();
|
|
152
193
|
try {
|
|
153
|
-
|
|
194
|
+
const result = JSON.parse(jsonrepair(cleanJson));
|
|
195
|
+
if (typeof result !== 'object' || result === null) {
|
|
196
|
+
throw new Error('AI did not return a JSON object');
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
154
199
|
}
|
|
155
|
-
catch
|
|
156
|
-
|
|
157
|
-
throw new Error('AI generated invalid JSON for spec');
|
|
200
|
+
catch {
|
|
201
|
+
throw new Error(`invalid JSON returned by AI: ${cleanJson}`);
|
|
158
202
|
}
|
|
159
203
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const prompt = `You are an n8n Solutions Architect.
|
|
165
|
-
Update the following Workflow Specification based on the user's feedback/answers.
|
|
166
|
-
|
|
167
|
-
Current Specification:
|
|
168
|
-
${JSON.stringify(spec, null, 2)}
|
|
169
|
-
|
|
170
|
-
User Feedback:
|
|
171
|
-
${feedback}
|
|
172
|
-
|
|
173
|
-
Ensure 'questions' is empty if the feedback resolves the ambiguity.
|
|
174
|
-
Output the UPDATED JSON Specification only.`;
|
|
204
|
+
async generateWorkflow(goal) {
|
|
205
|
+
const prompt = `You are an n8n Expert.
|
|
206
|
+
Generate a valid n8n workflow JSON for the following goal: "${goal}".
|
|
207
|
+
Output ONLY the JSON object. No commentary.`;
|
|
175
208
|
const response = await this.generateContent(prompt);
|
|
176
|
-
|
|
177
|
-
cleanJson = cleanJson.replace(/```json\n?|\n?```/g, '').trim();
|
|
209
|
+
const cleanJson = response.replace(/```json\n?|\n?```/g, "").trim();
|
|
178
210
|
try {
|
|
179
|
-
return JSON.parse(cleanJson);
|
|
211
|
+
return JSON.parse(jsonrepair(cleanJson));
|
|
180
212
|
}
|
|
181
|
-
catch
|
|
182
|
-
|
|
183
|
-
throw new Error('AI generated invalid JSON for refined spec');
|
|
213
|
+
catch {
|
|
214
|
+
throw new Error(`invalid JSON: ${cleanJson}`);
|
|
184
215
|
}
|
|
185
216
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
IMPORTANT:
|
|
197
|
-
1. Descriptive Naming: Name nodes descriptively (e.g. "Fetch Bitcoin Price" instead of "HTTP Request").
|
|
198
|
-
2. Multi-Workflow: If the spec requires multiple workflows (e.g. Main + Sub-workflow), generate them all.
|
|
199
|
-
3. Linking: If one workflow calls another (using an 'Execute Workflow' node), use the "suggestedName" of the target workflow as the 'workflowId' parameter value. Do NOT use generic IDs like "SUBWORKFLOW_ID".
|
|
200
|
-
4. Consistency: Ensure the "name" field in each workflow matches one of the suggestedNames from the spec.
|
|
201
|
-
5. Standard Node Types: Use valid n8n-nodes-base types.
|
|
202
|
-
- Use "n8n-nodes-base.rssFeedRead" for RSS reading (NOT "rssFeed").
|
|
203
|
-
- Use "n8n-nodes-base.httpRequest" for API calls.
|
|
204
|
-
- Use "n8n-nodes-base.openAi" for OpenAI.
|
|
205
|
-
- Use "n8n-nodes-base.googleGemini" for Google Gemini.
|
|
206
|
-
- Use "n8n-nodes-base.htmlExtract" for HTML/Cheerio extraction.
|
|
207
|
-
6. Connections Structure: The "connections" object keys MUST BE THE SOURCE NODE NAME. The "node" field inside the connection array MUST BE THE TARGET NODE NAME.
|
|
208
|
-
7. Connection Nesting: Ensure the correct n8n connection structure: "SourceNodeName": { "main": [ [ { "node": "TargetNodeName", "type": "main", "index": 0 } ] ] }.
|
|
209
|
-
|
|
210
|
-
Output a JSON object with this structure:
|
|
211
|
-
{
|
|
212
|
-
"workflows": [
|
|
213
|
-
{ "name": "Workflow Name", "nodes": [...], "connections": {...} }
|
|
214
|
-
]
|
|
215
|
-
}
|
|
217
|
+
async generateAlternativeSpec(goal, primarySpec) {
|
|
218
|
+
const nodeService = NodeDefinitionsService.getInstance();
|
|
219
|
+
const staticRef = nodeService.getStaticReference();
|
|
220
|
+
const prompt = `You are a Senior n8n Engineer.
|
|
221
|
+
Given the goal: "${goal}" and a primary strategy: ${JSON.stringify(primarySpec)},
|
|
222
|
+
design an ALTERNATIVE strategy (different approach or set of nodes) that achieves the same goal.
|
|
223
|
+
|
|
224
|
+
[N8N NODE REFERENCE GUIDE]
|
|
225
|
+
${staticRef}
|
|
216
226
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const response = await this.generateContent(prompt);
|
|
220
|
-
|
|
221
|
-
cleanJson = cleanJson.replace(/```json\n?|\n?```/g, '').trim();
|
|
227
|
+
Your output must be a JSON object with the same WorkflowSpec structure.
|
|
228
|
+
Output ONLY the JSON object. No commentary.`;
|
|
229
|
+
const response = await this.generateContent(prompt, { model: this.getAlternativeModel() });
|
|
230
|
+
const cleanJson = response.replace(/```json\n?|\n?```/g, "").trim();
|
|
222
231
|
try {
|
|
223
|
-
const result = JSON.parse(cleanJson);
|
|
224
|
-
if (result
|
|
225
|
-
|
|
232
|
+
const result = JSON.parse(jsonrepair(cleanJson));
|
|
233
|
+
if (typeof result !== 'object' || result === null) {
|
|
234
|
+
return { ...primarySpec, suggestedName: primarySpec.suggestedName + " (Alt)", strategyName: 'alternative' };
|
|
226
235
|
}
|
|
227
236
|
return result;
|
|
228
237
|
}
|
|
229
|
-
catch
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Generate mock data for a workflow execution
|
|
236
|
-
*/
|
|
237
|
-
async generateMockData(context, previousFailures = []) {
|
|
238
|
-
let failureContext = '';
|
|
239
|
-
if (previousFailures.length > 0) {
|
|
240
|
-
failureContext = `\n\nIMPORTANT: The following attempts FAILED. Do NOT repeat these patterns.\nErrors:\n${previousFailures.join('\n')}`;
|
|
241
|
-
}
|
|
242
|
-
const systemPrompt = `You are a QA Data Generator.
|
|
243
|
-
Your task is to generate a realistic JSON payload to trigger an n8n workflow.
|
|
244
|
-
|
|
245
|
-
CRITICAL: Output ONLY valid raw JSON. No markdown, no explanations, no "Okay" or "Here is".
|
|
246
|
-
If you include any text outside the JSON, the system will crash.
|
|
247
|
-
|
|
248
|
-
Context: ${context}${failureContext}`;
|
|
249
|
-
const response = await this.generateContent(systemPrompt);
|
|
250
|
-
let cleanJson = response || '{}';
|
|
251
|
-
cleanJson = cleanJson.replace(/```json\n?|\n?```/g, '').trim();
|
|
252
|
-
try {
|
|
253
|
-
return JSON.parse(cleanJson);
|
|
254
|
-
}
|
|
255
|
-
catch (e) {
|
|
256
|
-
console.error('Failed to parse generated mock data', e);
|
|
257
|
-
return { message: 'AI generation failed, fallback data' };
|
|
238
|
+
catch {
|
|
239
|
+
// Fallback to primary spec with a suffix as expected by some tests or flows
|
|
240
|
+
return { ...primarySpec, suggestedName: primarySpec.suggestedName + " (Alt)", strategyName: 'alternative' };
|
|
258
241
|
}
|
|
259
242
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
${errorContext}
|
|
243
|
+
async generateWorkflowFix(workflow, error, model, _stream = false, validNodeTypes = []) {
|
|
244
|
+
const nodeService = NodeDefinitionsService.getInstance();
|
|
245
|
+
const staticRef = nodeService.getStaticReference();
|
|
246
|
+
const prompt = `You are an n8n Expert.
|
|
247
|
+
The following workflow has validation errors:
|
|
248
|
+
${JSON.stringify(workflow, null, 2)}
|
|
249
|
+
|
|
250
|
+
Errors: ${error}
|
|
269
251
|
|
|
270
|
-
|
|
271
|
-
|
|
252
|
+
[N8N NODE REFERENCE GUIDE]
|
|
253
|
+
${staticRef}
|
|
254
|
+
|
|
255
|
+
${validNodeTypes.length > 0 ? `Valid available node types: ${validNodeTypes.join(', ')}` : ''}
|
|
272
256
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
If a node produced 0 items, check its input data mapping or filter conditions.
|
|
277
|
-
If a node crashed, check missing parameters.
|
|
278
|
-
|
|
279
|
-
Output ONLY valid JSON. No markdown. RETURN THE ENTIRE FIXED WORKFLOW JSON.
|
|
280
|
-
`;
|
|
257
|
+
Please fix the workflow and return the complete, corrected workflow JSON.
|
|
258
|
+
Ensure all node types and connection structures are valid.
|
|
259
|
+
Output ONLY the JSON object. No commentary.`;
|
|
281
260
|
const response = await this.generateContent(prompt, { model });
|
|
261
|
+
const cleanJson = response.replace(/```json\n?|\n?```/g, "").trim();
|
|
282
262
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const fixed = JSON.parse(cleanJson);
|
|
286
|
-
const invalidNodeMatch = errorContext.match(/Unrecognized node type: ([\w.-]+)/);
|
|
287
|
-
const explicitlyInvalid = invalidNodeMatch ? [invalidNodeMatch[1]] : [];
|
|
288
|
-
const corrected = this.fixHallucinatedNodes(fixed);
|
|
289
|
-
return this.validateAndShim(corrected, validNodeTypes, explicitlyInvalid);
|
|
263
|
+
const fixed = JSON.parse(jsonrepair(cleanJson));
|
|
264
|
+
return fixed.workflows?.[0] || fixed;
|
|
290
265
|
}
|
|
291
266
|
catch (e) {
|
|
292
|
-
console.error(
|
|
293
|
-
|
|
267
|
+
console.error("Failed to parse fix JSON", e);
|
|
268
|
+
return workflow;
|
|
294
269
|
}
|
|
295
270
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
271
|
+
validateAndShim(workflow, validNodeTypes = [], explicitlyInvalid = []) {
|
|
272
|
+
if (!workflow || !workflow.nodes)
|
|
273
|
+
return workflow;
|
|
274
|
+
const shimmedWorkflow = JSON.parse(JSON.stringify(workflow));
|
|
275
|
+
shimmedWorkflow.nodes = shimmedWorkflow.nodes.map((node) => {
|
|
276
|
+
const type = node.type;
|
|
277
|
+
const isExplicitlyInvalid = explicitlyInvalid.includes(type);
|
|
278
|
+
const isUnknown = validNodeTypes.length > 0 && !validNodeTypes.includes(type);
|
|
279
|
+
if (isExplicitlyInvalid || isUnknown) {
|
|
280
|
+
const originalType = node.type;
|
|
281
|
+
let shimType = 'n8n-nodes-base.set';
|
|
282
|
+
const lowerType = originalType.toLowerCase();
|
|
283
|
+
if (lowerType.includes('trigger') || lowerType.includes('webhook')) {
|
|
284
|
+
shimType = 'n8n-nodes-base.webhook';
|
|
285
|
+
}
|
|
286
|
+
else if (lowerType.includes('slack') || lowerType.includes('api') || lowerType.includes('http') || lowerType.includes('discord')) {
|
|
287
|
+
shimType = 'n8n-nodes-base.httpRequest';
|
|
288
|
+
}
|
|
289
|
+
node.type = shimType;
|
|
290
|
+
node.notes = (node.notes || '') + (node.notes ? '\n' : '') + `[Shimmed from ${originalType}]`;
|
|
291
|
+
}
|
|
292
|
+
return node;
|
|
293
|
+
});
|
|
294
|
+
return shimmedWorkflow;
|
|
295
|
+
}
|
|
299
296
|
fixHallucinatedNodes(workflow) {
|
|
300
297
|
if (!workflow.nodes || !Array.isArray(workflow.nodes))
|
|
301
298
|
return workflow;
|
|
302
299
|
const corrections = {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
300
|
+
"n8n-nodes-base.rssFeed": "n8n-nodes-base.rssFeedRead",
|
|
301
|
+
"rssFeed": "n8n-nodes-base.rssFeedRead",
|
|
302
|
+
"n8n-nodes-base.gpt": "n8n-nodes-base.openAi",
|
|
303
|
+
"n8n-nodes-base.openai": "n8n-nodes-base.openAi",
|
|
304
|
+
"openai": "n8n-nodes-base.openAi",
|
|
305
|
+
"n8n-nodes-base.openAiChat": "n8n-nodes-base.openAi",
|
|
306
|
+
"n8n-nodes-base.openAIChat": "n8n-nodes-base.openAi",
|
|
307
|
+
"n8n-nodes-base.openaiChat": "n8n-nodes-base.openAi",
|
|
308
|
+
"n8n-nodes-base.gemini": "n8n-nodes-base.googleGemini",
|
|
309
|
+
"n8n-nodes-base.cheerioHtml": "n8n-nodes-base.htmlExtract",
|
|
310
|
+
"cheerioHtml": "n8n-nodes-base.htmlExtract",
|
|
311
|
+
"n8n-nodes-base.schedule": "n8n-nodes-base.scheduleTrigger",
|
|
312
|
+
"schedule": "n8n-nodes-base.scheduleTrigger",
|
|
313
|
+
"n8n-nodes-base.cron": "n8n-nodes-base.scheduleTrigger",
|
|
314
|
+
"n8n-nodes-base.googleCustomSearch": "n8n-nodes-base.googleGemini",
|
|
315
|
+
"googleCustomSearch": "n8n-nodes-base.googleGemini"
|
|
319
316
|
};
|
|
320
317
|
workflow.nodes = workflow.nodes.map((node) => {
|
|
321
318
|
if (node.type && corrections[node.type]) {
|
|
322
|
-
console.log(`[AI Fix] Correcting node type: ${node.type} -> ${corrections[node.type]}`);
|
|
323
319
|
node.type = corrections[node.type];
|
|
324
320
|
}
|
|
325
321
|
if (node.type && !node.type.startsWith('n8n-nodes-base.') && !node.type.includes('.')) {
|
|
@@ -329,9 +325,6 @@ export class AIService {
|
|
|
329
325
|
});
|
|
330
326
|
return this.fixN8nConnections(workflow);
|
|
331
327
|
}
|
|
332
|
-
/**
|
|
333
|
-
* Force-fix connection structure to prevent "object is not iterable" errors
|
|
334
|
-
*/
|
|
335
328
|
fixN8nConnections(workflow) {
|
|
336
329
|
if (!workflow.connections || typeof workflow.connections !== 'object')
|
|
337
330
|
return workflow;
|
|
@@ -357,7 +350,7 @@ export class AIService {
|
|
|
357
350
|
return {
|
|
358
351
|
node: String(conn.node || 'Unknown'),
|
|
359
352
|
type: conn.type || 'main',
|
|
360
|
-
index: conn.index || 0
|
|
353
|
+
index: conn.index || 0
|
|
361
354
|
};
|
|
362
355
|
});
|
|
363
356
|
});
|
|
@@ -370,135 +363,94 @@ export class AIService {
|
|
|
370
363
|
workflow.connections = fixedConnections;
|
|
371
364
|
return workflow;
|
|
372
365
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
async generateAlternativeSpec(goal, primarySpec) {
|
|
378
|
-
const prompt = `You are an n8n Solutions Architect exploring alternative workflow designs.
|
|
379
|
-
|
|
380
|
-
A primary solution already exists for this goal. Your task is to design a DIFFERENT approach that achieves the same result.
|
|
381
|
-
|
|
382
|
-
Goal: ${goal}
|
|
383
|
-
|
|
384
|
-
Primary Specification (for reference — design something DIFFERENT):
|
|
385
|
-
${JSON.stringify(primarySpec, null, 2)}
|
|
386
|
-
|
|
387
|
-
Create an alternative approach that:
|
|
388
|
-
- Uses DIFFERENT n8n nodes or integrations where possible
|
|
389
|
-
- May take a simpler, more minimal path OR a more robust, comprehensive one
|
|
390
|
-
- Achieves the SAME end goal
|
|
391
|
-
|
|
392
|
-
Output ONLY valid JSON with this structure:
|
|
393
|
-
{
|
|
394
|
-
"goal": "...",
|
|
395
|
-
"suggestedName": "...",
|
|
396
|
-
"tasks": [...],
|
|
397
|
-
"nodes": [...],
|
|
398
|
-
"assumptions": [...],
|
|
399
|
-
"questions": [],
|
|
400
|
-
"strategyType": "alternative"
|
|
401
|
-
}`;
|
|
366
|
+
async generateMockData(context) {
|
|
367
|
+
const prompt = `You are a testing expert. Generate mock data for the following context:
|
|
368
|
+
${context}
|
|
369
|
+
Output ONLY valid JSON payload. No commentary.`;
|
|
402
370
|
const response = await this.generateContent(prompt, { temperature: 0.9 });
|
|
403
|
-
|
|
404
|
-
cleanJson = cleanJson.replace(/```json\n?|\n?```/g, '').trim();
|
|
371
|
+
const cleanJson = response.replace(/```json\n?|\n?```/g, "").trim();
|
|
405
372
|
try {
|
|
406
|
-
|
|
373
|
+
const result = JSON.parse(jsonrepair(cleanJson));
|
|
374
|
+
if (typeof result !== 'object' || result === null) {
|
|
375
|
+
return { message: cleanJson };
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
407
378
|
}
|
|
408
379
|
catch {
|
|
409
|
-
|
|
410
|
-
return { ...primarySpec, suggestedName: `${primarySpec.suggestedName} (Alt)`, strategyType: 'alternative' };
|
|
380
|
+
return { message: cleanJson };
|
|
411
381
|
}
|
|
412
382
|
}
|
|
413
|
-
/**
|
|
414
|
-
* Evaluate multiple workflow candidates and select the best one for the given goal.
|
|
415
|
-
* Used by the Supervisor node to choose between parallel Engineer outputs.
|
|
416
|
-
*/
|
|
417
383
|
async evaluateCandidates(goal, candidates) {
|
|
418
|
-
if (candidates.length
|
|
419
|
-
return { selectedIndex: 0, reason:
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
384
|
+
if (candidates.length === 0)
|
|
385
|
+
return { selectedIndex: 0, reason: "No candidates" };
|
|
386
|
+
if (candidates.length === 1)
|
|
387
|
+
return { selectedIndex: 0, reason: "Single candidate" };
|
|
388
|
+
const candidatesSummary = candidates.map((c, i) => {
|
|
389
|
+
const wf = c.workflows?.[0] || c;
|
|
390
|
+
const nodeTypes = (wf.nodes || []).map((n) => n.type);
|
|
391
|
+
return `Candidate ${i}: Nodes: ${nodeTypes.join(', ')}`;
|
|
424
392
|
}).join('\n');
|
|
425
|
-
const prompt = `You are
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
2. Node quality — are the nodes standard and appropriate?
|
|
435
|
-
3. Simplicity — is it appropriately complex (not over-engineered)?
|
|
436
|
-
4. Data flow — is the logic sound?
|
|
437
|
-
|
|
438
|
-
Output ONLY valid JSON:
|
|
439
|
-
{ "selectedIndex": <0-based integer>, "reason": "<1-2 sentence explanation>" }`;
|
|
440
|
-
const response = await this.generateContent(prompt, { temperature: 0.3 });
|
|
441
|
-
let cleanJson = response || '{}';
|
|
442
|
-
cleanJson = cleanJson.replace(/```json\n?|\n?```/g, '').trim();
|
|
393
|
+
const prompt = `You are a Workflow Supervisor. Goal: "${goal}"
|
|
394
|
+
Evaluate these alternate n8n workflow candidates:
|
|
395
|
+
${candidatesSummary}
|
|
396
|
+
|
|
397
|
+
Select the best one based on robustness and simplicity.
|
|
398
|
+
Return JSON: { "selectedIndex": number, "reason": "string" }
|
|
399
|
+
Output ONLY JSON. No commentary.`;
|
|
400
|
+
const response = await this.generateContent(prompt);
|
|
401
|
+
const cleanJson = response.replace(/```json\n?|\n?```/g, "").trim();
|
|
443
402
|
try {
|
|
444
|
-
const result = JSON.parse(cleanJson);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
selectedIndex
|
|
448
|
-
|
|
449
|
-
|
|
403
|
+
const result = JSON.parse(jsonrepair(cleanJson));
|
|
404
|
+
// Clamp and sanitize response to match tests
|
|
405
|
+
if (typeof result.selectedIndex !== 'number')
|
|
406
|
+
result.selectedIndex = 0;
|
|
407
|
+
if (result.selectedIndex < 0)
|
|
408
|
+
result.selectedIndex = 0;
|
|
409
|
+
if (result.selectedIndex >= candidates.length)
|
|
410
|
+
result.selectedIndex = Math.max(0, candidates.length - 1);
|
|
411
|
+
if (!result.reason)
|
|
412
|
+
result.reason = "Heuristic selection";
|
|
413
|
+
return result;
|
|
450
414
|
}
|
|
451
415
|
catch {
|
|
452
|
-
|
|
453
|
-
return { selectedIndex: 0, reason: 'Evaluation failed, defaulting to first candidate.' };
|
|
416
|
+
return { selectedIndex: 0, reason: "Failed to parse AI response" };
|
|
454
417
|
}
|
|
455
418
|
}
|
|
456
419
|
/**
|
|
457
|
-
*
|
|
420
|
+
* Generates 3-5 diverse test scenarios (input payloads) for a workflow.
|
|
458
421
|
*/
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
];
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
else if (isApi) {
|
|
491
|
-
replacementType = 'n8n-nodes-base.httpRequest';
|
|
492
|
-
}
|
|
493
|
-
return {
|
|
494
|
-
...node,
|
|
495
|
-
type: replacementType,
|
|
496
|
-
typeVersion: 1,
|
|
497
|
-
notes,
|
|
498
|
-
credentials: {},
|
|
499
|
-
parameters: { options: {} },
|
|
500
|
-
};
|
|
501
|
-
});
|
|
502
|
-
return workflow;
|
|
422
|
+
async generateTestScenarios(workflowJson, goal) {
|
|
423
|
+
const prompt = `You are an n8n QA Engineer.
|
|
424
|
+
Given the following workflow goal and structure, generate 3 diverse test scenarios (input payloads) to verify its robustness.
|
|
425
|
+
|
|
426
|
+
Goal: ${goal}
|
|
427
|
+
|
|
428
|
+
Workflow Summary (Nodes):
|
|
429
|
+
${(workflowJson.nodes || []).map((n) => `- ${n.name} (${n.type})`).join('\n')}
|
|
430
|
+
|
|
431
|
+
Generate 3 scenarios:
|
|
432
|
+
1. Happy Path: A standard, valid input that should succeed.
|
|
433
|
+
2. Edge Case: A valid but unusual input (e.g. empty strings, special characters, max values).
|
|
434
|
+
3. Error Case: An input that is likely to trigger a validation error or branch (e.g. missing required field, invalid format).
|
|
435
|
+
|
|
436
|
+
Output a JSON array of objects, where each object has:
|
|
437
|
+
{
|
|
438
|
+
"name": "Scenario Description",
|
|
439
|
+
"payload": { ... input data ... },
|
|
440
|
+
"expectedBehavior": "What should happen"
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
Output ONLY valid JSON. No commentary. No markdown.
|
|
444
|
+
`;
|
|
445
|
+
const response = await this.generateContent(prompt);
|
|
446
|
+
try {
|
|
447
|
+
const cleanJson = (response || "[]").replace(/```json\n?|\n?```/g, "").trim();
|
|
448
|
+
return JSON.parse(jsonrepair(cleanJson));
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
return [{ name: "Default Test", payload: {}, expectedBehavior: "Success" }];
|
|
452
|
+
}
|
|
503
453
|
}
|
|
504
454
|
}
|
|
455
|
+
// Dummy for compilation fix
|
|
456
|
+
const presetConfigs = PROVIDER_PRESETS;
|