@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.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/bin/dev.js +5 -0
- package/bin/run.js +6 -0
- package/dist/agentic/checkpointer.d.ts +2 -0
- package/dist/agentic/checkpointer.js +14 -0
- package/dist/agentic/graph.d.ts +483 -0
- package/dist/agentic/graph.js +100 -0
- package/dist/agentic/nodes/architect.d.ts +6 -0
- package/dist/agentic/nodes/architect.js +51 -0
- package/dist/agentic/nodes/engineer.d.ts +11 -0
- package/dist/agentic/nodes/engineer.js +182 -0
- package/dist/agentic/nodes/qa.d.ts +5 -0
- package/dist/agentic/nodes/qa.js +151 -0
- package/dist/agentic/nodes/reviewer.d.ts +5 -0
- package/dist/agentic/nodes/reviewer.js +111 -0
- package/dist/agentic/nodes/supervisor.d.ts +6 -0
- package/dist/agentic/nodes/supervisor.js +18 -0
- package/dist/agentic/state.d.ts +51 -0
- package/dist/agentic/state.js +26 -0
- package/dist/commands/config.d.ts +13 -0
- package/dist/commands/config.js +47 -0
- package/dist/commands/create.d.ts +14 -0
- package/dist/commands/create.js +182 -0
- package/dist/commands/deploy.d.ts +13 -0
- package/dist/commands/deploy.js +68 -0
- package/dist/commands/modify.d.ts +13 -0
- package/dist/commands/modify.js +276 -0
- package/dist/commands/prune.d.ts +9 -0
- package/dist/commands/prune.js +98 -0
- package/dist/commands/resume.d.ts +8 -0
- package/dist/commands/resume.js +39 -0
- package/dist/commands/test.d.ts +27 -0
- package/dist/commands/test.js +619 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/ai.service.d.ts +51 -0
- package/dist/services/ai.service.js +421 -0
- package/dist/services/n8n.service.d.ts +17 -0
- package/dist/services/n8n.service.js +81 -0
- package/dist/services/node-definitions.service.d.ts +36 -0
- package/dist/services/node-definitions.service.js +102 -0
- package/dist/utils/config.d.ts +15 -0
- package/dist/utils/config.js +25 -0
- package/dist/utils/multilinePrompt.d.ts +1 -0
- package/dist/utils/multilinePrompt.js +52 -0
- package/dist/utils/n8nClient.d.ts +97 -0
- package/dist/utils/n8nClient.js +440 -0
- package/dist/utils/sandbox.d.ts +13 -0
- package/dist/utils/sandbox.js +34 -0
- package/dist/utils/theme.d.ts +23 -0
- package/dist/utils/theme.js +92 -0
- package/oclif.manifest.json +331 -0
- 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
|
+
};
|