@lhi/n8m 0.1.2 → 0.1.3

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 CHANGED
@@ -235,6 +235,14 @@ npm run dev
235
235
 
236
236
  ---
237
237
 
238
+ ## Sponsors
239
+
240
+ ### Partially Sponsored By
241
+
242
+ [The Daily Caller](https://dailycaller.com)
243
+
244
+ ---
245
+
238
246
  ## Roadmap
239
247
 
240
248
  - [x] Agentic graph (Architect → Engineer → QA)
package/bin/dev.js CHANGED
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env node --loader ts-node/esm --no-warnings=ExperimentalWarning
2
2
 
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+ import { config } from 'dotenv';
6
+
7
+ // Load .env from the package root regardless of the user's cwd
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ config({ path: join(__dirname, '..', '.env') });
10
+
3
11
  import {execute} from '@oclif/core'
4
12
 
5
13
  await execute({development: true, dir: import.meta.url})
package/bin/run.js CHANGED
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import 'dotenv/config';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ import { config } from 'dotenv';
5
+
6
+ // Load .env from the package root regardless of the user's cwd
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ config({ path: join(__dirname, '..', '.env') });
3
9
 
4
10
  import {execute} from '@oclif/core'
5
11
 
@@ -129,9 +129,21 @@ export declare const graph: import("@langchain/langgraph").CompiledStateGraph<{
129
129
  collaborationLog: import("@langchain/langgraph").BinaryOperatorAggregate<string[], string[]>;
130
130
  }, import("@langchain/langgraph").StateDefinition, {
131
131
  architect: {
132
- spec: any;
133
- strategies: any[];
134
- needsClarification: any;
132
+ spec: import("../services/ai.service.js").WorkflowSpec;
133
+ strategies: {
134
+ strategyName: string;
135
+ aiModel: string;
136
+ suggestedName: string;
137
+ description: string;
138
+ nodes: {
139
+ type: string;
140
+ purpose: string;
141
+ config?: any;
142
+ }[];
143
+ questions?: string[];
144
+ aiProvider?: string;
145
+ }[];
146
+ needsClarification: boolean | undefined;
135
147
  collaborationLog: string[];
136
148
  };
137
149
  engineer: {
@@ -141,8 +153,8 @@ export declare const graph: import("@langchain/langgraph").CompiledStateGraph<{
141
153
  workflowJson?: undefined;
142
154
  candidates?: undefined;
143
155
  } | {
144
- workflowJson: any;
145
156
  candidates: any[];
157
+ workflowJson?: undefined;
146
158
  };
147
159
  reviewer: import("@langchain/langgraph").UpdateType<{
148
160
  userGoal: {
@@ -318,9 +330,21 @@ export declare const runAgenticWorkflow: (goal: string, initialState?: Partial<t
318
330
  */
319
331
  export declare const runAgenticWorkflowStream: (goal: string, threadId?: string) => Promise<import("@langchain/core/utils/stream").IterableReadableStream<{
320
332
  architect?: {
321
- spec: any;
322
- strategies: any[];
323
- needsClarification: any;
333
+ spec: import("../services/ai.service.js").WorkflowSpec;
334
+ strategies: {
335
+ strategyName: string;
336
+ aiModel: string;
337
+ suggestedName: string;
338
+ description: string;
339
+ nodes: {
340
+ type: string;
341
+ purpose: string;
342
+ config?: any;
343
+ }[];
344
+ questions?: string[];
345
+ aiProvider?: string;
346
+ }[];
347
+ needsClarification: boolean | undefined;
324
348
  collaborationLog: string[];
325
349
  } | undefined;
326
350
  engineer?: {
@@ -330,8 +354,8 @@ export declare const runAgenticWorkflowStream: (goal: string, threadId?: string)
330
354
  workflowJson?: undefined;
331
355
  candidates?: undefined;
332
356
  } | {
333
- workflowJson: any;
334
357
  candidates: any[];
358
+ workflowJson?: undefined;
335
359
  } | undefined;
336
360
  reviewer?: import("@langchain/langgraph").UpdateType<{
337
361
  userGoal: {
@@ -1,7 +1,19 @@
1
1
  import { TeamState } from "../state.js";
2
2
  export declare const architectNode: (state: typeof TeamState.State) => Promise<{
3
- spec: any;
4
- strategies: any[];
5
- needsClarification: any;
3
+ spec: import("../../services/ai.service.js").WorkflowSpec;
4
+ strategies: {
5
+ strategyName: string;
6
+ aiModel: string;
7
+ suggestedName: string;
8
+ description: string;
9
+ nodes: {
10
+ type: string;
11
+ purpose: string;
12
+ config?: any;
13
+ }[];
14
+ questions?: string[];
15
+ aiProvider?: string;
16
+ }[];
17
+ needsClarification: boolean | undefined;
6
18
  collaborationLog: string[];
7
19
  }>;
@@ -33,9 +33,18 @@ export const architectNode = async (state) => {
33
33
  // Multi-agent collaboration: generate an alternative strategy in parallel with the primary.
34
34
  // Both are handed off to separate Engineer agents that run concurrently.
35
35
  const alternativeSpec = await aiService.generateAlternativeSpec(state.userGoal, spec);
36
+ const alternativeModel = aiService.getAlternativeModel();
36
37
  const strategies = [
37
- { ...spec, strategyName: "Primary Strategy" },
38
- { ...alternativeSpec, strategyName: "Alternative Strategy" },
38
+ {
39
+ ...spec,
40
+ strategyName: "Primary Strategy",
41
+ aiModel: aiService.getDefaultModel()
42
+ },
43
+ {
44
+ ...alternativeSpec,
45
+ strategyName: "Alternative Strategy",
46
+ aiModel: alternativeModel
47
+ },
39
48
  ];
40
49
  const logEntry = `Architect: Generated 2 strategies — "${strategies[0].suggestedName}" (primary) and "${strategies[1].suggestedName}" (alternative)`;
41
50
  console.log(`[Architect] ${logEntry}`);
@@ -6,6 +6,6 @@ export declare const engineerNode: (state: typeof TeamState.State) => Promise<{
6
6
  workflowJson?: undefined;
7
7
  candidates?: undefined;
8
8
  } | {
9
- workflowJson: any;
10
9
  candidates: any[];
10
+ workflowJson?: undefined;
11
11
  }>;
@@ -1,5 +1,6 @@
1
1
  import { AIService } from "../../services/ai.service.js";
2
2
  import { NodeDefinitionsService } from "../../services/node-definitions.service.js";
3
+ import { jsonrepair } from "jsonrepair";
3
4
  export const engineerNode = async (state) => {
4
5
  const aiService = AIService.getInstance();
5
6
  // RAG: Load and Search Node Definitions
@@ -9,8 +10,9 @@ export const engineerNode = async (state) => {
9
10
  const queryText = (state.userGoal + (state.spec ? ` ${state.spec.suggestedName} ${state.spec.description}` : "")).replace(/\n/g, " ");
10
11
  // Search for relevant nodes (limit 8 to save context)
11
12
  const relevantDefs = nodeService.search(queryText, 8);
12
- const ragContext = relevantDefs.length > 0
13
- ? `\n\n[AVAILABLE NODE SCHEMAS - USE THESE EXACT PARAMETERS]\n${nodeService.formatForLLM(relevantDefs)}`
13
+ const staticRef = nodeService.getStaticReference();
14
+ const ragContext = (relevantDefs.length > 0 || staticRef)
15
+ ? `\n\n[N8N NODE REFERENCE GUIDE]\n${staticRef}\n\n[AVAILABLE NODE SCHEMAS - USE THESE EXACT PARAMETERS]\n${nodeService.formatForLLM(relevantDefs)}`
14
16
  : "";
15
17
  if (relevantDefs.length > 0) {
16
18
  console.log(`[Engineer] RAG: Found ${relevantDefs.length} relevant node schemas.`);
@@ -22,7 +24,8 @@ export const engineerNode = async (state) => {
22
24
  // We pass the entire list of errors as context
23
25
  const errorContext = state.validationErrors.join('\n');
24
26
  // Use the robust fix logic from AIService
25
- const fixedWorkflow = await aiService.generateWorkflowFix(state.workflowJson, errorContext, undefined, false, state.availableNodeTypes || []);
27
+ const fixedWorkflow = await aiService.generateWorkflowFix(state.workflowJson, errorContext, state.spec?.aiModel, // Pass model if available
28
+ false, state.availableNodeTypes || []);
26
29
  return {
27
30
  workflowJson: fixedWorkflow,
28
31
  // validationErrors will be overwritten by next QA run
@@ -74,23 +77,27 @@ export const engineerNode = async (state) => {
74
77
  Output ONLY valid JSON. No commentary. No markdown.
75
78
  `;
76
79
  // Using AIService just for the LLM call to keep auth logic dry
77
- const response = await aiService.generateContent(prompt);
80
+ const response = await aiService.generateContent(prompt, {
81
+ provider: state.spec.aiProvider,
82
+ model: state.spec.aiModel
83
+ });
78
84
  let cleanJson = response || "{}";
79
85
  cleanJson = cleanJson.replace(/```json\n?|\n?```/g, "").trim();
80
86
  let result;
81
87
  try {
82
- result = JSON.parse(cleanJson);
88
+ result = JSON.parse(jsonrepair(cleanJson));
83
89
  }
84
- catch (e) {
85
- console.error("Failed to parse workflow JSON from spec", e);
90
+ catch (e2) {
91
+ console.error("Failed to parse workflow JSON from spec", e2);
86
92
  throw new Error("AI generated invalid JSON for workflow from spec");
87
93
  }
88
94
  if (result.workflows && Array.isArray(result.workflows)) {
89
95
  result.workflows = result.workflows.map((wf) => fixHallucinatedNodes(wf));
90
96
  }
91
97
  return {
92
- workflowJson: result,
93
- // For parallel execution, push to candidates
98
+ // Only push to candidates — the Supervisor sets workflowJson after fan-in.
99
+ // Writing workflowJson here would cause a LastValue conflict when two
100
+ // Engineers run in parallel via Send().
94
101
  candidates: [result],
95
102
  };
96
103
  }
@@ -10,8 +10,9 @@ export const qaNode = async (state) => {
10
10
  }
11
11
  // 1. Load Credentials
12
12
  const config = await ConfigManager.load();
13
- const n8nUrl = config.n8nUrl || process.env.N8N_API_URL;
14
- const n8nKey = config.n8nKey || process.env.N8N_API_KEY;
13
+ // Env vars take priority over stored config so a fresh key in .env is always used
14
+ const n8nUrl = process.env.N8N_API_URL || config.n8nUrl;
15
+ const n8nKey = process.env.N8N_API_KEY || config.n8nKey;
15
16
  if (!n8nUrl || !n8nKey) {
16
17
  throw new Error('Credentials missing. Configure environment via \'n8m config\'.');
17
18
  }
@@ -28,10 +29,21 @@ export const qaNode = async (state) => {
28
29
  targetWorkflow = workflowJson.workflows[0];
29
30
  }
30
31
  const workflowName = targetWorkflow.name || 'Agentic_Test_Workflow';
32
+ // Drop timezone — sanitizeSettings in N8nClient strips it unconditionally
33
+ const rawSettings = { ...(targetWorkflow.settings || {}) };
34
+ delete rawSettings.timezone;
35
+ // Strip credentials from all nodes — n8n 2.x refuses to activate ("publish")
36
+ // a workflow that references credentials that don't exist on the instance.
37
+ // Structural validation can still run; only live execution of credentialed
38
+ // nodes will be skipped/fail, which is expected for an ephemeral test.
39
+ const strippedNodes = targetWorkflow.nodes.map((node) => {
40
+ const { credentials: _creds, ...rest } = node;
41
+ return rest;
42
+ });
31
43
  const rootPayload = {
32
- nodes: targetWorkflow.nodes,
44
+ nodes: strippedNodes,
33
45
  connections: targetWorkflow.connections,
34
- settings: targetWorkflow.settings || {},
46
+ settings: rawSettings,
35
47
  staticData: targetWorkflow.staticData || {},
36
48
  name: `[n8m:test] ${workflowName}`,
37
49
  };
@@ -66,6 +66,8 @@ export const reviewerNode = async (state) => {
66
66
  // Triggers usually have no input.
67
67
  // We use a broader check: any node with "trigger" or "webhook" in the name, plus generic start types.
68
68
  const isTrigger = (type) => {
69
+ if (!type)
70
+ return false;
69
71
  const lower = type.toLowerCase();
70
72
  return lower.includes('trigger') ||
71
73
  lower.includes('webhook') ||
@@ -79,9 +81,9 @@ export const reviewerNode = async (state) => {
79
81
  // Sticky notes and Merge nodes can be tricky, but generally Merge needs input.
80
82
  if (!node.type.includes('StickyNote')) {
81
83
  // Double check for "On Execution" (custom trigger name sometimes used)
82
- if (!node.name.toLowerCase().includes('trigger') && !node.name.toLowerCase().includes('webhook')) {
83
- console.log(theme.warn(`[Reviewer] Validated disconnection: Node "${node.name}" has no incoming connections.`));
84
- validationErrors.push(`Node "${node.name}" (${node.type}) is disconnected (orphaned). Connect it or remove it.`);
84
+ if (!node.name || (!node.name.toLowerCase().includes('trigger') && !node.name.toLowerCase().includes('webhook'))) {
85
+ console.log(theme.warn(`[Reviewer] Validated disconnection: Node "${node.name || 'Unnamed'}" has no incoming connections.`));
86
+ validationErrors.push(`Node "${node.name || 'Unnamed'}" (${node.type || 'unknown type'}) is disconnected (orphaned). Connect it or remove it.`);
85
87
  }
86
88
  }
87
89
  }
@@ -0,0 +1,213 @@
1
+ [
2
+ {
3
+ "name": "n8n-nodes-base.start",
4
+ "displayName": "Manual Trigger",
5
+ "description": "The starting point for manual execution.",
6
+ "properties": []
7
+ },
8
+ {
9
+ "name": "n8n-nodes-base.httpRequest",
10
+ "displayName": "HTTP Request",
11
+ "description": "Send HTTP requests to any API",
12
+ "properties": [
13
+ {
14
+ "name": "method",
15
+ "displayName": "Method",
16
+ "type": "options",
17
+ "default": "GET",
18
+ "options": [
19
+ { "name": "GET", "value": "GET" },
20
+ { "name": "POST", "value": "POST" },
21
+ { "name": "PUT", "value": "PUT" },
22
+ { "name": "DELETE", "value": "DELETE" }
23
+ ]
24
+ },
25
+ {
26
+ "name": "url",
27
+ "displayName": "URL",
28
+ "type": "string",
29
+ "default": "",
30
+ "description": "The URL to send the request to"
31
+ },
32
+ {
33
+ "name": "authentication",
34
+ "displayName": "Authentication",
35
+ "type": "options",
36
+ "default": "none",
37
+ "options": [
38
+ { "name": "None", "value": "none" },
39
+ { "name": "Predefined Credential Type", "value": "predefinedCredentialType" }
40
+ ]
41
+ },
42
+ {
43
+ "name": "sendBody",
44
+ "displayName": "Send Body",
45
+ "type": "boolean",
46
+ "default": false
47
+ },
48
+ {
49
+ "name": "body",
50
+ "displayName": "Body",
51
+ "type": "json",
52
+ "default": ""
53
+ }
54
+ ]
55
+ },
56
+ {
57
+ "name": "n8n-nodes-base.slack",
58
+ "displayName": "Slack",
59
+ "description": "Interact with Slack",
60
+ "properties": [
61
+ {
62
+ "name": "resource",
63
+ "displayName": "Resource",
64
+ "type": "options",
65
+ "default": "message",
66
+ "options": [
67
+ { "name": "Message", "value": "message" },
68
+ { "name": "Channel", "value": "channel" }
69
+ ]
70
+ },
71
+ {
72
+ "name": "operation",
73
+ "displayName": "Operation",
74
+ "type": "options",
75
+ "default": "post",
76
+ "options": [
77
+ { "name": "Post", "value": "post" },
78
+ { "name": "Update", "value": "update" }
79
+ ]
80
+ },
81
+ {
82
+ "name": "channel",
83
+ "displayName": "Channel",
84
+ "type": "string",
85
+ "default": ""
86
+ },
87
+ {
88
+ "name": "text",
89
+ "displayName": "Text",
90
+ "type": "string",
91
+ "default": ""
92
+ }
93
+ ]
94
+ },
95
+ {
96
+ "name": "n8n-nodes-base.googleSheets",
97
+ "displayName": "Google Sheets",
98
+ "description": "Interact with Google Sheets",
99
+ "properties": [
100
+ {
101
+ "name": "resource",
102
+ "displayName": "Resource",
103
+ "type": "options",
104
+ "default": "sheet",
105
+ "options": [
106
+ { "name": "Sheet", "value": "sheet" }
107
+ ]
108
+ },
109
+ {
110
+ "name": "operation",
111
+ "displayName": "Operation",
112
+ "type": "options",
113
+ "default": "append",
114
+ "options": [
115
+ { "name": "Append", "value": "append" },
116
+ { "name": "Read", "value": "read" },
117
+ { "name": "Update", "value": "update" }
118
+ ]
119
+ },
120
+ {
121
+ "name": "sheetId",
122
+ "displayName": "Sheet ID",
123
+ "type": "string",
124
+ "default": ""
125
+ },
126
+ {
127
+ "name": "range",
128
+ "displayName": "Range",
129
+ "type": "string",
130
+ "default": "Sheet1!A:Z"
131
+ }
132
+ ]
133
+ },
134
+ {
135
+ "name": "n8n-nodes-base.if",
136
+ "displayName": "IF",
137
+ "description": "Conditional logic: True/False",
138
+ "properties": [
139
+ {
140
+ "name": "conditions",
141
+ "displayName": "Conditions",
142
+ "type": "json",
143
+ "default": {}
144
+ }
145
+ ]
146
+ },
147
+ {
148
+ "name": "n8n-nodes-base.set",
149
+ "displayName": "Set",
150
+ "description": "Set variables or values",
151
+ "properties": [
152
+ {
153
+ "name": "values",
154
+ "displayName": "Values",
155
+ "type": "fixedCollection",
156
+ "default": {}
157
+ }
158
+ ]
159
+ },
160
+ {
161
+ "name": "n8n-nodes-base.webhook",
162
+ "displayName": "Webhook",
163
+ "description": "Receive HTTP requests",
164
+ "properties": [
165
+ {
166
+ "name": "httpMethod",
167
+ "displayName": "HTTP Method",
168
+ "type": "options",
169
+ "default": "GET",
170
+ "options": [
171
+ { "name": "GET", "value": "GET" },
172
+ { "name": "POST", "value": "POST" }
173
+ ]
174
+ },
175
+ {
176
+ "name": "path",
177
+ "displayName": "Path",
178
+ "type": "string",
179
+ "default": ""
180
+ }
181
+ ]
182
+ },
183
+ {
184
+ "name": "n8n-nodes-base.code",
185
+ "displayName": "Code",
186
+ "description": "Run JavaScript/TypeScript",
187
+ "properties": [
188
+ {
189
+ "name": "jsCode",
190
+ "displayName": "JS Code",
191
+ "type": "string",
192
+ "default": "// Your code here\nreturn items;"
193
+ }
194
+ ]
195
+ },
196
+ {
197
+ "name": "n8n-nodes-base.merge",
198
+ "displayName": "Merge",
199
+ "description": "Merge data from multiple inputs",
200
+ "properties": [
201
+ {
202
+ "name": "mode",
203
+ "displayName": "Mode",
204
+ "type": "options",
205
+ "default": "append",
206
+ "options": [
207
+ { "name": "Append", "value": "append" },
208
+ { "name": "Merge by Index", "value": "mergeByIndex" }
209
+ ]
210
+ }
211
+ ]
212
+ }
213
+ ]
@@ -1,64 +1,47 @@
1
1
  export interface GenerateOptions {
2
2
  model?: string;
3
+ provider?: string;
3
4
  temperature?: number;
4
5
  }
6
+ export interface WorkflowSpec {
7
+ suggestedName: string;
8
+ description: string;
9
+ nodes: {
10
+ type: string;
11
+ purpose: string;
12
+ config?: any;
13
+ }[];
14
+ questions?: string[];
15
+ strategyName?: string;
16
+ aiModel?: string;
17
+ aiProvider?: string;
18
+ }
19
+ export declare const PROVIDER_PRESETS: Record<string, {
20
+ baseURL?: string;
21
+ defaultModel: string;
22
+ models: string[];
23
+ }>;
5
24
  export declare class AIService {
6
25
  private static instance;
7
- private client;
8
- private model;
26
+ private clients;
27
+ private defaultProvider;
28
+ private defaultModel;
29
+ private apiKey;
30
+ private baseURL?;
9
31
  private constructor();
32
+ private getClient;
10
33
  static getInstance(): AIService;
11
- /**
12
- * Core generation method — works with any OpenAI-compatible API
13
- */
34
+ private callAnthropicNative;
14
35
  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
- * Generate an alternative workflow specification with a different approach to the same goal.
49
- * Used by the Architect node to produce a second strategy for parallel Engineer execution.
50
- */
51
- generateAlternativeSpec(goal: string, primarySpec: any): Promise<any>;
52
- /**
53
- * Evaluate multiple workflow candidates and select the best one for the given goal.
54
- * Used by the Supervisor node to choose between parallel Engineer outputs.
55
- */
36
+ getAlternativeModel(): string;
37
+ getDefaultModel(): string;
38
+ getDefaultProvider(): string;
39
+ generateSpec(goal: string): Promise<WorkflowSpec>;
40
+ generateAlternativeSpec(goal: string, primarySpec: WorkflowSpec): Promise<WorkflowSpec>;
41
+ generateWorkflowFix(workflow: any, error: string, model?: string, stream?: boolean, validNodeTypes?: string[]): Promise<any>;
42
+ generateMockData(context: string): Promise<any>;
56
43
  evaluateCandidates(goal: string, candidates: any[]): Promise<{
57
44
  selectedIndex: number;
58
45
  reason: string;
59
46
  }>;
60
- /**
61
- * Validate against real node types and shim unknown ones
62
- */
63
- validateAndShim(workflow: any, validNodeTypes?: string[], explicitlyInvalid?: string[]): any;
64
47
  }