@lhi/n8m 0.1.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.js +6 -0
  5. package/dist/agentic/checkpointer.d.ts +2 -0
  6. package/dist/agentic/checkpointer.js +14 -0
  7. package/dist/agentic/graph.d.ts +483 -0
  8. package/dist/agentic/graph.js +100 -0
  9. package/dist/agentic/nodes/architect.d.ts +6 -0
  10. package/dist/agentic/nodes/architect.js +51 -0
  11. package/dist/agentic/nodes/engineer.d.ts +11 -0
  12. package/dist/agentic/nodes/engineer.js +182 -0
  13. package/dist/agentic/nodes/qa.d.ts +5 -0
  14. package/dist/agentic/nodes/qa.js +151 -0
  15. package/dist/agentic/nodes/reviewer.d.ts +5 -0
  16. package/dist/agentic/nodes/reviewer.js +111 -0
  17. package/dist/agentic/nodes/supervisor.d.ts +6 -0
  18. package/dist/agentic/nodes/supervisor.js +18 -0
  19. package/dist/agentic/state.d.ts +51 -0
  20. package/dist/agentic/state.js +26 -0
  21. package/dist/commands/config.d.ts +13 -0
  22. package/dist/commands/config.js +47 -0
  23. package/dist/commands/create.d.ts +14 -0
  24. package/dist/commands/create.js +182 -0
  25. package/dist/commands/deploy.d.ts +13 -0
  26. package/dist/commands/deploy.js +68 -0
  27. package/dist/commands/modify.d.ts +13 -0
  28. package/dist/commands/modify.js +276 -0
  29. package/dist/commands/prune.d.ts +9 -0
  30. package/dist/commands/prune.js +98 -0
  31. package/dist/commands/resume.d.ts +8 -0
  32. package/dist/commands/resume.js +39 -0
  33. package/dist/commands/test.d.ts +27 -0
  34. package/dist/commands/test.js +619 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/services/ai.service.d.ts +51 -0
  38. package/dist/services/ai.service.js +421 -0
  39. package/dist/services/n8n.service.d.ts +17 -0
  40. package/dist/services/n8n.service.js +81 -0
  41. package/dist/services/node-definitions.service.d.ts +36 -0
  42. package/dist/services/node-definitions.service.js +102 -0
  43. package/dist/utils/config.d.ts +15 -0
  44. package/dist/utils/config.js +25 -0
  45. package/dist/utils/multilinePrompt.d.ts +1 -0
  46. package/dist/utils/multilinePrompt.js +52 -0
  47. package/dist/utils/n8nClient.d.ts +97 -0
  48. package/dist/utils/n8nClient.js +440 -0
  49. package/dist/utils/sandbox.d.ts +13 -0
  50. package/dist/utils/sandbox.js +34 -0
  51. package/dist/utils/theme.d.ts +23 -0
  52. package/dist/utils/theme.js +92 -0
  53. package/oclif.manifest.json +331 -0
  54. package/package.json +95 -0
