@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,25 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ export class ConfigManager {
5
+ static configDir = path.join(os.homedir(), '.n8m');
6
+ static configFile = path.join(os.homedir(), '.n8m', 'config.json');
7
+ static async load() {
8
+ try {
9
+ const data = await fs.readFile(this.configFile, 'utf-8');
10
+ return JSON.parse(data);
11
+ }
12
+ catch {
13
+ return {};
14
+ }
15
+ }
16
+ static async save(config) {
17
+ await fs.mkdir(this.configDir, { recursive: true });
18
+ const existing = await this.load();
19
+ const merged = { ...existing, ...config };
20
+ await fs.writeFile(this.configFile, JSON.stringify(merged, null, 2));
21
+ }
22
+ static async clear() {
23
+ await fs.writeFile(this.configFile, JSON.stringify({}, null, 2));
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ export declare function promptMultiline(message?: string): Promise<string>;
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, render, useApp } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import { MultilineInput } from 'ink-multiline-input';
6
+ const SmartPromptElement = ({ onDone, title }) => {
7
+ const [mode, setMode] = useState('single');
8
+ const [value, setValue] = useState('');
9
+ const [multiValue, setMultiValue] = useState('');
10
+ const { exit } = useApp();
11
+ const handleSingleSubmit = (text) => {
12
+ if (text.trim() === '```') {
13
+ setMode('multi');
14
+ }
15
+ else if (text.trim().length > 0) {
16
+ onDone(text.trim());
17
+ exit();
18
+ }
19
+ // If empty, do nothing (stays open)
20
+ };
21
+ const handleMultiSubmit = (text) => {
22
+ // End with ``` to submit
23
+ if (text.trim().endsWith('```')) {
24
+ const finalValue = text.trim();
25
+ const cleaned = finalValue.slice(0, -3).trim();
26
+ if (cleaned.length > 0) {
27
+ onDone(cleaned);
28
+ exit();
29
+ return;
30
+ }
31
+ }
32
+ setMultiValue(text + '\n');
33
+ };
34
+ if (mode === 'single') {
35
+ return (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "? " }), _jsxs(Text, { bold: true, children: [title || 'Describe the workflow (use ``` for multiline): ', " "] }), _jsx(TextInput, { value: value, onChange: setValue, onSubmit: handleSingleSubmit })] }));
36
+ }
37
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2714 " }), _jsxs(Text, { bold: true, children: [title || 'Describe the workflow (use ``` for multiline): ', " "] }), _jsx(Text, { color: "cyan", children: "```" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "cyan", children: "Entering multiline mode. Type ``` on a new line to finish." }) }), _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "gray", children: "\u2503 " }), _jsx(Box, { flexGrow: 1, children: _jsx(MultilineInput, { value: multiValue, onChange: setMultiValue, onSubmit: handleMultiSubmit, rows: 5, maxRows: 15, keyBindings: {
38
+ submit: (key) => key.return && !key.shift,
39
+ newline: (key) => key.return && key.shift
40
+ } }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Arrows: Navigate | Enter: Submit (if ends with ```) | Shift+Enter: Newline" }) })] }));
41
+ };
42
+ export async function promptMultiline(message) {
43
+ return new Promise((resolve) => {
44
+ let result = '';
45
+ const instance = render(_jsx(SmartPromptElement, { onDone: (val) => {
46
+ result = val;
47
+ }, title: message }));
48
+ instance.waitUntilExit().then(() => {
49
+ resolve(result);
50
+ });
51
+ });
52
+ }
@@ -0,0 +1,97 @@
1
+ export interface N8nClientConfig {
2
+ apiUrl?: string;
3
+ apiKey?: string;
4
+ }
5
+ export interface WorkflowExecutionResult {
6
+ executionId: string;
7
+ finished: boolean;
8
+ success: boolean;
9
+ data?: unknown;
10
+ error?: string;
11
+ }
12
+ export interface WorkflowValidationResult {
13
+ valid: boolean;
14
+ errors: string[];
15
+ warnings: string[];
16
+ }
17
+ /**
18
+ * n8n API Client
19
+ *
20
+ * Handles authentication and environment variables automatically.
21
+ * Follows @typescript-expert patterns for robust error handling and type safety.
22
+ */
23
+ export declare class N8nClient {
24
+ private apiUrl;
25
+ private apiKey;
26
+ private headers;
27
+ constructor(config?: N8nClientConfig);
28
+ /**
29
+ * Activate a workflow
30
+ */
31
+ activateWorkflow(workflowId: string): Promise<void>;
32
+ /**
33
+ * Deactivate a workflow
34
+ */
35
+ deactivateWorkflow(workflowId: string): Promise<void>;
36
+ /**
37
+ * Execute a workflow and return the result
38
+ */
39
+ executeWorkflow(workflowId: string, _data?: unknown): Promise<WorkflowExecutionResult>;
40
+ /**
41
+ * Get workflow execution details and logs
42
+ */
43
+ getExecution(executionId: string): Promise<unknown>;
44
+ /**
45
+ * Get executions for a workflow
46
+ */
47
+ getWorkflowExecutions(workflowId: string): Promise<any[]>;
48
+ /**
49
+ * Update a workflow JSON
50
+ */
51
+ updateWorkflow(workflowId: string, workflowData: unknown): Promise<void>;
52
+ /**
53
+ * Get workflow by ID
54
+ */
55
+ getWorkflow(workflowId: string): Promise<unknown>;
56
+ /**
57
+ * Create a new workflow
58
+ */
59
+ createWorkflow(name: string, workflowData: unknown): Promise<{
60
+ id: string;
61
+ }>;
62
+ /**
63
+ * Get all installed node types via Probe Workflow (Webhook)
64
+ *
65
+ * Strategy:
66
+ * 1. Create a workflow with Webhook -> HTTP Request (Internal API)
67
+ * 2. Activate it
68
+ * 3. Call the webhook -> Returns the node types
69
+ */
70
+ /**
71
+ * Get all installed node types via Probe Workflow (Webhook)
72
+ * Returns full node type objects including parameters, not just names.
73
+ */
74
+ getNodeTypes(): Promise<any[]>;
75
+ /**
76
+ * Get all workflows
77
+ */
78
+ getWorkflows(): Promise<{
79
+ id: string;
80
+ name: string;
81
+ active: boolean;
82
+ updatedAt: string;
83
+ }[]>;
84
+ /**
85
+ * Delete a workflow
86
+ */
87
+ deleteWorkflow(workflowId: string): Promise<void>;
88
+ /**
89
+ * Inject a trigger node to satisfy activation requirements.
90
+ * Uses a Webhook to allow actual activation (Manual triggers don't allow activation).
91
+ */
92
+ injectManualTrigger(workflowData: any): any;
93
+ /**
94
+ * Get n8n instance deep link for a workflow
95
+ */
96
+ getWorkflowLink(workflowId: string): string;
97
+ }
@@ -0,0 +1,440 @@
1
+ /**
2
+ * n8n API Client
3
+ *
4
+ * Handles authentication and environment variables automatically.
5
+ * Follows @typescript-expert patterns for robust error handling and type safety.
6
+ */
7
+ export class N8nClient {
8
+ apiUrl;
9
+ apiKey;
10
+ headers;
11
+ constructor(config) {
12
+ // Priority: Explicit Config > Environment > Defaults
13
+ this.apiUrl = config?.apiUrl ?? process.env.N8N_API_URL ?? 'http://localhost:5678/api/v1';
14
+ this.apiKey = config?.apiKey ?? process.env.N8N_API_KEY ?? '';
15
+ // Constructor validation moved to method call time to allow lazy loading
16
+ // from config file if not provided here.
17
+ this.headers = {
18
+ 'Content-Type': 'application/json',
19
+ 'X-N8N-API-KEY': this.apiKey,
20
+ };
21
+ }
22
+ /**
23
+ * Activate a workflow
24
+ */
25
+ async activateWorkflow(workflowId) {
26
+ const response = await fetch(`${this.apiUrl}/workflows/${workflowId}/activate`, {
27
+ headers: this.headers,
28
+ method: 'POST',
29
+ });
30
+ if (!response.ok) {
31
+ const errorText = await response.text();
32
+ throw new Error(`Failed to activate workflow: ${response.status} - ${errorText}`);
33
+ }
34
+ }
35
+ /**
36
+ * Deactivate a workflow
37
+ */
38
+ async deactivateWorkflow(workflowId) {
39
+ const response = await fetch(`${this.apiUrl}/workflows/${workflowId}/deactivate`, {
40
+ headers: this.headers,
41
+ method: 'POST',
42
+ });
43
+ if (!response.ok) {
44
+ const errorText = await response.text();
45
+ throw new Error(`Failed to deactivate workflow: ${response.status} - ${errorText}`);
46
+ }
47
+ }
48
+ /**
49
+ * Execute a workflow and return the result
50
+ */
51
+ async executeWorkflow(workflowId, _data) {
52
+ try {
53
+ // NOTE: Public API does not always expose a direct 'execute' endpoint for all workflow types.
54
+ // For validation purposes, we 'activate' the workflow which runs internal validation.
55
+ const response = await fetch(`${this.apiUrl}/workflows/${workflowId}/activate`, {
56
+ headers: this.headers,
57
+ method: 'POST',
58
+ });
59
+ if (!response.ok) {
60
+ const errorText = await response.text();
61
+ throw new Error(`n8n Validation Error: ${response.status} - ${errorText}`);
62
+ }
63
+ // If activation succeeds, we assume basic validation passed.
64
+ const result = await response.json();
65
+ return {
66
+ data: result,
67
+ error: undefined,
68
+ executionId: 'val-' + Math.random().toString(36).substring(7),
69
+ finished: true,
70
+ success: true,
71
+ };
72
+ }
73
+ catch (error) {
74
+ throw new Error(`Failed to validate workflow: ${error.message}`);
75
+ }
76
+ }
77
+ /**
78
+ * Get workflow execution details and logs
79
+ */
80
+ async getExecution(executionId) {
81
+ try {
82
+ const response = await fetch(`${this.apiUrl}/executions/${executionId}?includeData=true`, {
83
+ headers: this.headers,
84
+ method: 'GET',
85
+ });
86
+ if (!response.ok) {
87
+ throw new Error(`Failed to get execution: ${response.status}`);
88
+ }
89
+ return response.json();
90
+ }
91
+ catch (error) {
92
+ throw new Error(`Failed to fetch execution: ${error.message}`);
93
+ }
94
+ }
95
+ /**
96
+ * Get executions for a workflow
97
+ */
98
+ async getWorkflowExecutions(workflowId) {
99
+ try {
100
+ const url = new URL(`${this.apiUrl}/executions`);
101
+ url.searchParams.set('workflowId', workflowId);
102
+ url.searchParams.set('limit', '5');
103
+ const response = await fetch(url.toString(), {
104
+ headers: this.headers,
105
+ method: 'GET',
106
+ });
107
+ if (!response.ok) {
108
+ throw new Error(`Failed to get executions: ${response.status}`);
109
+ }
110
+ const result = await response.json();
111
+ return result.data;
112
+ }
113
+ catch (error) {
114
+ throw new Error(`Failed to fetch workflow executions: ${error.message}`);
115
+ }
116
+ }
117
+ /**
118
+ * Update a workflow JSON
119
+ */
120
+ async updateWorkflow(workflowId, workflowData) {
121
+ try {
122
+ const response = await fetch(`${this.apiUrl}/workflows/${workflowId}`, {
123
+ body: JSON.stringify(workflowData),
124
+ headers: this.headers,
125
+ method: 'PUT',
126
+ });
127
+ if (!response.ok) {
128
+ const errorText = await response.text();
129
+ throw new Error(`Failed to update workflow: ${response.status} - ${errorText}`);
130
+ }
131
+ }
132
+ catch (error) {
133
+ throw new Error(`Failed to update workflow: ${error.message}`);
134
+ }
135
+ }
136
+ /**
137
+ * Get workflow by ID
138
+ */
139
+ async getWorkflow(workflowId) {
140
+ try {
141
+ const response = await fetch(`${this.apiUrl}/workflows/${workflowId}`, {
142
+ headers: this.headers,
143
+ method: 'GET',
144
+ });
145
+ if (!response.ok) {
146
+ throw new Error(`Failed to get workflow: ${response.status}`);
147
+ }
148
+ return response.json();
149
+ }
150
+ catch (error) {
151
+ throw new Error(`Failed to fetch workflow: ${error.message}`);
152
+ }
153
+ }
154
+ /**
155
+ * Create a new workflow
156
+ */
157
+ async createWorkflow(name, workflowData) {
158
+ try {
159
+ const payload = {
160
+ name,
161
+ ...workflowData,
162
+ };
163
+ // Debug logging for payload validation errors
164
+ // console.log('DEBUG: createWorkflow payload keys:', Object.keys(payload));
165
+ const response = await fetch(`${this.apiUrl}/workflows`, {
166
+ body: JSON.stringify(payload),
167
+ headers: this.headers,
168
+ method: 'POST',
169
+ });
170
+ if (!response.ok) {
171
+ const errorText = await response.text();
172
+ throw new Error(`Failed to create workflow: ${response.status} - ${errorText}`);
173
+ }
174
+ const result = await response.json();
175
+ return { id: result.id };
176
+ }
177
+ catch (error) {
178
+ throw new Error(`Failed to create workflow: ${error.message}`);
179
+ }
180
+ }
181
+ /**
182
+ * Get all installed node types via Probe Workflow (Webhook)
183
+ *
184
+ * Strategy:
185
+ * 1. Create a workflow with Webhook -> HTTP Request (Internal API)
186
+ * 2. Activate it
187
+ * 3. Call the webhook -> Returns the node types
188
+ */
189
+ /**
190
+ * Get all installed node types via Probe Workflow (Webhook)
191
+ * Returns full node type objects including parameters, not just names.
192
+ */
193
+ async getNodeTypes() {
194
+ const probeId = `probe-${Math.random().toString(36).substring(7)}`;
195
+ const probePath = `n8m-probe-${Math.random().toString(36).substring(7)}`;
196
+ let workflowId = null;
197
+ try {
198
+ const internalApiUrl = this.apiUrl + '/node-types';
199
+ const probeWorkflow = {
200
+ name: `[n8m:system] Node Probe ${probeId}`,
201
+ nodes: [
202
+ {
203
+ parameters: {
204
+ httpMethod: "GET",
205
+ path: probePath,
206
+ responseMode: "lastNode",
207
+ options: {}
208
+ },
209
+ id: "webhook",
210
+ name: "ProbeWebhook",
211
+ type: "n8n-nodes-base.webhook",
212
+ typeVersion: 1,
213
+ position: [400, 300],
214
+ webhookId: probePath
215
+ },
216
+ {
217
+ parameters: {
218
+ url: internalApiUrl,
219
+ method: "GET",
220
+ authentication: "none",
221
+ sendHeaders: true,
222
+ headerParameters: {
223
+ parameters: [
224
+ {
225
+ name: "X-N8N-API-KEY",
226
+ value: this.apiKey
227
+ }
228
+ ]
229
+ },
230
+ options: {}
231
+ },
232
+ id: "http-request",
233
+ name: "FetchNodes",
234
+ type: "n8n-nodes-base.httpRequest",
235
+ typeVersion: 4.1,
236
+ position: [600, 300]
237
+ }
238
+ ],
239
+ connections: {
240
+ "ProbeWebhook": {
241
+ main: [[{ node: "FetchNodes", type: "main", index: 0 }]]
242
+ }
243
+ },
244
+ settings: {
245
+ saveManualExecutions: false,
246
+ callerPolicy: 'workflowsFromSameOwner'
247
+ }
248
+ };
249
+ // Header Injection
250
+ probeWorkflow.nodes[1].parameters.authentication = 'none';
251
+ probeWorkflow.nodes[1].parameters.headerParameters = {
252
+ parameters: [
253
+ { name: 'X-N8N-API-KEY', value: this.apiKey }
254
+ ]
255
+ };
256
+ // 1. Create
257
+ const { id } = await this.createWorkflow(probeWorkflow.name, probeWorkflow);
258
+ workflowId = id;
259
+ // 2. Activate
260
+ await this.activateWorkflow(id);
261
+ // 3. Trigger
262
+ const baseUrl = this.apiUrl.replace('/api/v1', '');
263
+ const webhookUrl = `${baseUrl}/webhook/${probePath}`;
264
+ console.log(`[N8nClient] Triggering probe at ${webhookUrl}...`);
265
+ const response = await fetch(webhookUrl);
266
+ if (!response.ok) {
267
+ const errorText = await response.text();
268
+ if (process.env.DEBUG) {
269
+ console.warn(`[N8nClient] Probe webhook failed (Status ${response.status}): ${errorText}`);
270
+ }
271
+ throw new Error(`Probe webhook failed: ${response.status}`);
272
+ }
273
+ const result = await response.json();
274
+ // Return full objects
275
+ if (Array.isArray(result)) {
276
+ return result;
277
+ }
278
+ if (result.data && Array.isArray(result.data)) {
279
+ return result.data;
280
+ }
281
+ return [];
282
+ }
283
+ catch (error) {
284
+ if (process.env.DEBUG) {
285
+ console.warn(`[N8nClient] Probe failed: ${error.message}`);
286
+ }
287
+ return [];
288
+ }
289
+ finally {
290
+ if (workflowId) {
291
+ try {
292
+ await this.deleteWorkflow(workflowId);
293
+ }
294
+ catch { /* intentionally empty */ }
295
+ }
296
+ }
297
+ }
298
+ /**
299
+ * Get all workflows
300
+ */
301
+ async getWorkflows() {
302
+ try {
303
+ let allWorkflows = [];
304
+ let cursor = undefined;
305
+ do {
306
+ const url = new URL(`${this.apiUrl}/workflows`);
307
+ if (cursor)
308
+ url.searchParams.set('cursor', cursor);
309
+ // increase limit to max to minimize requests
310
+ url.searchParams.set('limit', '250');
311
+ const response = await fetch(url.toString(), {
312
+ headers: this.headers,
313
+ method: 'GET',
314
+ });
315
+ if (!response.ok) {
316
+ throw new Error(`Failed to get workflows: ${response.status}`);
317
+ }
318
+ const result = await response.json();
319
+ allWorkflows = [...allWorkflows, ...result.data];
320
+ cursor = result.nextCursor;
321
+ } while (cursor);
322
+ return allWorkflows;
323
+ }
324
+ catch (error) {
325
+ throw new Error(`Failed to fetch workflows: ${error.message}`);
326
+ }
327
+ }
328
+ /**
329
+ * Delete a workflow
330
+ */
331
+ async deleteWorkflow(workflowId) {
332
+ try {
333
+ const response = await fetch(`${this.apiUrl}/workflows/${workflowId}`, {
334
+ headers: this.headers,
335
+ method: 'DELETE',
336
+ });
337
+ if (!response.ok) {
338
+ throw new Error(`Failed to delete workflow: ${response.status}`);
339
+ }
340
+ }
341
+ catch (error) {
342
+ throw new Error(`Failed to delete workflow: ${error.message}`);
343
+ }
344
+ }
345
+ /**
346
+ * Inject a trigger node to satisfy activation requirements.
347
+ * Uses a Webhook to allow actual activation (Manual triggers don't allow activation).
348
+ */
349
+ injectManualTrigger(workflowData) {
350
+ const shimNodeId = "shim-trigger-" + Math.random().toString(36).substring(7);
351
+ const webhookPath = String("n8m-shim-" + Math.random().toString(36).substring(7));
352
+ // Shim Node (Trigger)
353
+ const shimNode = {
354
+ parameters: {
355
+ httpMethod: "POST",
356
+ path: webhookPath,
357
+ responseMode: "onReceived",
358
+ options: {}
359
+ },
360
+ id: shimNodeId,
361
+ name: "N8M_Shim_Webhook",
362
+ type: "n8n-nodes-base.webhook",
363
+ typeVersion: 1,
364
+ position: [0, 0],
365
+ webhookId: String(webhookPath)
366
+ };
367
+ // Flattener Node (Code)
368
+ // Hoists 'body' and 'query' to root so downstream nodes see expected schema
369
+ const flattenerId = "shim-flattener-" + Math.random().toString(36).substring(7);
370
+ const flattenerNode = {
371
+ parameters: {
372
+ jsCode: `return items.map(item => {
373
+ const body = item.json.body || {};
374
+ const query = item.json.query || {};
375
+ // Prioritize body over query, and existing item props
376
+ return {
377
+ json: {
378
+ ...item.json,
379
+ ...query,
380
+ ...body
381
+ }
382
+ };
383
+ });`
384
+ },
385
+ id: flattenerId,
386
+ name: "Shim_Flattener",
387
+ type: "n8n-nodes-base.code",
388
+ typeVersion: 2,
389
+ position: [200, 0]
390
+ };
391
+ // Ensure no webhookId on flattener or non-webhooks
392
+ delete flattenerNode.webhookId;
393
+ const nodes = [...(workflowData.nodes || []), shimNode, flattenerNode];
394
+ const connections = { ...(workflowData.connections || {}) };
395
+ // Try to connect to the first non-trigger node to make it a valid flow
396
+ const targetNode = nodes.find((n) => n.id !== shimNodeId && n.id !== flattenerId && !n.type.includes('Trigger'));
397
+ if (targetNode) {
398
+ // Connect Webhook -> Flattener
399
+ if (!connections[shimNode.name]) {
400
+ connections[shimNode.name] = {
401
+ main: [
402
+ [
403
+ {
404
+ node: flattenerNode.name,
405
+ type: 'main',
406
+ index: 0
407
+ }
408
+ ]
409
+ ]
410
+ };
411
+ }
412
+ // Connect Flattener -> Target
413
+ if (!connections[flattenerNode.name]) {
414
+ connections[flattenerNode.name] = {
415
+ main: [
416
+ [
417
+ {
418
+ node: targetNode.name,
419
+ type: 'main',
420
+ index: 0
421
+ }
422
+ ]
423
+ ]
424
+ };
425
+ }
426
+ }
427
+ return {
428
+ ...workflowData,
429
+ nodes,
430
+ connections
431
+ };
432
+ }
433
+ /**
434
+ * Get n8n instance deep link for a workflow
435
+ */
436
+ getWorkflowLink(workflowId) {
437
+ const baseUrl = this.apiUrl.replace('/api/v1', '');
438
+ return `${baseUrl}/workflow/${workflowId}`;
439
+ }
440
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sandboxed Execution Environment for Dynamic Tools
3
+ * Allows agents to write and run temporary analysis scripts.
4
+ */
5
+ export declare class Sandbox {
6
+ /**
7
+ * Run a snippet of JavaScript code safely
8
+ * @param code The code to execute
9
+ * @param context External variables to expose to the script
10
+ * @returns The result of the execution
11
+ */
12
+ static run(code: string, context?: Record<string, any>): any;
13
+ }
@@ -0,0 +1,34 @@
1
+ import vm from 'vm';
2
+ import { theme } from './theme.js';
3
+ /**
4
+ * Sandboxed Execution Environment for Dynamic Tools
5
+ * Allows agents to write and run temporary analysis scripts.
6
+ */
7
+ export class Sandbox {
8
+ /**
9
+ * Run a snippet of JavaScript code safely
10
+ * @param code The code to execute
11
+ * @param context External variables to expose to the script
12
+ * @returns The result of the execution
13
+ */
14
+ static run(code, context = {}) {
15
+ const sandbox = {
16
+ console: {
17
+ log: (...args) => console.log(theme.muted('[Sandbox]'), ...args),
18
+ error: (...args) => console.error(theme.error('[Sandbox Error]'), ...args)
19
+ },
20
+ ...context
21
+ };
22
+ const script = new vm.Script(code);
23
+ const vmContext = vm.createContext(sandbox);
24
+ try {
25
+ console.log(theme.agent(`Running Dynamic Tool Script (${code.length} bytes)...`));
26
+ const result = script.runInContext(vmContext, { timeout: 5000 }); // 5s timeout
27
+ return result;
28
+ }
29
+ catch (error) {
30
+ console.error(theme.error(`Sandbox Execution Failed: ${error.message}`));
31
+ throw error;
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,23 @@
1
+ export declare const theme: {
2
+ divider: (len?: number) => string;
3
+ header: (text: string) => string;
4
+ subHeader: (text: string) => string;
5
+ label: (text: string) => string;
6
+ value: (text: string | number | boolean) => string;
7
+ info: (text: string) => string;
8
+ done: (text: string) => string;
9
+ warn: (text: string) => string;
10
+ fail: (text: string) => string;
11
+ agent: (text: string) => string;
12
+ brand: () => string;
13
+ tag: (text: string) => string;
14
+ primary: import("chalk").ChalkInstance;
15
+ secondary: import("chalk").ChalkInstance;
16
+ muted: import("chalk").ChalkInstance;
17
+ foreground: import("chalk").ChalkInstance;
18
+ success: import("chalk").ChalkInstance;
19
+ warning: import("chalk").ChalkInstance;
20
+ error: import("chalk").ChalkInstance;
21
+ ai: import("chalk").ChalkInstance;
22
+ card: import("chalk").ChalkInstance;
23
+ };