@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,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
- // Base URLs and default models for known providers
6
- const PROVIDER_PRESETS = {
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-2.5-flash',
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
- client;
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 — that's fine
49
+ // Config file doesn't exist yet
32
50
  }
33
- let apiKey = process.env.AI_API_KEY || fileConfig['aiKey'];
34
- let baseURL = process.env.AI_BASE_URL || fileConfig['aiBaseUrl'];
35
- let provider = (process.env.AI_PROVIDER || fileConfig['aiProvider'])?.toLowerCase();
36
- // Backward compat: GEMINI_API_KEY still works
37
- if (!apiKey && process.env.GEMINI_API_KEY) {
38
- apiKey = process.env.GEMINI_API_KEY;
39
- if (!provider)
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 (!apiKey) {
43
- console.warn('⚠️ No AI key found. Run: n8m config --ai-key <your-key> --ai-provider openai');
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.client = new OpenAI({
52
- apiKey: apiKey || 'no-key',
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
- * Core generation method — works with any OpenAI-compatible API
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 model = options.model || this.model;
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(' (AI Thinking...)\n');
125
+ if (process.stdout.isTTY && process.env.NODE_ENV !== 'test') {
126
+ process.stdout.write(` (AI Thinking: ${model})\n`);
76
127
  }
77
- const stream = await this.client.chat.completions.create({
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
- let text = '';
84
- for await (const chunk of stream) {
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
- const isRetryable = error.status === 503 ||
94
- error.status === 529 ||
95
- (error.message && (error.message.includes('fetch failed') ||
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
- * Generate an n8n workflow from a description
111
- */
112
- async generateWorkflow(description) {
113
- const systemPrompt = `You are an expert n8n workflow architect.
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
- catch (e) {
127
- console.error('Failed to parse generated workflow JSON', e);
128
- throw new Error('AI generated invalid JSON');
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
- * Generate a Workflow Specification from a description
133
- */
134
- async generateSpec(description) {
135
- const prompt = `You are an n8n Solutions Architect.
136
- Convert the following user request into a structured Workflow Specification.
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
- The Specification should be a JSON object with:
139
- 1. goal: A clear statement of the objective.
140
- 2. suggestedName: A concise, descriptive name for the main workflow (e.g., "Hourly Bitcoin Price Alert").
141
- 3. tasks: A list of strings, each describing a logical step to achieve the goal.
142
- 4. nodes: Potential n8n nodes involved (e.g. ['Webhook', 'HTTP Request', 'Slack']).
143
- 5. assumptions: Any assumptions made about credentials or environment.
144
- 6. questions: A list of string questions if the request is ambiguous or missing critical details. Empty if clear.
145
-
146
- Output ONLY valid JSON.
147
-
148
- User Request: ${description}`;
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
- let cleanJson = response || '{}';
151
- cleanJson = cleanJson.replace(/```json\n?|\n?```/g, '').trim();
192
+ const cleanJson = response.replace(/```json\n?|\n?```/g, "").trim();
152
193
  try {
153
- return JSON.parse(cleanJson);
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 (e) {
156
- console.error('Failed to parse generated spec JSON', e);
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
- * Refine a Specification based on user feedback
162
- */
163
- async refineSpec(spec, feedback) {
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
- let cleanJson = response || '{}';
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 (e) {
182
- console.error('Failed to parse refined spec JSON', e);
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
- * Generate workflow JSONs from an approved Specification
188
- */
189
- async generateWorkflowFromSpec(spec) {
190
- const prompt = `You are an n8n Workflow Engineer.
191
- Generate the valid n8n workflow JSON(s) based on the following approved Specification.
192
-
193
- Specification:
194
- ${JSON.stringify(spec, null, 2)}
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
- Output ONLY valid JSON. No commentary. No markdown.
218
- `;
219
- const response = await this.generateContent(prompt);
220
- let cleanJson = response || '{}';
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.workflows && Array.isArray(result.workflows)) {
225
- result.workflows = result.workflows.map((wf) => this.fixHallucinatedNodes(wf));
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 (e) {
230
- console.error('Failed to parse workflow JSON from spec', e);
231
- throw new Error('AI generated invalid JSON for workflow from spec');
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
- * Diagnostic Repair: Fix a workflow based on execution error
262
- */
263
- async generateWorkflowFix(workflowJson, errorContext, model, _useSearch = false, validNodeTypes = []) {
264
- const prompt = `You are a Senior n8n Workflow Engineer.
265
- A workflow failed during execution. Your task is to analyze the JSON and the Error, and provide a FIXED version of the workflow JSON.
266
-
267
- Error Context:
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
- Workflow JSON:
271
- ${JSON.stringify(workflowJson, null, 2)}
252
+ [N8N NODE REFERENCE GUIDE]
253
+ ${staticRef}
254
+
255
+ ${validNodeTypes.length > 0 ? `Valid available node types: ${validNodeTypes.join(', ')}` : ''}
272
256
 
273
- Review the nodes involved in the error.
274
- ${validNodeTypes.length > 0 ? `CRITICAL: You MUST only use node types from the following ALLOWED list: ${JSON.stringify(validNodeTypes.slice(0, 100))}... (and other standard n8n-nodes-base.* types). If a node type is not valid, replace it with 'n8n-nodes-base.httpRequest' or 'n8n-nodes-base.set'.` : ''}
275
- IMPORTANT: If the error is "Unrecognized node type: n8n-nodes-base.schedule", you MUST fix it to "n8n-nodes-base.scheduleTrigger".
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
- let cleanJson = response || '{}';
284
- cleanJson = cleanJson.replace(/```json\n?|\n?```/g, '').trim();
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('Failed to parse AI workflow fix', e);
293
- throw new Error('AI generated invalid JSON for fix');
267
+ console.error("Failed to parse fix JSON", e);
268
+ return workflow;
294
269
  }
295
270
  }
296
- /**
297
- * Auto-correct common n8n node type hallucinations
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
- 'n8n-nodes-base.rssFeed': 'n8n-nodes-base.rssFeedRead',
304
- 'rssFeed': 'n8n-nodes-base.rssFeedRead',
305
- 'n8n-nodes-base.gpt': 'n8n-nodes-base.openAi',
306
- 'n8n-nodes-base.openai': 'n8n-nodes-base.openAi',
307
- 'openai': 'n8n-nodes-base.openAi',
308
- 'n8n-nodes-base.openAiChat': 'n8n-nodes-base.openAi',
309
- 'n8n-nodes-base.openAIChat': 'n8n-nodes-base.openAi',
310
- 'n8n-nodes-base.openaiChat': 'n8n-nodes-base.openAi',
311
- 'n8n-nodes-base.gemini': 'n8n-nodes-base.googleGemini',
312
- 'n8n-nodes-base.cheerioHtml': 'n8n-nodes-base.htmlExtract',
313
- 'cheerioHtml': 'n8n-nodes-base.htmlExtract',
314
- 'n8n-nodes-base.schedule': 'n8n-nodes-base.scheduleTrigger',
315
- 'schedule': 'n8n-nodes-base.scheduleTrigger',
316
- 'n8n-nodes-base.cron': 'n8n-nodes-base.scheduleTrigger',
317
- 'n8n-nodes-base.googleCustomSearch': 'n8n-nodes-base.googleGemini',
318
- 'googleCustomSearch': 'n8n-nodes-base.googleGemini',
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
- * Generate an alternative workflow specification with a different approach to the same goal.
375
- * Used by the Architect node to produce a second strategy for parallel Engineer execution.
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
- let cleanJson = response || '{}';
404
- cleanJson = cleanJson.replace(/```json\n?|\n?```/g, '').trim();
371
+ const cleanJson = response.replace(/```json\n?|\n?```/g, "").trim();
405
372
  try {
406
- return JSON.parse(cleanJson);
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
- console.warn('[AIService] Failed to parse alternative spec, falling back to primary variant');
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 <= 1) {
419
- return { selectedIndex: 0, reason: 'Single candidate — no evaluation needed.' };
420
- }
421
- const candidateSummaries = candidates.map((c, i) => {
422
- const nodeNames = c?.nodes?.map((n) => n.name || n.type).join(', ') || 'unknown nodes';
423
- return `Candidate ${i + 1}: "${c?.name || 'Unnamed'}" — Nodes: [${nodeNames}]`;
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 an n8n Solutions Architect evaluating workflow implementations.
426
-
427
- Goal: ${goal}
428
-
429
- Evaluate these ${candidates.length} candidate workflows and select the BEST one:
430
- ${candidateSummaries}
431
-
432
- Select the best candidate based on:
433
- 1. Completeness does it fully achieve the goal?
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
- const idx = typeof result.selectedIndex === 'number' ? result.selectedIndex : 0;
446
- return {
447
- selectedIndex: Math.max(0, Math.min(idx, candidates.length - 1)),
448
- reason: result.reason || 'Best overall candidate.',
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
- console.warn('[AIService] Failed to parse candidate evaluation, defaulting to index 0');
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
- * Validate against real node types and shim unknown ones
420
+ * Generates 3-5 diverse test scenarios (input payloads) for a workflow.
458
421
  */
459
- validateAndShim(workflow, validNodeTypes = [], explicitlyInvalid = []) {
460
- const valid = validNodeTypes || [];
461
- const invalid = explicitlyInvalid || [];
462
- if (valid.length === 0 && invalid.length === 0)
463
- return workflow;
464
- if (!workflow || !workflow.nodes || !Array.isArray(workflow.nodes))
465
- return workflow;
466
- const validSet = new Set(valid.map(t => t.toLowerCase()));
467
- const invalidSet = new Set(invalid.map(t => t.toLowerCase()));
468
- const isTrigger = (name) => name.toLowerCase().includes('trigger') || name.toLowerCase().includes('webhook');
469
- workflow.nodes = workflow.nodes.map((node) => {
470
- if (!node || !node.type)
471
- return node;
472
- const type = node.type.toLowerCase();
473
- const shouldShim = invalidSet.has(type) || (validSet.size > 0 && !validSet.has(type));
474
- if (!shouldShim)
475
- return node;
476
- console.warn(`[Validation] Unknown/Invalid node type detected: ${node.type}. Shimming...`);
477
- const originalType = node.type;
478
- const notes = `[Shim] Original Type: ${originalType}. Replaced because type is not installed on this n8n instance.`;
479
- const apiKeywords = [
480
- 'api', 'http', 'slack', 'discord', 'telegram', 'google', 'aws',
481
- 'github', 'stripe', 'twilio', 'linear', 'notion', 'airtable',
482
- 'alpaca', 'openai', 'hubspot', 'mailchimp', 'postgres', 'mysql',
483
- 'redis', 'mongo', 'firebase', 'supabase',
484
- ];
485
- const isApi = apiKeywords.some(keyword => originalType.includes(keyword));
486
- let replacementType = 'n8n-nodes-base.set';
487
- if (isTrigger(originalType)) {
488
- replacementType = 'n8n-nodes-base.webhook';
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;