@@ -0,0 +1,619 @@
1
+ import inquirer from 'inquirer';
2
+ import { Args, Command, Flags } from '@oclif/core';
3
+ import { theme } from '../utils/theme.js';
4
+ import { N8nClient } from '../utils/n8nClient.js';
5
+ import { ConfigManager } from '../utils/config.js';
6
+ import { runAgenticWorkflow, graph, resumeAgenticWorkflow } from '../agentic/graph.js';
7
+ import * as path from 'path';
8
+ import * as fs from 'fs/promises';
9
+ import { existsSync } from 'fs';
10
+ export default class Test extends Command {
11
+ static args = {
12
+ workflow: Args.string({
13
+ description: 'Path to workflow JSON file',
14
+ required: false,
15
+ }),
16
+ };
17
+ static description = 'Run ephemeral end-to-end tests for n8n workflows';
18
+ static flags = {
19
+ headless: Flags.boolean({
20
+ char: 'h',
21
+ default: true,
22
+ description: 'Run tests in headless mode',
23
+ }),
24
+ 'keep-on-fail': Flags.boolean({
25
+ default: false,
26
+ description: 'Do not delete workflow if test fails',
27
+ }),
28
+ 'no-brand': Flags.boolean({
29
+ default: false,
30
+ hidden: true,
31
+ description: 'Suppress branding header',
32
+ }),
33
+ 'validate-only': Flags.boolean({
34
+ default: false,
35
+ hidden: true,
36
+ description: 'Execute test but do not prompt for deploy/save actions',
37
+ }),
38
+ };
39
+ async run() {
40
+ const { args, flags } = await this.parse(Test);
41
+ if (!flags['no-brand']) {
42
+ this.log(theme.brand());
43
+ }
44
+ this.log(theme.header('EPHEMERAL VALIDATION'));
45
+ // 1. Load Credentials
46
+ const config = await ConfigManager.load();
47
+ const n8nUrl = config.n8nUrl || process.env.N8N_API_URL;
48
+ const n8nKey = config.n8nKey || process.env.N8N_API_KEY;
49
+ if (!n8nUrl || !n8nKey) {
50
+ this.error('Credentials missing. Configure environment via \'n8m config\'.');
51
+ }
52
+ const client = new N8nClient({ apiUrl: n8nUrl, apiKey: n8nKey });
53
+ // 1a. Fetch Valid Node Types (New)
54
+ let validNodeTypes = [];
55
+ try {
56
+ validNodeTypes = await client.getNodeTypes();
57
+ if (validNodeTypes.length > 0) {
58
+ this.log(theme.success(`✔ Loaded ${validNodeTypes.length} valid node types.`));
59
+ }
60
+ else {
61
+ this.log(theme.warn('⚠ Could not load node types. Validation/Shimming will be limited.'));
62
+ }
63
+ }
64
+ catch (e) {
65
+ this.log(theme.warn(`⚠ Failed to fetch node types: ${e.message}`));
66
+ }
67
+ let createdWorkflowId = null;
68
+ const deployedDefinitions = new Map(); // TempId -> Original JSON (for patching)
69
+ let globalSuccess = false;
70
+ try {
71
+ let workflowData;
72
+ let workflowName = 'Untitled';
73
+ let rootRealTargetId = undefined;
74
+ const dependencyMap = new Map();
75
+ const visited = new Set();
76
+ const resolutionMap = new Map();
77
+ let workflowChoices = [];
78
+ const fetchDependencies = async (id, contextNodeName = 'Unknown') => {
79
+ const realId = resolutionMap.get(id) || id;
80
+ if (visited.has(realId))
81
+ return;
82
+ visited.add(realId);
83
+ let wf;
84
+ // 1. Check local filesystem FIRST (prioritize local over instance)
85
+ let localPath = '';
86
+ const searchDirs = [
87
+ ...(args.workflow ? [path.dirname(args.workflow)] : []),
88
+ path.join(process.cwd(), 'workflows'),
89
+ process.cwd()
90
+ ];
91
+ const sanitized = id.replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase();
92
+ const candidates = [];
93
+ for (const dir of searchDirs) {
94
+ candidates.push(path.join(dir, `${id}.json`));
95
+ candidates.push(path.join(dir, `${sanitized}.json`));
96
+ candidates.push(path.join(dir, `${id.replace(/\s+/g, '-')}.json`));
97
+ }
98
+ for (const candidate of candidates) {
99
+ if (existsSync(candidate)) {
100
+ localPath = candidate;
101
+ break;
102
+ }
103
+ }
104
+ if (localPath) {
105
+ const content = await fs.readFile(localPath, 'utf-8');
106
+ wf = JSON.parse(content);
107
+ }
108
+ else {
109
+ // Case-insensitive search in workflows/
110
+ const workflowsDir = path.join(process.cwd(), 'workflows');
111
+ if (existsSync(workflowsDir)) {
112
+ const files = await fs.readdir(workflowsDir);
113
+ const match = files.find(f => f.toLowerCase() === `${sanitized}.json` || f.toLowerCase() === `${id.toLowerCase()}.json`);
114
+ if (match) {
115
+ localPath = path.join(workflowsDir, match);
116
+ const content = await fs.readFile(localPath, 'utf-8');
117
+ wf = JSON.parse(content);
118
+ }
119
+ else if (id.toUpperCase().includes('SUBWORKFLOW') || id.toUpperCase().includes('ID')) {
120
+ // FUZZY MATCH: If it's a generic placeholder, look for ANY newly created workflow in the same dir
121
+ const dir = args.workflow ? path.dirname(args.workflow) : workflowsDir;
122
+ const dirFiles = await fs.readdir(dir);
123
+ const jsonFiles = dirFiles.filter(f => f.endsWith('.json') && !f.includes(path.basename(args.workflow || '')));
124
+ if (jsonFiles.length === 1) {
125
+ localPath = path.join(dir, jsonFiles[0]);
126
+ const content = await fs.readFile(localPath, 'utf-8');
127
+ wf = JSON.parse(content);
128
+ }
129
+ }
130
+ }
131
+ }
132
+ // 2. If not local, try fetching from instance
133
+ if (!wf) {
134
+ try {
135
+ wf = await client.getWorkflow(realId);
136
+ }
137
+ catch {
138
+ this.log(theme.warn(`Dependency ${theme.value(id)} referenced in node [${contextNodeName}] could not be found.`));
139
+ if (workflowChoices.length === 0) {
140
+ const workflowsList = await client.getWorkflows();
141
+ workflowChoices = workflowsList.map(w => ({ name: w.name, value: w.id }));
142
+ }
143
+ const { resolvedId } = await inquirer.prompt([{
144
+ type: 'select',
145
+ name: 'resolvedId',
146
+ message: `Select replacement for missing dependency ${id}:`,
147
+ choices: workflowChoices,
148
+ pageSize: 15
149
+ }]);
150
+ resolutionMap.set(id, resolvedId);
151
+ await fetchDependencies(resolvedId, contextNodeName);
152
+ return;
153
+ }
154
+ }
155
+ dependencyMap.set(realId, { name: wf.name, data: wf });
156
+ const nodes = wf.nodes || [];
157
+ for (const node of nodes) {
158
+ if (node.type === 'n8n-nodes-base.executeWorkflow') {
159
+ const subId = node.parameters?.workflowId;
160
+ if (subId && typeof subId === 'string' && !subId.startsWith('=')) {
161
+ await fetchDependencies(subId, node.name);
162
+ }
163
+ }
164
+ }
165
+ };
166
+ if (args.workflow) {
167
+ if (!args.workflow.endsWith('.json'))
168
+ this.error('Local JSON path required.');
169
+ const content = await fs.readFile(args.workflow, 'utf-8');
170
+ workflowData = JSON.parse(content);
171
+ workflowName = workflowData.name || 'Untitled';
172
+ if (workflowData.id) {
173
+ rootRealTargetId = workflowData.id;
174
+ }
175
+ const nodes = workflowData.nodes || [];
176
+ for (const node of nodes) {
177
+ if (node.type === 'n8n-nodes-base.executeWorkflow') {
178
+ const subId = node.parameters?.workflowId;
179
+ if (subId && typeof subId === 'string' && !subId.startsWith('=')) {
180
+ await fetchDependencies(subId, node.name);
181
+ }
182
+ }
183
+ }
184
+ }
185
+ else {
186
+ this.log(theme.info('Searching for local and remote workflows...'));
187
+ const localChoices = [];
188
+ const workflowsDir = path.join(process.cwd(), 'workflows');
189
+ const searchDirs = [workflowsDir, process.cwd()];
190
+ for (const dir of searchDirs) {
191
+ if (existsSync(dir)) {
192
+ const files = await fs.readdir(dir);
193
+ for (const file of files) {
194
+ if (file.endsWith('.json')) {
195
+ localChoices.push({
196
+ name: `${theme.value('[LOCAL]')} ${file}`,
197
+ value: { type: 'local', path: path.join(dir, file) }
198
+ });
199
+ }
200
+ }
201
+ }
202
+ }
203
+ const remoteWorkflows = await client.getWorkflows();
204
+ const remoteChoices = remoteWorkflows
205
+ .filter(w => !w.name.startsWith('[TEST'))
206
+ .map(w => ({
207
+ name: `${theme.info('[n8n]')} ${w.name} (${w.id}) ${w.active ? '[Active]' : ''}`,
208
+ value: { type: 'remote', id: w.id }
209
+ }));
210
+ const choices = [
211
+ ...(localChoices.length > 0 ? [new inquirer.Separator('--- Local Files ---'), ...localChoices] : []),
212
+ ...(remoteChoices.length > 0 ? [new inquirer.Separator('--- n8n Instance ---'), ...remoteChoices] : []),
213
+ ];
214
+ if (choices.length === 0)
215
+ this.error('No workflows found locally or on n8n instance.');
216
+ const { selection } = await inquirer.prompt([{
217
+ type: 'select',
218
+ name: 'selection',
219
+ message: 'Select a workflow to test:',
220
+ choices,
221
+ pageSize: 15
222
+ }]);
223
+ if (selection.type === 'local') {
224
+ this.log(theme.agent(`Initializing virtual orchestrator for ${theme.value(selection.path)}`));
225
+ const content = await fs.readFile(selection.path, 'utf-8');
226
+ workflowData = JSON.parse(content);
227
+ workflowName = workflowData.name || 'Untitled';
228
+ const nodes = workflowData.nodes || [];
229
+ for (const node of nodes) {
230
+ if (node.type === 'n8n-nodes-base.executeWorkflow') {
231
+ const subId = node.parameters?.workflowId;
232
+ if (subId && typeof subId === 'string' && !subId.startsWith('=')) {
233
+ await fetchDependencies(subId, node.name);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ else {
239
+ await fetchDependencies(selection.id, 'ROOT');
240
+ const rootRealId = resolutionMap.get(selection.id) || selection.id;
241
+ const rootInfo = dependencyMap.get(rootRealId);
242
+ if (rootInfo) {
243
+ workflowName = rootInfo.name;
244
+ workflowData = rootInfo.data;
245
+ dependencyMap.delete(rootRealId);
246
+ }
247
+ rootRealTargetId = rootRealId;
248
+ }
249
+ }
250
+ // --- 3. Deploy Dependencies Ephemerally ---
251
+ const remappedIds = new Map();
252
+ if (dependencyMap.size > 0) {
253
+ // this.log(theme.subHeader('DEPENDENCY LINKING'));
254
+ // this.log(theme.info(`Found ${dependencyMap.size} dependencies. Deploying ephemeral copies...`));
255
+ for (const [originalId, info] of dependencyMap.entries()) {
256
+ try {
257
+ const depName = `[n8m:test:dep] ${info.name}`;
258
+ // Strict sanitize for n8n API which is picky about extra fields
259
+ // Strict sanitize for n8n API which is picky about extra fields
260
+ // 'meta' often contains templateId/instanceId which are rejected on create
261
+ // 'staticData', 'pinData', and 'tags' can also trigger "additional properties" on strict/older APIs
262
+ const allowedKeys = ['name', 'nodes', 'connections', 'settings'];
263
+ const depData = {};
264
+ for (const key of allowedKeys) {
265
+ if (info.data[key] !== undefined) {
266
+ depData[key] = info.data[key];
267
+ }
268
+ }
269
+ // Ensure settings is clean
270
+ if (depData.settings) {
271
+ // Strictly allow only safe settings
272
+ const safeSettings = ['saveManualExecutions', 'callerPolicy', 'errorWorkflow', 'timezone', 'saveExecutionProgress', 'executionOrder'];
273
+ const cleanSettings = {};
274
+ for (const k of safeSettings) {
275
+ if (depData.settings[k] !== undefined)
276
+ cleanSettings[k] = depData.settings[k];
277
+ }
278
+ depData.settings = cleanSettings;
279
+ }
280
+ // Ensure settings exists
281
+ if (!depData.settings) {
282
+ depData.settings = {};
283
+ }
284
+ // CRITICAL: Sub-workflows often don't have triggers, preventing activation.
285
+ // But n8n requires referenced workflows to be "published" (active) in some contexts,
286
+ // or at least we want them active to be safe.
287
+ // So we INJECT a dummy trigger if one is missing, to satisfy the activation requirement.
288
+ const hasTrigger = (depData.nodes || []).some((n) => n.type.includes('Trigger') && !n.type.includes('executeWorkflowTrigger'));
289
+ if (!hasTrigger) {
290
+ // Use the client's helper to inject a shim trigger
291
+ // We need to cast client to any or ensure the method is public (it is)
292
+ const shimmed = client.injectManualTrigger(depData);
293
+ depData.nodes = shimmed.nodes;
294
+ depData.connections = shimmed.connections;
295
+ }
296
+ depData.name = depName;
297
+ let result;
298
+ try {
299
+ result = await client.createWorkflow(depName, depData);
300
+ }
301
+ catch (createErr) {
302
+ if (createErr.message.includes('additional properties')) {
303
+ this.log(theme.warn(` ⚠ Strict validation error. Retrying with minimal payload...`));
304
+ // Fallback: Drop settings entirely if it fails
305
+ delete depData.settings;
306
+ result = await client.createWorkflow(depName, depData);
307
+ }
308
+ else {
309
+ throw createErr;
310
+ }
311
+ }
312
+ // this.log(theme.success(`✔ Linked dependency: ${theme.value(info.name)} -> ${result.id}`));
313
+ // ACTIVATE the dependency so it can be called
314
+ try {
315
+ await client.activateWorkflow(result.id);
316
+ // this.log(theme.info(` └─ Active`));
317
+ }
318
+ catch (actErr) {
319
+ this.log(theme.warn(` ⚠ Could not activate dependency: ${actErr.message}`));
320
+ }
321
+ remappedIds.set(originalId, result.id);
322
+ // Track for cleanup
323
+ if (!this.createdWorkflowIds)
324
+ this.createdWorkflowIds = [];
325
+ this.createdWorkflowIds.push(result.id);
326
+ // Also handle the "Resolved" ID if it was different
327
+ // (e.g. valid-id in node -> resolved-id in file)
328
+ // The dependencyMap key *is* the ID from the node (or resolution map), so we should be good.
329
+ }
330
+ catch (e) {
331
+ this.log(theme.warn(`⚠ Failed to deploy dependency ${info.name}: ${e.message}`));
332
+ }
333
+ }
334
+ }
335
+ // --- 4. Patch Root Workflow with New IDs ---
336
+ if (remappedIds.size > 0) {
337
+ const patchNodes = (nodes) => {
338
+ for (const node of nodes) {
339
+ if (node.type === 'n8n-nodes-base.executeWorkflow') {
340
+ const subId = node.parameters?.workflowId;
341
+ const realId = resolutionMap.get(subId) || subId;
342
+ if (subId && typeof subId === 'string' && remappedIds.has(realId)) {
343
+ node.parameters.workflowId = remappedIds.get(realId);
344
+ }
345
+ }
346
+ }
347
+ };
348
+ if (workflowData.nodes) {
349
+ patchNodes(workflowData.nodes);
350
+ }
351
+ }
352
+ // --- GLOBAL REPAIR LOOP (Structural + Logical) ---
353
+ // --- AGENTIC WORKFLOW EXECUTION ---
354
+ this.log(theme.subHeader('AGENTIC VALIDATION'));
355
+ this.log(theme.agent("Initializing Agentic Workflow to validate/repair this workflow..."));
356
+ const goal = `Validate and fix the workflow named "${workflowName}"`;
357
+ const initialState = {
358
+ userGoal: goal,
359
+ messages: [],
360
+ validationErrors: [],
361
+ workflowJson: workflowData,
362
+ availableNodeTypes: validNodeTypes
363
+ };
364
+ // We need to route the graph logger to our CLI logger if possible, or just let it print to stdout
365
+ // The graph uses console.log currently, which is fine.
366
+ // Run the graph
367
+ // Note: We need to cast to any because TeamState might have stricter typing than what we pass
368
+ const ephemeralThreadId = `test-${Date.now()}`;
369
+ let result = await runAgenticWorkflow(goal, initialState, ephemeralThreadId);
370
+ // HITL Handling for Test Command
371
+ // Check if paused
372
+ const snapshot = await graph.getState({ configurable: { thread_id: ephemeralThreadId } });
373
+ if (snapshot.next && snapshot.next.length > 0) {
374
+ if (flags.headless) {
375
+ this.log(theme.info("Headless mode active. Auto-resuming..."));
376
+ result = await resumeAgenticWorkflow(ephemeralThreadId);
377
+ }
378
+ else {
379
+ const { resume } = await inquirer.prompt([{
380
+ type: 'confirm',
381
+ name: 'resume',
382
+ message: 'Reviewer passed blueprint. Proceed to QA Execution?',
383
+ default: true
384
+ }]);
385
+ if (resume) {
386
+ result = await resumeAgenticWorkflow(ephemeralThreadId);
387
+ }
388
+ else {
389
+ this.log(theme.warn("Test aborted by user."));
390
+ return;
391
+ }
392
+ }
393
+ }
394
+ if (result.validationStatus === 'passed') {
395
+ globalSuccess = true;
396
+ // this.log(theme.success("Agentic Validation Passed!"));
397
+ if (result.workflowJson) {
398
+ // Extract the fixed/validated workflows
399
+ // The graph result uses the same structure as Engineer: { workflows: [...] } or just workflowJson object
400
+ let fixedWorkflow = result.workflowJson;
401
+ // If it's wrapped in a workflows array (Multi-workflow support), take the first one for now
402
+ if (result.workflowJson.workflows && Array.isArray(result.workflowJson.workflows)) {
403
+ fixedWorkflow = result.workflowJson.workflows[0];
404
+ }
405
+ const finalName = fixedWorkflow.name || workflowName;
406
+ deployedDefinitions.set('agentic-result', {
407
+ name: finalName,
408
+ data: fixedWorkflow,
409
+ type: 'root',
410
+ realId: rootRealTargetId
411
+ });
412
+ }
413
+ }
414
+ else {
415
+ this.log(theme.fail("Agentic Validation Failed."));
416
+ if (result.validationErrors && result.validationErrors.length > 0) {
417
+ result.validationErrors.forEach((err) => this.log(theme.error(`Error: ${err}`)));
418
+ }
419
+ }
420
+ }
421
+ catch (error) {
422
+ const errMsg = this.cleanErrorMsg(error.message);
423
+ this.log(theme.fail(`Validation Failed`));
424
+ this.log(theme.error(errMsg));
425
+ process.exitCode = 1;
426
+ if (flags['keep-on-fail'] && createdWorkflowId) {
427
+ this.log(theme.warn(`PRESERVATION ACTIVE: Workflow ${createdWorkflowId} persists.`));
428
+ createdWorkflowId = null;
429
+ }
430
+ }
431
+ finally {
432
+ if (deployedDefinitions.size > 0 && globalSuccess) {
433
+ if (flags['validate-only']) {
434
+ if (args.workflow && args.workflow.endsWith('.json')) {
435
+ const rootDef = Array.from(deployedDefinitions.values()).find(d => d.type === 'root');
436
+ if (rootDef) {
437
+ const cleanData = this.sanitizeWorkflow(this.stripShim(rootDef.data));
438
+ cleanData.name = rootDef.name;
439
+ try {
440
+ await fs.writeFile(args.workflow, JSON.stringify(cleanData, null, 2));
441
+ this.log(theme.muted(`[Repair-Sync] Updated local file with latest repairs.`));
442
+ }
443
+ catch (e) {
444
+ this.warn(`Failed to sync repairs: ${e.message}`);
445
+ }
446
+ }
447
+ }
448
+ }
449
+ else {
450
+ await this.handlePostTestActions(deployedDefinitions, args.workflow, client);
451
+ }
452
+ }
453
+ const allIds = [createdWorkflowId, ...(this.createdWorkflowIds || [])].filter(Boolean);
454
+ const uniqueIds = [...new Set(allIds)];
455
+ if (uniqueIds.length > 0) {
456
+ this.log(theme.info(`Purging ${uniqueIds.length} temporary assets...`));
457
+ for (const wid of uniqueIds) {
458
+ try {
459
+ if (wid)
460
+ await client.deleteWorkflow(wid);
461
+ }
462
+ catch { /* intentionally empty */ }
463
+ }
464
+ this.log(theme.done('Environment clean.'));
465
+ }
466
+ process.exit(0);
467
+ }
468
+ }
469
+ /**
470
+ * Extract clean error message from n8n API responses
471
+ */
472
+ cleanErrorMsg(errMsg) {
473
+ if (!errMsg || typeof errMsg !== 'string')
474
+ return String(errMsg || 'Unknown Error');
475
+ try {
476
+ const jsonMatch = errMsg.match(/\{.*\}/);
477
+ if (jsonMatch) {
478
+ const errorObj = JSON.parse(jsonMatch[0]);
479
+ if (errorObj.message) {
480
+ return errorObj.message;
481
+ }
482
+ }
483
+ }
484
+ catch { /* intentionally empty */ }
485
+ return errMsg;
486
+ }
487
+ /**
488
+ * Normalize error messages to catch "similar" errors (masking IDs/numbers)
489
+ */
490
+ normalizeError(msg) {
491
+ const normalized = msg.toLowerCase();
492
+ // Group all unrecognized node type errors
493
+ if (normalized.includes('unrecognized node type')) {
494
+ return 'unrecognized node type';
495
+ }
496
+ return normalized
497
+ .replace(/\b[a-f0-9-]{36}\b/g, 'ID')
498
+ .replace(/\b[a-f0-9]{24}\b/g, 'ID')
499
+ .replace(/\b\d+\b/g, 'N')
500
+ .replace(/\s+/g, ' ')
501
+ .trim();
502
+ }
503
+ sanitizeWorkflow(data) {
504
+ // n8n API is extremely picky during UPDATE/CREATE.
505
+ // properties like 'meta', 'pinData', 'tags', and 'versionId' often cause 400 Bad Request
506
+ // 'request/body must NOT have additional properties'.
507
+ // We only send the core structure.
508
+ const allowedKeys = ['name', 'nodes', 'connections', 'settings'];
509
+ const sanitized = {
510
+ settings: data.settings || {}
511
+ };
512
+ for (const key of allowedKeys) {
513
+ if (data[key] !== undefined && key !== 'settings') {
514
+ sanitized[key] = data[key];
515
+ }
516
+ }
517
+ return sanitized;
518
+ }
519
+ stripShim(workflowData) {
520
+ if (!workflowData.nodes)
521
+ return workflowData;
522
+ const nodes = workflowData.nodes.filter((n) => n.name !== 'N8M_Shim_Webhook' &&
523
+ n.name !== 'Shim_Flattener');
524
+ const connections = {};
525
+ for (const [nodeName, conns] of Object.entries(workflowData.connections || {})) {
526
+ if (nodeName === 'N8M_Shim_Webhook' || nodeName === 'Shim_Flattener')
527
+ continue;
528
+ connections[nodeName] = conns;
529
+ }
530
+ return { ...workflowData, nodes, connections };
531
+ }
532
+ async saveWorkflows(deployedDefinitions, originalPath) {
533
+ if (deployedDefinitions.size === 0)
534
+ return;
535
+ const { save } = await inquirer.prompt([{
536
+ type: 'confirm',
537
+ name: 'save',
538
+ message: 'Test passed. Save workflows locally?',
539
+ default: true
540
+ }]);
541
+ if (!save)
542
+ return;
543
+ for (const [, def] of deployedDefinitions.entries()) {
544
+ const cleanData = this.sanitizeWorkflow(this.stripShim(def.data));
545
+ cleanData.name = def.name;
546
+ const targetPath = originalPath && def.type === 'root' ? originalPath : path.join(process.cwd(), 'workflows', `${def.name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json`);
547
+ const { confirmPath } = await inquirer.prompt([{
548
+ type: 'input',
549
+ name: 'confirmPath',
550
+ message: `Save '${def.name}' to:`,
551
+ default: targetPath
552
+ }]);
553
+ try {
554
+ await fs.mkdir(path.dirname(confirmPath), { recursive: true });
555
+ await fs.writeFile(confirmPath, JSON.stringify(cleanData, null, 2));
556
+ this.log(theme.success(`Saved to ${confirmPath}`));
557
+ }
558
+ catch (e) {
559
+ this.log(theme.fail(`Failed to save: ${e.message}`));
560
+ }
561
+ }
562
+ }
563
+ async handlePostTestActions(deployedDefinitions, originalPath, client) {
564
+ if (deployedDefinitions.size === 0)
565
+ return;
566
+ const { action } = await inquirer.prompt([{
567
+ type: 'confirm',
568
+ name: 'action',
569
+ message: 'Test completed. Deploy changes to instance? (Y = Deploy, n = Save to file)',
570
+ default: true
571
+ }]);
572
+ if (action) {
573
+ await this.deployWorkflows(deployedDefinitions, client);
574
+ }
575
+ else {
576
+ await this.saveWorkflows(deployedDefinitions, originalPath);
577
+ }
578
+ }
579
+ async deployWorkflows(deployedDefinitions, client) {
580
+ for (const [, def] of deployedDefinitions.entries()) {
581
+ const cleanData = this.sanitizeWorkflow(this.stripShim(def.data));
582
+ cleanData.name = def.name;
583
+ if (def.realId) {
584
+ try {
585
+ this.log(theme.agent(`Deploying (Overwriting) ${theme.value(def.name)} [${def.realId}]...`));
586
+ await client.updateWorkflow(def.realId, cleanData);
587
+ this.log(theme.success(`✔ Updated ${def.name}`));
588
+ }
589
+ catch (e) {
590
+ const msg = e.message;
591
+ if (msg.includes('trigger') || msg.includes('activated')) {
592
+ try {
593
+ await client.deactivateWorkflow(def.realId);
594
+ await client.updateWorkflow(def.realId, cleanData);
595
+ this.log(theme.success(`✔ Updated ${def.name} (Deactivated)`));
596
+ }
597
+ catch (retryErr) {
598
+ this.log(theme.fail(`Failed to update ${def.name}: ${retryErr.message}`));
599
+ }
600
+ }
601
+ else {
602
+ this.log(theme.fail(`Failed to update ${def.name}: ${msg}`));
603
+ }
604
+ }
605
+ }
606
+ else {
607
+ try {
608
+ this.log(theme.agent(`Deploying (Creating New) ${theme.value(def.name)}...`));
609
+ const result = await client.createWorkflow(def.name, cleanData);
610
+ this.log(theme.success(`✔ Created ${def.name} [ID: ${result.id}]`));
611
+ this.log(`${theme.label('Link')} ${theme.secondary(client.getWorkflowLink(result.id))}`);
612
+ }
613
+ catch (e) {
614
+ this.log(theme.fail(`Failed to create ${def.name}: ${e.message}`));
615
+ }
616
+ }
617
+ }
618
+ }
619
+ }
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
@@ -0,0 +1,51 @@
1
+ export interface GenerateOptions {
2
+ model?: string;
3
+ temperature?: number;
4
+ }
5
+ export declare class AIService {
6
+ private static instance;
7
+ private client;
8
+ private model;
9
+ private constructor();
10
+ static getInstance(): AIService;
11
+ /**
12
+ * Core generation method — works with any OpenAI-compatible API
13
+ */
14
+ generateContent(prompt: string, options?: GenerateOptions): Promise<string>;
15
+ /**
16
+ * Generate an n8n workflow from a description
17
+ */
18
+ generateWorkflow(description: string): Promise<any>;
19
+ /**
20
+ * Generate a Workflow Specification from a description
21
+ */
22
+ generateSpec(description: string): Promise<any>;
23
+ /**
24
+ * Refine a Specification based on user feedback
25
+ */
26
+ refineSpec(spec: any, feedback: string): Promise<any>;
27
+ /**
28
+ * Generate workflow JSONs from an approved Specification
29
+ */
30
+ generateWorkflowFromSpec(spec: any): Promise<any>;
31
+ /**
32
+ * Generate mock data for a workflow execution
33
+ */
34
+ generateMockData(context: string, previousFailures?: string[]): Promise<any>;
35
+ /**
36
+ * Diagnostic Repair: Fix a workflow based on execution error
37
+ */
38
+ generateWorkflowFix(workflowJson: any, errorContext: string, model?: string, _useSearch?: boolean, validNodeTypes?: string[]): Promise<any>;
39
+ /**
40
+ * Auto-correct common n8n node type hallucinations
41
+ */
42
+ private fixHallucinatedNodes;
43
+ /**
44
+ * Force-fix connection structure to prevent "object is not iterable" errors
45
+ */
46
+ private fixN8nConnections;
47
+ /**
48
+ * Validate against real node types and shim unknown ones
49
+ */
50
+ validateAndShim(workflow: any, validNodeTypes?: string[], explicitlyInvalid?: string[]): any;
51
+ }