@kairos-sdk/core 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 +326 -0
- package/dist/chunk-IADOKKFO.js +1296 -0
- package/dist/chunk-IADOKKFO.js.map +1 -0
- package/dist/cli.cjs +1508 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +213 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1456 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +417 -0
- package/dist/index.d.ts +417 -0
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,1508 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/client.ts
|
|
27
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
28
|
+
|
|
29
|
+
// src/utils/uuid.ts
|
|
30
|
+
function generateUUID() {
|
|
31
|
+
return crypto.randomUUID();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/library/null-library.ts
|
|
35
|
+
var NullLibrary = class {
|
|
36
|
+
async initialize() {
|
|
37
|
+
}
|
|
38
|
+
async search(_description, _options) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
async save(_workflow, _metadata) {
|
|
42
|
+
return generateUUID();
|
|
43
|
+
}
|
|
44
|
+
async recordDeployment(_id) {
|
|
45
|
+
}
|
|
46
|
+
async get(_id) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
async list(_filters) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/errors/base.ts
|
|
55
|
+
var KairosError = class extends Error {
|
|
56
|
+
constructor(message, cause) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.cause = cause;
|
|
59
|
+
this.name = "KairosError";
|
|
60
|
+
}
|
|
61
|
+
cause;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/errors/api-error.ts
|
|
65
|
+
var ApiError = class extends KairosError {
|
|
66
|
+
constructor(message, statusCode, cause) {
|
|
67
|
+
super(message, cause);
|
|
68
|
+
this.statusCode = statusCode;
|
|
69
|
+
this.name = "ApiError";
|
|
70
|
+
}
|
|
71
|
+
statusCode;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/errors/provider-error.ts
|
|
75
|
+
var ProviderError = class extends KairosError {
|
|
76
|
+
constructor(message, cause) {
|
|
77
|
+
super(message, cause);
|
|
78
|
+
this.name = "ProviderError";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/providers/n8n/api-client.ts
|
|
83
|
+
var EXECUTION_LIMIT_CAP = 100;
|
|
84
|
+
var N8nApiClient = class {
|
|
85
|
+
constructor(baseUrl, apiKey, logger) {
|
|
86
|
+
this.baseUrl = baseUrl;
|
|
87
|
+
this.apiKey = apiKey;
|
|
88
|
+
this.logger = logger;
|
|
89
|
+
}
|
|
90
|
+
baseUrl;
|
|
91
|
+
apiKey;
|
|
92
|
+
logger;
|
|
93
|
+
async request(method, path, body) {
|
|
94
|
+
const url = `${this.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
|
|
95
|
+
this.logger.debug(`n8n ${method} ${path}`);
|
|
96
|
+
let response;
|
|
97
|
+
try {
|
|
98
|
+
response = await fetch(url, {
|
|
99
|
+
method,
|
|
100
|
+
headers: {
|
|
101
|
+
"X-N8N-API-KEY": this.apiKey,
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
Accept: "application/json"
|
|
104
|
+
},
|
|
105
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
throw new ProviderError(`Network error calling n8n API: ${path}`, err);
|
|
109
|
+
}
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
let errorBody;
|
|
112
|
+
try {
|
|
113
|
+
errorBody = await response.json();
|
|
114
|
+
} catch {
|
|
115
|
+
errorBody = await response.text().catch(() => "");
|
|
116
|
+
}
|
|
117
|
+
this.logger.error(`n8n API error ${response.status} on ${method} ${path}`, {
|
|
118
|
+
status: response.status,
|
|
119
|
+
body: String(errorBody)
|
|
120
|
+
});
|
|
121
|
+
throw new ApiError(
|
|
122
|
+
`n8n API returned ${response.status} for ${method} ${path}: ${JSON.stringify(errorBody)}`,
|
|
123
|
+
response.status,
|
|
124
|
+
errorBody
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (response.status === 204) return void 0;
|
|
128
|
+
return response.json();
|
|
129
|
+
}
|
|
130
|
+
async createWorkflow(workflow) {
|
|
131
|
+
return this.request("POST", "/workflows", workflow);
|
|
132
|
+
}
|
|
133
|
+
async updateWorkflow(id, workflow) {
|
|
134
|
+
return this.request("PUT", `/workflows/${id}`, workflow);
|
|
135
|
+
}
|
|
136
|
+
async getWorkflow(id) {
|
|
137
|
+
return this.request("GET", `/workflows/${id}`);
|
|
138
|
+
}
|
|
139
|
+
async listWorkflows() {
|
|
140
|
+
const response = await this.request("GET", "/workflows?limit=250");
|
|
141
|
+
return response.data.map((w) => ({
|
|
142
|
+
id: w.id,
|
|
143
|
+
name: w.name,
|
|
144
|
+
active: w.active,
|
|
145
|
+
createdAt: w.createdAt,
|
|
146
|
+
updatedAt: w.updatedAt,
|
|
147
|
+
...w.tags !== void 0 ? { tags: w.tags } : {}
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
async deleteWorkflow(id) {
|
|
151
|
+
await this.request("DELETE", `/workflows/${id}`);
|
|
152
|
+
}
|
|
153
|
+
async activateWorkflow(id) {
|
|
154
|
+
await this.request("POST", `/workflows/${id}/activate`);
|
|
155
|
+
}
|
|
156
|
+
async deactivateWorkflow(id) {
|
|
157
|
+
await this.request("POST", `/workflows/${id}/deactivate`);
|
|
158
|
+
}
|
|
159
|
+
async getExecutions(workflowId, filter) {
|
|
160
|
+
const params = new URLSearchParams();
|
|
161
|
+
if (workflowId) params.set("workflowId", workflowId);
|
|
162
|
+
if (filter?.status) params.set("status", filter.status);
|
|
163
|
+
const limit = Math.min(filter?.limit ?? 20, EXECUTION_LIMIT_CAP);
|
|
164
|
+
params.set("limit", String(limit));
|
|
165
|
+
if (filter?.cursor) params.set("cursor", filter.cursor);
|
|
166
|
+
const qs = params.toString();
|
|
167
|
+
const response = await this.request("GET", `/executions${qs ? `?${qs}` : ""}`);
|
|
168
|
+
return response.data.map(this.mapExecution);
|
|
169
|
+
}
|
|
170
|
+
async getExecution(id) {
|
|
171
|
+
const response = await this.request("GET", `/executions/${id}`);
|
|
172
|
+
return { ...this.mapExecution(response), data: response.data, workflowData: response.workflowData };
|
|
173
|
+
}
|
|
174
|
+
async listTags() {
|
|
175
|
+
const response = await this.request("GET", "/tags");
|
|
176
|
+
return response.data.map((t) => ({ id: t.id, name: t.name }));
|
|
177
|
+
}
|
|
178
|
+
async createTag(name) {
|
|
179
|
+
const response = await this.request("POST", "/tags", { name });
|
|
180
|
+
return { id: response.id, name: response.name };
|
|
181
|
+
}
|
|
182
|
+
async tagWorkflow(workflowId, tagIds) {
|
|
183
|
+
await this.request("PUT", `/workflows/${workflowId}/tags`, tagIds.map((id) => ({ id })));
|
|
184
|
+
}
|
|
185
|
+
async untagWorkflow(workflowId, tagIds) {
|
|
186
|
+
const current = await this.getWorkflow(workflowId);
|
|
187
|
+
const remaining = (current.tags ?? []).filter((t) => !tagIds.includes(t.id)).map((t) => ({ id: t.id }));
|
|
188
|
+
await this.request("PUT", `/workflows/${workflowId}/tags`, remaining);
|
|
189
|
+
}
|
|
190
|
+
mapExecution(e) {
|
|
191
|
+
return {
|
|
192
|
+
id: e.id,
|
|
193
|
+
workflowId: e.workflowId,
|
|
194
|
+
status: e.status,
|
|
195
|
+
startedAt: e.startedAt,
|
|
196
|
+
...e.stoppedAt !== void 0 ? { stoppedAt: e.stoppedAt } : {},
|
|
197
|
+
mode: e.mode
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/providers/n8n/types.ts
|
|
203
|
+
var FORBIDDEN_ON_CREATE = [
|
|
204
|
+
"id",
|
|
205
|
+
"createdAt",
|
|
206
|
+
"updatedAt",
|
|
207
|
+
"versionId",
|
|
208
|
+
"meta",
|
|
209
|
+
"isArchived",
|
|
210
|
+
"activeVersionId",
|
|
211
|
+
"activeVersion",
|
|
212
|
+
"active",
|
|
213
|
+
"pinData",
|
|
214
|
+
"triggerCount",
|
|
215
|
+
"shared"
|
|
216
|
+
];
|
|
217
|
+
var FORBIDDEN_ON_UPDATE = FORBIDDEN_ON_CREATE.filter((f) => f !== "id");
|
|
218
|
+
|
|
219
|
+
// src/providers/n8n/stripper.ts
|
|
220
|
+
var N8nFieldStripper = class {
|
|
221
|
+
stripForCreate(workflow) {
|
|
222
|
+
return this.strip(workflow, FORBIDDEN_ON_CREATE);
|
|
223
|
+
}
|
|
224
|
+
stripForUpdate(workflow) {
|
|
225
|
+
return this.strip(workflow, FORBIDDEN_ON_UPDATE);
|
|
226
|
+
}
|
|
227
|
+
strip(workflow, forbidden) {
|
|
228
|
+
const result = { ...workflow };
|
|
229
|
+
for (const field of forbidden) {
|
|
230
|
+
delete result[field];
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/errors/guard-error.ts
|
|
237
|
+
var GuardError = class extends KairosError {
|
|
238
|
+
constructor(message) {
|
|
239
|
+
super(message);
|
|
240
|
+
this.name = "GuardError";
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// src/providers/n8n/provider.ts
|
|
245
|
+
var N8nProvider = class {
|
|
246
|
+
constructor(client, stripper) {
|
|
247
|
+
this.client = client;
|
|
248
|
+
this.stripper = stripper;
|
|
249
|
+
}
|
|
250
|
+
client;
|
|
251
|
+
stripper;
|
|
252
|
+
platform = "n8n";
|
|
253
|
+
async deploy(workflow) {
|
|
254
|
+
const stripped = this.stripper.stripForCreate(workflow);
|
|
255
|
+
const response = await this.client.createWorkflow(stripped);
|
|
256
|
+
return { workflowId: response.id, name: response.name };
|
|
257
|
+
}
|
|
258
|
+
async update(id, workflow) {
|
|
259
|
+
const stripped = this.stripper.stripForUpdate(workflow);
|
|
260
|
+
const response = await this.client.updateWorkflow(id, stripped);
|
|
261
|
+
return { workflowId: response.id, name: response.name };
|
|
262
|
+
}
|
|
263
|
+
async get(id) {
|
|
264
|
+
const response = await this.client.getWorkflow(id);
|
|
265
|
+
return {
|
|
266
|
+
name: response.name,
|
|
267
|
+
nodes: response.nodes,
|
|
268
|
+
connections: response.connections,
|
|
269
|
+
...response.settings !== void 0 ? { settings: response.settings } : {},
|
|
270
|
+
...response.tags !== void 0 ? { tags: response.tags } : {}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
async list() {
|
|
274
|
+
return this.client.listWorkflows();
|
|
275
|
+
}
|
|
276
|
+
async activate(id) {
|
|
277
|
+
await this.client.activateWorkflow(id);
|
|
278
|
+
}
|
|
279
|
+
async deactivate(id) {
|
|
280
|
+
await this.client.deactivateWorkflow(id);
|
|
281
|
+
}
|
|
282
|
+
async delete(id, options) {
|
|
283
|
+
if (options.confirm !== true) {
|
|
284
|
+
throw new GuardError("delete() requires { confirm: true } to prevent accidental deletion");
|
|
285
|
+
}
|
|
286
|
+
await this.client.deleteWorkflow(id);
|
|
287
|
+
}
|
|
288
|
+
async executions(workflowId, filter) {
|
|
289
|
+
return this.client.getExecutions(workflowId, filter);
|
|
290
|
+
}
|
|
291
|
+
async execution(id) {
|
|
292
|
+
return this.client.getExecution(id);
|
|
293
|
+
}
|
|
294
|
+
async listTags() {
|
|
295
|
+
return this.client.listTags();
|
|
296
|
+
}
|
|
297
|
+
async createTag(name) {
|
|
298
|
+
return this.client.createTag(name);
|
|
299
|
+
}
|
|
300
|
+
async tag(workflowId, tagIds) {
|
|
301
|
+
await this.client.tagWorkflow(workflowId, tagIds);
|
|
302
|
+
}
|
|
303
|
+
async untag(workflowId, tagIds) {
|
|
304
|
+
await this.client.untagWorkflow(workflowId, tagIds);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// src/validation/registry.ts
|
|
309
|
+
var DEFAULT_REGISTRY = [
|
|
310
|
+
// Trigger nodes
|
|
311
|
+
{ type: "n8n-nodes-base.manualTrigger", safeTypeVersions: [1], requiredParams: [], isTrigger: true },
|
|
312
|
+
{ type: "n8n-nodes-base.scheduleTrigger", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], isTrigger: true },
|
|
313
|
+
{ type: "n8n-nodes-base.webhook", safeTypeVersions: [1, 1.1, 2], requiredParams: ["httpMethod", "path"], isTrigger: true },
|
|
314
|
+
{ type: "n8n-nodes-base.formTrigger", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [], isTrigger: true },
|
|
315
|
+
{ type: "n8n-nodes-base.emailReadImap", safeTypeVersions: [2], requiredParams: [], credentialType: "imap", isTrigger: true },
|
|
316
|
+
{ type: "n8n-nodes-base.errorTrigger", safeTypeVersions: [1], requiredParams: [], isTrigger: true },
|
|
317
|
+
{ type: "n8n-nodes-base.executeWorkflowTrigger", safeTypeVersions: [1, 1.1], requiredParams: [], isTrigger: true },
|
|
318
|
+
{ type: "n8n-nodes-base.gmailTrigger", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], credentialType: "gmailOAuth2", isTrigger: true },
|
|
319
|
+
{ type: "n8n-nodes-base.googleDriveTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "googleDriveOAuth2Api", isTrigger: true },
|
|
320
|
+
{ type: "n8n-nodes-base.googleSheetsTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "googleSheetsTriggerOAuth2Api", isTrigger: true },
|
|
321
|
+
{ type: "n8n-nodes-base.slackTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "slackApi", isTrigger: true },
|
|
322
|
+
{ type: "n8n-nodes-base.telegramTrigger", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], credentialType: "telegramApi", isTrigger: true },
|
|
323
|
+
{ type: "n8n-nodes-base.githubTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "githubApi", isTrigger: true },
|
|
324
|
+
{ type: "n8n-nodes-base.stripeTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "stripeApi", isTrigger: true },
|
|
325
|
+
{ type: "n8n-nodes-base.airtableTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "airtableTokenApi", isTrigger: true },
|
|
326
|
+
{ type: "n8n-nodes-base.notionTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "notionApi", isTrigger: true },
|
|
327
|
+
{ type: "@n8n/n8n-nodes-langchain.chatTrigger", safeTypeVersions: [1, 1.1], requiredParams: [], isTrigger: true },
|
|
328
|
+
// Core logic nodes
|
|
329
|
+
{ type: "n8n-nodes-base.code", safeTypeVersions: [1, 2], requiredParams: [] },
|
|
330
|
+
{ type: "n8n-nodes-base.httpRequest", safeTypeVersions: [1, 2, 3, 4, 4.1, 4.2], requiredParams: ["url"] },
|
|
331
|
+
{ type: "n8n-nodes-base.set", safeTypeVersions: [1, 2, 3, 3.1, 3.2, 3.3, 3.4], requiredParams: [] },
|
|
332
|
+
{ type: "n8n-nodes-base.if", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [] },
|
|
333
|
+
{ type: "n8n-nodes-base.switch", safeTypeVersions: [1, 2, 3, 3.1, 3.2], requiredParams: [] },
|
|
334
|
+
{ type: "n8n-nodes-base.filter", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [] },
|
|
335
|
+
{ type: "n8n-nodes-base.merge", safeTypeVersions: [1, 2, 2.1, 3], requiredParams: [] },
|
|
336
|
+
{ type: "n8n-nodes-base.splitInBatches", safeTypeVersions: [1, 2, 3], requiredParams: [] },
|
|
337
|
+
{ type: "n8n-nodes-base.wait", safeTypeVersions: [1, 1.1], requiredParams: [] },
|
|
338
|
+
{ type: "n8n-nodes-base.executeWorkflow", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [] },
|
|
339
|
+
{ type: "n8n-nodes-base.respondToWebhook", safeTypeVersions: [1, 1.1], requiredParams: [] },
|
|
340
|
+
{ type: "n8n-nodes-base.noOp", safeTypeVersions: [1], requiredParams: [] },
|
|
341
|
+
{ type: "n8n-nodes-base.stopAndError", safeTypeVersions: [1], requiredParams: [] },
|
|
342
|
+
{ type: "n8n-nodes-base.splitOut", safeTypeVersions: [1], requiredParams: [] },
|
|
343
|
+
{ type: "n8n-nodes-base.aggregate", safeTypeVersions: [1], requiredParams: [] },
|
|
344
|
+
{ type: "n8n-nodes-base.stickyNote", safeTypeVersions: [1], requiredParams: [] },
|
|
345
|
+
// Email / messaging
|
|
346
|
+
{ type: "n8n-nodes-base.emailSend", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "smtp" },
|
|
347
|
+
{ type: "n8n-nodes-base.slack", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [], credentialType: "slackOAuth2Api" },
|
|
348
|
+
{ type: "n8n-nodes-base.telegram", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], credentialType: "telegramApi" },
|
|
349
|
+
{ type: "n8n-nodes-base.discord", safeTypeVersions: [1, 2], requiredParams: [], credentialType: "discordWebhookApi" },
|
|
350
|
+
// Google
|
|
351
|
+
{ type: "n8n-nodes-base.gmail", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "gmailOAuth2" },
|
|
352
|
+
{ type: "n8n-nodes-base.googleSheets", safeTypeVersions: [1, 2, 3, 4, 4.1, 4.2, 4.3, 4.4, 4.5], requiredParams: [], credentialType: "googleSheetsOAuth2Api" },
|
|
353
|
+
{ type: "n8n-nodes-base.googleDrive", safeTypeVersions: [1, 2, 3], requiredParams: [], credentialType: "googleDriveOAuth2Api" },
|
|
354
|
+
{ type: "n8n-nodes-base.googleCalendar", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [], credentialType: "googleCalendarOAuth2Api" },
|
|
355
|
+
// Project management / CRM
|
|
356
|
+
{ type: "n8n-nodes-base.notion", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [], credentialType: "notionApi" },
|
|
357
|
+
{ type: "n8n-nodes-base.airtable", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "airtableTokenApi" },
|
|
358
|
+
{ type: "n8n-nodes-base.github", safeTypeVersions: [1, 1.1], requiredParams: [], credentialType: "githubApi" },
|
|
359
|
+
{ type: "n8n-nodes-base.jira", safeTypeVersions: [1], requiredParams: [], credentialType: "jiraSoftwareCloudApi" },
|
|
360
|
+
{ type: "n8n-nodes-base.hubspot", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "hubspotOAuth2Api" },
|
|
361
|
+
// Databases
|
|
362
|
+
{ type: "n8n-nodes-base.postgres", safeTypeVersions: [1, 2, 2.1, 2.2, 2.3, 2.4, 2.5], requiredParams: [], credentialType: "postgres" },
|
|
363
|
+
{ type: "n8n-nodes-base.mySql", safeTypeVersions: [1, 2, 2.1, 2.2, 2.3, 2.4], requiredParams: [], credentialType: "mySql" },
|
|
364
|
+
{ type: "n8n-nodes-base.redis", safeTypeVersions: [1], requiredParams: [], credentialType: "redis" },
|
|
365
|
+
{ type: "n8n-nodes-base.supabase", safeTypeVersions: [1], requiredParams: [], credentialType: "supabaseApi" },
|
|
366
|
+
// Cloud
|
|
367
|
+
{ type: "n8n-nodes-base.awsS3", safeTypeVersions: [1, 2], requiredParams: [], credentialType: "aws" },
|
|
368
|
+
// Payment / commerce
|
|
369
|
+
{ type: "n8n-nodes-base.stripe", safeTypeVersions: [1], requiredParams: [], credentialType: "stripeApi" },
|
|
370
|
+
// AI / LangChain root nodes
|
|
371
|
+
{ type: "@n8n/n8n-nodes-langchain.agent", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9], requiredParams: [] },
|
|
372
|
+
{ type: "@n8n/n8n-nodes-langchain.chainLlm", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5], requiredParams: [] },
|
|
373
|
+
{ type: "@n8n/n8n-nodes-langchain.chainRetrievalQa", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4], requiredParams: [] },
|
|
374
|
+
{ type: "@n8n/n8n-nodes-langchain.informationExtractor", safeTypeVersions: [1], requiredParams: [] },
|
|
375
|
+
{ type: "@n8n/n8n-nodes-langchain.textClassifier", safeTypeVersions: [1], requiredParams: [] },
|
|
376
|
+
// AI / LangChain sub-nodes (models)
|
|
377
|
+
{ type: "@n8n/n8n-nodes-langchain.lmChatOpenAi", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7], requiredParams: [], credentialType: "openAiApi" },
|
|
378
|
+
{ type: "@n8n/n8n-nodes-langchain.lmChatAnthropic", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [], credentialType: "anthropicApi" },
|
|
379
|
+
{ type: "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", safeTypeVersions: [1], requiredParams: [], credentialType: "googlePalmApi" },
|
|
380
|
+
// AI / LangChain sub-nodes (memory, tools, etc.)
|
|
381
|
+
{ type: "@n8n/n8n-nodes-langchain.memoryBufferWindow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
|
|
382
|
+
{ type: "@n8n/n8n-nodes-langchain.toolWorkflow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
|
|
383
|
+
{ type: "@n8n/n8n-nodes-langchain.toolCode", safeTypeVersions: [1, 1.1], requiredParams: [] },
|
|
384
|
+
{ type: "@n8n/n8n-nodes-langchain.toolHttpRequest", safeTypeVersions: [1, 1.1], requiredParams: [] }
|
|
385
|
+
];
|
|
386
|
+
var NodeRegistry = class {
|
|
387
|
+
byType;
|
|
388
|
+
constructor(definitions = DEFAULT_REGISTRY) {
|
|
389
|
+
this.byType = new Map(definitions.map((d) => [d.type, d]));
|
|
390
|
+
}
|
|
391
|
+
get(type) {
|
|
392
|
+
return this.byType.get(type);
|
|
393
|
+
}
|
|
394
|
+
isTrigger(type) {
|
|
395
|
+
return this.byType.get(type)?.isTrigger === true;
|
|
396
|
+
}
|
|
397
|
+
isVersionSafe(type, version) {
|
|
398
|
+
const def = this.byType.get(type);
|
|
399
|
+
if (!def) return true;
|
|
400
|
+
return def.safeTypeVersions.includes(version);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// src/validation/validator.ts
|
|
405
|
+
var AI_CONNECTION_TYPES = [
|
|
406
|
+
"ai_languageModel",
|
|
407
|
+
"ai_memory",
|
|
408
|
+
"ai_tool",
|
|
409
|
+
"ai_outputParser",
|
|
410
|
+
"ai_embedding",
|
|
411
|
+
"ai_document",
|
|
412
|
+
"ai_textSplitter",
|
|
413
|
+
"ai_retriever",
|
|
414
|
+
"ai_vectorStore"
|
|
415
|
+
];
|
|
416
|
+
var TRIGGER_TYPE_PATTERNS = [/trigger/i, /Trigger$/];
|
|
417
|
+
var NODE_TYPE_PATTERN = /^(@[a-z0-9-]+\/[a-z0-9-]+\.|n8n-nodes-[a-z0-9-]+\.)[a-zA-Z][a-zA-Z0-9]+$/;
|
|
418
|
+
var N8nValidator = class {
|
|
419
|
+
registry;
|
|
420
|
+
constructor(registry = new NodeRegistry(DEFAULT_REGISTRY)) {
|
|
421
|
+
this.registry = registry;
|
|
422
|
+
}
|
|
423
|
+
validate(workflow) {
|
|
424
|
+
const issues = [];
|
|
425
|
+
this.checkRule1(workflow, issues);
|
|
426
|
+
this.checkRule2(workflow, issues);
|
|
427
|
+
this.checkRule3(workflow, issues);
|
|
428
|
+
this.checkRule4(workflow, issues);
|
|
429
|
+
this.checkRule5(workflow, issues);
|
|
430
|
+
this.checkRule6(workflow, issues);
|
|
431
|
+
this.checkRule7(workflow, issues);
|
|
432
|
+
this.checkRule8(workflow, issues);
|
|
433
|
+
this.checkRule9(workflow, issues);
|
|
434
|
+
this.checkRule10(workflow, issues);
|
|
435
|
+
this.checkRule11(workflow, issues);
|
|
436
|
+
this.checkRule12(workflow, issues);
|
|
437
|
+
this.checkRule13(workflow, issues);
|
|
438
|
+
this.checkRule14(workflow, issues);
|
|
439
|
+
this.checkRule15(workflow, issues);
|
|
440
|
+
this.checkRule16(workflow, issues);
|
|
441
|
+
this.checkRule17(workflow, issues);
|
|
442
|
+
this.checkRule18(workflow, issues);
|
|
443
|
+
this.checkRule19(workflow, issues);
|
|
444
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
445
|
+
return { valid: errors.length === 0, issues };
|
|
446
|
+
}
|
|
447
|
+
err(issues, rule, message, nodeId) {
|
|
448
|
+
const issue = { rule, severity: "error", message };
|
|
449
|
+
if (nodeId !== void 0) issue.nodeId = nodeId;
|
|
450
|
+
issues.push(issue);
|
|
451
|
+
}
|
|
452
|
+
warn(issues, rule, message, nodeId) {
|
|
453
|
+
const issue = { rule, severity: "warn", message };
|
|
454
|
+
if (nodeId !== void 0) issue.nodeId = nodeId;
|
|
455
|
+
issues.push(issue);
|
|
456
|
+
}
|
|
457
|
+
isTriggerNode(node) {
|
|
458
|
+
if (this.registry.isTrigger(node.type)) return true;
|
|
459
|
+
return TRIGGER_TYPE_PATTERNS.some((p) => p.test(node.type));
|
|
460
|
+
}
|
|
461
|
+
// Rule 1: name is a non-empty string
|
|
462
|
+
checkRule1(w, issues) {
|
|
463
|
+
if (typeof w.name !== "string" || w.name.trim() === "") {
|
|
464
|
+
this.err(issues, 1, "Workflow name is required and must be a non-empty string");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Rule 2: nodes is an array with at least one element
|
|
468
|
+
checkRule2(w, issues) {
|
|
469
|
+
if (!Array.isArray(w.nodes) || w.nodes.length === 0) {
|
|
470
|
+
this.err(issues, 2, "Workflow must have at least one node");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Rule 3: every node has a non-empty id
|
|
474
|
+
checkRule3(w, issues) {
|
|
475
|
+
if (!Array.isArray(w.nodes)) return;
|
|
476
|
+
for (const node of w.nodes) {
|
|
477
|
+
if (typeof node.id !== "string" || node.id.trim() === "") {
|
|
478
|
+
this.err(issues, 3, `Node "${node.name ?? "unknown"}" is missing a valid id`, node.id);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Rule 4: node ids are unique
|
|
483
|
+
checkRule4(w, issues) {
|
|
484
|
+
if (!Array.isArray(w.nodes)) return;
|
|
485
|
+
const seen = /* @__PURE__ */ new Set();
|
|
486
|
+
for (const node of w.nodes) {
|
|
487
|
+
if (!node.id) continue;
|
|
488
|
+
if (seen.has(node.id)) {
|
|
489
|
+
this.err(issues, 4, `Duplicate node id: "${node.id}"`, node.id);
|
|
490
|
+
}
|
|
491
|
+
seen.add(node.id);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Rule 5: every node has a non-empty type string
|
|
495
|
+
checkRule5(w, issues) {
|
|
496
|
+
if (!Array.isArray(w.nodes)) return;
|
|
497
|
+
for (const node of w.nodes) {
|
|
498
|
+
if (typeof node.type !== "string" || node.type.trim() === "") {
|
|
499
|
+
this.err(issues, 5, `Node "${node.name ?? node.id}" is missing a type`, node.id);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Rule 6: every node has a positive typeVersion number
|
|
504
|
+
checkRule6(w, issues) {
|
|
505
|
+
if (!Array.isArray(w.nodes)) return;
|
|
506
|
+
for (const node of w.nodes) {
|
|
507
|
+
if (typeof node.typeVersion !== "number" || node.typeVersion <= 0) {
|
|
508
|
+
this.err(issues, 6, `Node "${node.name}" has invalid typeVersion: ${String(node.typeVersion)}`, node.id);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// Rule 7: every node has a valid [x, y] position
|
|
513
|
+
checkRule7(w, issues) {
|
|
514
|
+
if (!Array.isArray(w.nodes)) return;
|
|
515
|
+
for (const node of w.nodes) {
|
|
516
|
+
const pos = node.position;
|
|
517
|
+
if (!Array.isArray(pos) || pos.length !== 2 || typeof pos[0] !== "number" || typeof pos[1] !== "number") {
|
|
518
|
+
this.err(issues, 7, `Node "${node.name}" has invalid position (must be [x, y])`, node.id);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// Rule 8: every node has a non-empty name
|
|
523
|
+
checkRule8(w, issues) {
|
|
524
|
+
if (!Array.isArray(w.nodes)) return;
|
|
525
|
+
for (const node of w.nodes) {
|
|
526
|
+
if (typeof node.name !== "string" || node.name.trim() === "") {
|
|
527
|
+
this.err(issues, 8, `Node with id "${node.id}" is missing a name`, node.id);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Rule 9: connections is a plain object
|
|
532
|
+
checkRule9(w, issues) {
|
|
533
|
+
if (typeof w.connections !== "object" || w.connections === null || Array.isArray(w.connections)) {
|
|
534
|
+
this.err(issues, 9, "connections must be a plain object (use {} for single-node workflows)");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Rule 10: every connection target node name exists in nodes
|
|
538
|
+
checkRule10(w, issues) {
|
|
539
|
+
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
540
|
+
const nodeNames = new Set(w.nodes.map((n) => n.name));
|
|
541
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
542
|
+
if (!nodeNames.has(sourceName)) {
|
|
543
|
+
this.err(issues, 10, `Connection source "${sourceName}" does not exist in nodes`);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (typeof outputs !== "object" || outputs === null) continue;
|
|
547
|
+
for (const portGroup of Object.values(outputs)) {
|
|
548
|
+
if (!Array.isArray(portGroup)) continue;
|
|
549
|
+
for (const targets of portGroup) {
|
|
550
|
+
if (!Array.isArray(targets)) continue;
|
|
551
|
+
for (const target of targets) {
|
|
552
|
+
const t = target;
|
|
553
|
+
if (typeof t?.node === "string" && !nodeNames.has(t.node)) {
|
|
554
|
+
this.err(issues, 10, `Connection target "${t.node}" does not exist in nodes`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// Rule 11 (WARN): every non-trigger node has at least one incoming connection
|
|
562
|
+
checkRule11(w, issues) {
|
|
563
|
+
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
564
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
565
|
+
for (const [, outputs] of Object.entries(w.connections)) {
|
|
566
|
+
if (typeof outputs !== "object" || outputs === null) continue;
|
|
567
|
+
for (const portGroup of Object.values(outputs)) {
|
|
568
|
+
if (!Array.isArray(portGroup)) continue;
|
|
569
|
+
for (const targets of portGroup) {
|
|
570
|
+
if (!Array.isArray(targets)) continue;
|
|
571
|
+
for (const target of targets) {
|
|
572
|
+
const t = target;
|
|
573
|
+
if (typeof t?.node === "string") reachable.add(t.node);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
for (const node of w.nodes) {
|
|
579
|
+
if (!this.isTriggerNode(node) && !reachable.has(node.name)) {
|
|
580
|
+
this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// Rule 12: forbidden fields absent from workflow root
|
|
585
|
+
checkRule12(w, issues) {
|
|
586
|
+
const wObj = w;
|
|
587
|
+
for (const field of FORBIDDEN_ON_CREATE) {
|
|
588
|
+
if (field in wObj) {
|
|
589
|
+
this.err(issues, 12, `Forbidden field "${field}" present in workflow \u2014 remove it before deploying`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// Rule 13: settings, if present, is a plain object
|
|
594
|
+
checkRule13(w, issues) {
|
|
595
|
+
if (w.settings !== void 0) {
|
|
596
|
+
if (typeof w.settings !== "object" || w.settings === null || Array.isArray(w.settings)) {
|
|
597
|
+
this.err(issues, 13, "workflow.settings must be a plain object");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Rule 14: at least one trigger node is present
|
|
602
|
+
checkRule14(w, issues) {
|
|
603
|
+
if (!Array.isArray(w.nodes)) return;
|
|
604
|
+
const hasTrigger = w.nodes.some((n) => this.isTriggerNode(n));
|
|
605
|
+
if (!hasTrigger) {
|
|
606
|
+
this.err(issues, 14, "Workflow must contain at least one trigger node");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// Rule 15: node type string matches expected format
|
|
610
|
+
checkRule15(w, issues) {
|
|
611
|
+
if (!Array.isArray(w.nodes)) return;
|
|
612
|
+
for (const node of w.nodes) {
|
|
613
|
+
if (typeof node.type !== "string") continue;
|
|
614
|
+
if (!NODE_TYPE_PATTERN.test(node.type)) {
|
|
615
|
+
this.err(issues, 15, `Node "${node.name}" has malformed type string: "${node.type}"`, node.id);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Rule 16: node names are unique within the workflow
|
|
620
|
+
checkRule16(w, issues) {
|
|
621
|
+
if (!Array.isArray(w.nodes)) return;
|
|
622
|
+
const seen = /* @__PURE__ */ new Set();
|
|
623
|
+
for (const node of w.nodes) {
|
|
624
|
+
if (!node.name) continue;
|
|
625
|
+
if (seen.has(node.name)) {
|
|
626
|
+
this.err(issues, 16, `Duplicate node name: "${node.name}"`, node.id);
|
|
627
|
+
}
|
|
628
|
+
seen.add(node.name);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Rule 17: credentials shape — each entry has id and name
|
|
632
|
+
checkRule17(w, issues) {
|
|
633
|
+
if (!Array.isArray(w.nodes)) return;
|
|
634
|
+
for (const node of w.nodes) {
|
|
635
|
+
if (!node.credentials) continue;
|
|
636
|
+
for (const [credType, credRef] of Object.entries(node.credentials)) {
|
|
637
|
+
if (typeof credRef !== "object" || credRef === null) {
|
|
638
|
+
this.err(issues, 17, `Node "${node.name}" credential "${credType}" must be an object with id and name`, node.id);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const ref = credRef;
|
|
642
|
+
if (typeof ref["id"] !== "string" || ref["id"].trim() === "" || typeof ref["name"] !== "string" || ref["name"].trim() === "") {
|
|
643
|
+
this.err(issues, 17, `Node "${node.name}" credential "${credType}" must have non-empty string id and name fields`, node.id);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// Rule 18 (WARN): AI connections originate from sub-nodes, not the agent/chain root
|
|
649
|
+
checkRule18(w, issues) {
|
|
650
|
+
if (typeof w.connections !== "object" || w.connections === null) return;
|
|
651
|
+
const agentTypes = /* @__PURE__ */ new Set([
|
|
652
|
+
"@n8n/n8n-nodes-langchain.agent",
|
|
653
|
+
"@n8n/n8n-nodes-langchain.chainLlm",
|
|
654
|
+
"@n8n/n8n-nodes-langchain.chainRetrievalQa",
|
|
655
|
+
"@n8n/n8n-nodes-langchain.chainSummarization"
|
|
656
|
+
]);
|
|
657
|
+
if (!Array.isArray(w.nodes)) return;
|
|
658
|
+
const nodesByName = new Map(w.nodes.map((n) => [n.name, n]));
|
|
659
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
660
|
+
const sourceNode = nodesByName.get(sourceName);
|
|
661
|
+
if (!sourceNode) continue;
|
|
662
|
+
if (!agentTypes.has(sourceNode.type)) continue;
|
|
663
|
+
if (typeof outputs !== "object" || outputs === null) continue;
|
|
664
|
+
for (const connType of AI_CONNECTION_TYPES) {
|
|
665
|
+
if (connType in outputs) {
|
|
666
|
+
this.warn(
|
|
667
|
+
issues,
|
|
668
|
+
18,
|
|
669
|
+
`Node "${sourceName}" uses AI connection type "${connType}" as a SOURCE \u2014 AI sub-nodes should be the source, not the agent/chain root`,
|
|
670
|
+
sourceNode.id
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// Rule 19 (WARN): typeVersion is within known safe range for registered node types
|
|
677
|
+
checkRule19(w, issues) {
|
|
678
|
+
if (!Array.isArray(w.nodes)) return;
|
|
679
|
+
for (const node of w.nodes) {
|
|
680
|
+
if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
|
|
681
|
+
if (!this.registry.isVersionSafe(node.type, node.typeVersion)) {
|
|
682
|
+
this.warn(
|
|
683
|
+
issues,
|
|
684
|
+
19,
|
|
685
|
+
`Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
|
|
686
|
+
node.id
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/errors/generation-error.ts
|
|
694
|
+
var GenerationError = class extends KairosError {
|
|
695
|
+
constructor(message, cause) {
|
|
696
|
+
super(message, cause);
|
|
697
|
+
this.name = "GenerationError";
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// src/errors/response-parse-error.ts
|
|
702
|
+
var ResponseParseError = class extends KairosError {
|
|
703
|
+
constructor(message, cause) {
|
|
704
|
+
super(message, cause);
|
|
705
|
+
this.name = "ResponseParseError";
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// src/errors/validation-error.ts
|
|
710
|
+
var ValidationError = class extends KairosError {
|
|
711
|
+
constructor(message, issues) {
|
|
712
|
+
super(message);
|
|
713
|
+
this.issues = issues;
|
|
714
|
+
this.name = "ValidationError";
|
|
715
|
+
}
|
|
716
|
+
issues;
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// src/generation/prompts/v1.ts
|
|
720
|
+
var SYSTEM_PROMPT_V1 = `You are a workflow generation engine for n8n. Your only output is a generate_workflow tool call containing valid n8n workflow JSON. You never respond with prose, explanations, or markdown. If you cannot fulfill the request, set the error field in the tool call.
|
|
721
|
+
|
|
722
|
+
## HARD RULES \u2014 violating any of these causes immediate deployment failure
|
|
723
|
+
|
|
724
|
+
### Forbidden fields \u2014 NEVER include these in the workflow object:
|
|
725
|
+
id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId, activeVersion, pinData, triggerCount, shared, staticData
|
|
726
|
+
|
|
727
|
+
### Required top-level structure:
|
|
728
|
+
{
|
|
729
|
+
"name": "<descriptive name>",
|
|
730
|
+
"nodes": [...],
|
|
731
|
+
"connections": {...},
|
|
732
|
+
"settings": {
|
|
733
|
+
"saveExecutionProgress": true,
|
|
734
|
+
"saveManualExecutions": true,
|
|
735
|
+
"saveDataErrorExecution": "all",
|
|
736
|
+
"saveDataSuccessExecution": "all",
|
|
737
|
+
"executionTimeout": 3600,
|
|
738
|
+
"timezone": "America/New_York",
|
|
739
|
+
"executionOrder": "v1"
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
### Node IDs:
|
|
744
|
+
- Every node.id must be a valid UUID v4 (random hex, format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
|
|
745
|
+
- Never reuse IDs, never use sequential fake IDs like "node-1"
|
|
746
|
+
|
|
747
|
+
### Credentials:
|
|
748
|
+
- Only reference credentials with exact type names (see catalog below)
|
|
749
|
+
- If credential ID is unknown, OMIT the credentials block entirely \u2014 never invent credential IDs
|
|
750
|
+
- Never put API keys or tokens in parameters when a credential type exists
|
|
751
|
+
|
|
752
|
+
### Node names:
|
|
753
|
+
- All node names must be unique within the workflow
|
|
754
|
+
- Use descriptive names: "Fetch Open Invoices" not "HTTP Request 2"
|
|
755
|
+
|
|
756
|
+
### Positioning:
|
|
757
|
+
- Trigger node: [250, 300]
|
|
758
|
+
- Each subsequent step: x + 220 minimum
|
|
759
|
+
- Parallel branches: offset y by \xB1150
|
|
760
|
+
- AI sub-nodes: place below their root node (y + 200)
|
|
761
|
+
|
|
762
|
+
---
|
|
763
|
+
|
|
764
|
+
## CONNECTION RULES \u2014 the most common source of errors
|
|
765
|
+
|
|
766
|
+
### Standard connections (main data flow):
|
|
767
|
+
"NodeA": { "main": [ [ { "node": "NodeB", "type": "main", "index": 0 } ] ] }
|
|
768
|
+
|
|
769
|
+
### AI connections \u2014 CRITICAL: the SUB-NODE is the SOURCE, NOT the agent/chain:
|
|
770
|
+
"OpenAI Chat Model": { "ai_languageModel": [ [ { "node": "AI Agent", "type": "ai_languageModel", "index": 0 } ] ] }
|
|
771
|
+
"Simple Memory": { "ai_memory": [ [ { "node": "AI Agent", "type": "ai_memory", "index": 0 } ] ] }
|
|
772
|
+
"Calculator Tool": { "ai_tool": [ [ { "node": "AI Agent", "type": "ai_tool", "index": 0 } ] ] }
|
|
773
|
+
|
|
774
|
+
The AI Agent node does NOT appear in connections as a source for ai_* types.
|
|
775
|
+
Every AI Agent must have at least one ai_languageModel sub-node connected.
|
|
776
|
+
|
|
777
|
+
### IF node \u2014 two output ports (0 = true, 1 = false):
|
|
778
|
+
"IF Check": { "main": [ [{ "node": "True Path", "type": "main", "index": 0 }], [{ "node": "False Path", "type": "main", "index": 0 }] ] }
|
|
779
|
+
|
|
780
|
+
### Triggers have no incoming connections.
|
|
781
|
+
### Connection keys are NODE NAMES, never node IDs.
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
## NODE CATALOG \u2014 exact type strings and safe typeVersions
|
|
786
|
+
|
|
787
|
+
### Triggers (always at least one required):
|
|
788
|
+
n8n-nodes-base.manualTrigger typeVersion: 1 \u2014 testing only
|
|
789
|
+
n8n-nodes-base.scheduleTrigger typeVersion: 1.2 \u2014 params: rule.interval[{field, ...}]
|
|
790
|
+
n8n-nodes-base.webhook typeVersion: 2 \u2014 params: httpMethod, path, responseMode
|
|
791
|
+
n8n-nodes-base.formTrigger typeVersion: 2.2
|
|
792
|
+
n8n-nodes-base.emailReadImap typeVersion: 2 \u2014 cred: imap
|
|
793
|
+
n8n-nodes-base.errorTrigger typeVersion: 1
|
|
794
|
+
n8n-nodes-base.executeWorkflowTrigger typeVersion: 1.1
|
|
795
|
+
n8n-nodes-base.gmailTrigger typeVersion: 1.2 \u2014 cred: gmailOAuth2
|
|
796
|
+
n8n-nodes-base.slackTrigger typeVersion: 1 \u2014 cred: slackApi
|
|
797
|
+
n8n-nodes-base.telegramTrigger typeVersion: 1.2 \u2014 cred: telegramApi
|
|
798
|
+
n8n-nodes-base.githubTrigger typeVersion: 1 \u2014 cred: githubApi
|
|
799
|
+
n8n-nodes-base.airtableTrigger typeVersion: 1 \u2014 cred: airtableTokenApi
|
|
800
|
+
n8n-nodes-base.notionTrigger typeVersion: 1 \u2014 cred: notionApi
|
|
801
|
+
@n8n/n8n-nodes-langchain.chatTrigger typeVersion: 1.1 \u2014 pairs with AI Agent
|
|
802
|
+
|
|
803
|
+
### Core logic:
|
|
804
|
+
n8n-nodes-base.code typeVersion: 2 \u2014 params: mode, jsCode
|
|
805
|
+
n8n-nodes-base.httpRequest typeVersion: 4.2 \u2014 params: method, url, [sendBody, jsonBody, sendHeaders, headerParameters]
|
|
806
|
+
n8n-nodes-base.set typeVersion: 3.4 \u2014 params: assignments.assignments[{id, name, value, type}]
|
|
807
|
+
n8n-nodes-base.if typeVersion: 2.2 \u2014 params: conditions.conditions[{id, leftValue, rightValue, operator}], combinator
|
|
808
|
+
n8n-nodes-base.switch typeVersion: 3.2 \u2014 multi-branch routing
|
|
809
|
+
n8n-nodes-base.filter typeVersion: 2.2 \u2014 params: conditions (same as IF), 1 output
|
|
810
|
+
n8n-nodes-base.merge typeVersion: 3 \u2014 modes: append/combine/chooseBranch
|
|
811
|
+
n8n-nodes-base.splitInBatches typeVersion: 3 \u2014 output 0=done, output 1=loop body
|
|
812
|
+
n8n-nodes-base.wait typeVersion: 1.1
|
|
813
|
+
n8n-nodes-base.executeWorkflow typeVersion: 1.2
|
|
814
|
+
n8n-nodes-base.respondToWebhook typeVersion: 1.1 \u2014 required when webhook responseMode is "responseNode"
|
|
815
|
+
n8n-nodes-base.noOp typeVersion: 1
|
|
816
|
+
n8n-nodes-base.splitOut typeVersion: 1
|
|
817
|
+
n8n-nodes-base.aggregate typeVersion: 1
|
|
818
|
+
n8n-nodes-base.stickyNote typeVersion: 1 \u2014 never connected, canvas annotation only
|
|
819
|
+
|
|
820
|
+
### Email / messaging:
|
|
821
|
+
n8n-nodes-base.emailSend typeVersion: 2.1 \u2014 cred: smtp
|
|
822
|
+
n8n-nodes-base.slack typeVersion: 2.2 \u2014 cred: slackOAuth2Api \u2014 params: resource, operation, select, channelId{__rl}, text
|
|
823
|
+
n8n-nodes-base.telegram typeVersion: 1.2 \u2014 cred: telegramApi
|
|
824
|
+
n8n-nodes-base.discord typeVersion: 2 \u2014 cred: discordWebhookApi
|
|
825
|
+
|
|
826
|
+
### Google:
|
|
827
|
+
n8n-nodes-base.gmail typeVersion: 2.1 \u2014 cred: gmailOAuth2 \u2014 params: resource, operation
|
|
828
|
+
n8n-nodes-base.googleSheets typeVersion: 4.5 \u2014 cred: googleSheetsOAuth2Api \u2014 params: resource, operation, documentId{__rl}, sheetName{__rl}
|
|
829
|
+
n8n-nodes-base.googleDrive typeVersion: 3 \u2014 cred: googleDriveOAuth2Api
|
|
830
|
+
n8n-nodes-base.googleCalendar typeVersion: 1.3 \u2014 cred: googleCalendarOAuth2Api
|
|
831
|
+
|
|
832
|
+
### Productivity:
|
|
833
|
+
n8n-nodes-base.notion typeVersion: 2.2 \u2014 cred: notionApi
|
|
834
|
+
n8n-nodes-base.airtable typeVersion: 2.1 \u2014 cred: airtableTokenApi
|
|
835
|
+
n8n-nodes-base.github typeVersion: 1.1 \u2014 cred: githubApi
|
|
836
|
+
n8n-nodes-base.jira typeVersion: 1 \u2014 cred: jiraSoftwareCloudApi
|
|
837
|
+
n8n-nodes-base.hubspot typeVersion: 2.1 \u2014 cred: hubspotOAuth2Api
|
|
838
|
+
|
|
839
|
+
### Databases:
|
|
840
|
+
n8n-nodes-base.postgres typeVersion: 2.5 \u2014 cred: postgres
|
|
841
|
+
n8n-nodes-base.mySql typeVersion: 2.4 \u2014 cred: mySql
|
|
842
|
+
n8n-nodes-base.redis typeVersion: 1 \u2014 cred: redis
|
|
843
|
+
n8n-nodes-base.supabase typeVersion: 1 \u2014 cred: supabaseApi
|
|
844
|
+
n8n-nodes-base.awsS3 typeVersion: 2 \u2014 cred: aws
|
|
845
|
+
|
|
846
|
+
### AI \u2014 Root nodes (sit on main data flow):
|
|
847
|
+
@n8n/n8n-nodes-langchain.agent typeVersion: 1.9 \u2014 params: promptType, text (if define), options.systemMessage
|
|
848
|
+
@n8n/n8n-nodes-langchain.chainLlm typeVersion: 1.5
|
|
849
|
+
@n8n/n8n-nodes-langchain.chainRetrievalQa typeVersion: 1.4
|
|
850
|
+
@n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi
|
|
851
|
+
@n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi
|
|
852
|
+
|
|
853
|
+
### AI \u2014 Sub-nodes (sources of ai_* connections):
|
|
854
|
+
@n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel
|
|
855
|
+
@n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel
|
|
856
|
+
@n8n/n8n-nodes-langchain.lmChatGoogleGemini typeVersion: 1 \u2014 cred: googlePalmApi \u2014 ai_languageModel
|
|
857
|
+
@n8n/n8n-nodes-langchain.memoryBufferWindow typeVersion: 1.3 \u2014 \u2014 ai_memory
|
|
858
|
+
@n8n/n8n-nodes-langchain.toolWorkflow typeVersion: 2 \u2014 \u2014 ai_tool
|
|
859
|
+
@n8n/n8n-nodes-langchain.toolCode typeVersion: 1.1 \u2014 \u2014 ai_tool
|
|
860
|
+
@n8n/n8n-nodes-langchain.toolHttpRequest typeVersion: 1.1 \u2014 \u2014 ai_tool
|
|
861
|
+
@n8n/n8n-nodes-langchain.toolCalculator typeVersion: 1 \u2014 \u2014 ai_tool
|
|
862
|
+
|
|
863
|
+
### Resource locator (__rl) format (Google / Slack / Notion modern nodes):
|
|
864
|
+
{ "__rl": true, "mode": "id", "value": "ACTUAL_ID" }
|
|
865
|
+
{ "__rl": true, "mode": "name", "value": "#channel-name" }
|
|
866
|
+
|
|
867
|
+
### App node parameter pattern:
|
|
868
|
+
{ "resource": "message", "operation": "send", ...operation-specific fields }
|
|
869
|
+
|
|
870
|
+
### Schedule Trigger \u2014 daily at 9am example:
|
|
871
|
+
{ "rule": { "interval": [{ "field": "days", "daysInterval": 1, "triggerAtHour": 9, "triggerAtMinute": 0 }] } }
|
|
872
|
+
Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] } }
|
|
873
|
+
|
|
874
|
+
---
|
|
875
|
+
|
|
876
|
+
## PRE-DELIVERY SELF-CHECK (do this before calling the tool):
|
|
877
|
+
1. Every connection source/target name exists in nodes array
|
|
878
|
+
2. No duplicate node names
|
|
879
|
+
3. No duplicate node IDs
|
|
880
|
+
4. No forbidden fields at the workflow root
|
|
881
|
+
5. At least one trigger node present
|
|
882
|
+
6. Every AI Agent has an ai_languageModel sub-node
|
|
883
|
+
7. settings block is complete with executionOrder: "v1"
|
|
884
|
+
|
|
885
|
+
---
|
|
886
|
+
|
|
887
|
+
Respond ONLY with a generate_workflow tool call. No prose. No markdown outside the tool call.
|
|
888
|
+
If the request is impossible or unclear, set the error field instead of generating a workflow.`;
|
|
889
|
+
|
|
890
|
+
// src/generation/prompt-builder.ts
|
|
891
|
+
var PromptBuilder = class {
|
|
892
|
+
build(request, matches) {
|
|
893
|
+
const mode = this.resolveMode(matches);
|
|
894
|
+
const system = this.buildSystem(matches, mode);
|
|
895
|
+
const userMessage = this.buildUserMessage(request, matches, mode);
|
|
896
|
+
return { system, userMessage, mode, matches };
|
|
897
|
+
}
|
|
898
|
+
buildCorrectionMessage(request, matches, allIssues, attempt) {
|
|
899
|
+
const base = this.buildUserMessage(request, matches, this.resolveMode(matches));
|
|
900
|
+
return `${base}
|
|
901
|
+
|
|
902
|
+
IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
|
|
903
|
+
${allIssues.join("\n")}
|
|
904
|
+
|
|
905
|
+
Fix ALL of the above issues in your new response. Do not repeat any of these mistakes.`;
|
|
906
|
+
}
|
|
907
|
+
resolveMode(matches) {
|
|
908
|
+
if (matches.length === 0) return "scratch";
|
|
909
|
+
const top = matches[0];
|
|
910
|
+
if (!top) return "scratch";
|
|
911
|
+
if (top.score >= 0.92) return "direct";
|
|
912
|
+
if (top.score >= 0.72) return "reference";
|
|
913
|
+
return "scratch";
|
|
914
|
+
}
|
|
915
|
+
buildSystem(matches, mode) {
|
|
916
|
+
const blocks = [
|
|
917
|
+
{
|
|
918
|
+
type: "text",
|
|
919
|
+
text: SYSTEM_PROMPT_V1,
|
|
920
|
+
cache_control: { type: "ephemeral" }
|
|
921
|
+
}
|
|
922
|
+
];
|
|
923
|
+
if (mode === "reference" && matches.length > 0) {
|
|
924
|
+
const refText = matches.slice(0, 3).map((m) => {
|
|
925
|
+
const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
926
|
+
return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
|
|
927
|
+
Nodes:
|
|
928
|
+
${nodes}`;
|
|
929
|
+
}).join("\n\n");
|
|
930
|
+
blocks.push({
|
|
931
|
+
type: "text",
|
|
932
|
+
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
933
|
+
|
|
934
|
+
${refText}`
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
if (mode === "direct" && matches[0]) {
|
|
938
|
+
const match = matches[0];
|
|
939
|
+
blocks.push({
|
|
940
|
+
type: "text",
|
|
941
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
|
|
942
|
+
|
|
943
|
+
${JSON.stringify(match.workflow.workflow, null, 2)}`
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
return blocks;
|
|
947
|
+
}
|
|
948
|
+
buildUserMessage(request, _matches, _mode) {
|
|
949
|
+
const namePart = request.name ? `
|
|
950
|
+
Workflow name: "${request.name}"` : "";
|
|
951
|
+
return `Build a workflow that: ${request.description}${namePart}`;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
// src/generation/designer.ts
|
|
956
|
+
var MAX_ATTEMPTS = 3;
|
|
957
|
+
var BASE_TEMPERATURE = 0.2;
|
|
958
|
+
var FINAL_TEMPERATURE = 0.1;
|
|
959
|
+
var GENERATE_WORKFLOW_TOOL = {
|
|
960
|
+
name: "generate_workflow",
|
|
961
|
+
description: "Generate a valid n8n workflow JSON object",
|
|
962
|
+
input_schema: {
|
|
963
|
+
type: "object",
|
|
964
|
+
properties: {
|
|
965
|
+
workflow: {
|
|
966
|
+
type: "object",
|
|
967
|
+
description: "The complete n8n workflow object",
|
|
968
|
+
properties: {
|
|
969
|
+
name: { type: "string" },
|
|
970
|
+
nodes: { type: "array" },
|
|
971
|
+
connections: { type: "object" },
|
|
972
|
+
settings: { type: "object" }
|
|
973
|
+
},
|
|
974
|
+
required: ["name", "nodes", "connections"]
|
|
975
|
+
},
|
|
976
|
+
credentialsNeeded: {
|
|
977
|
+
type: "array",
|
|
978
|
+
description: "List of credentials the user must configure before activating",
|
|
979
|
+
items: {
|
|
980
|
+
type: "object",
|
|
981
|
+
properties: {
|
|
982
|
+
service: { type: "string" },
|
|
983
|
+
credentialType: { type: "string" },
|
|
984
|
+
description: { type: "string" }
|
|
985
|
+
},
|
|
986
|
+
required: ["service", "credentialType", "description"]
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
error: {
|
|
990
|
+
type: "string",
|
|
991
|
+
description: "Set this if the request cannot be fulfilled \u2014 explain why"
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
required: ["workflow"]
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
var WorkflowDesigner = class {
|
|
998
|
+
constructor(anthropic, model, logger) {
|
|
999
|
+
this.anthropic = anthropic;
|
|
1000
|
+
this.model = model;
|
|
1001
|
+
this.logger = logger;
|
|
1002
|
+
this.validator = new N8nValidator();
|
|
1003
|
+
this.promptBuilder = new PromptBuilder();
|
|
1004
|
+
}
|
|
1005
|
+
anthropic;
|
|
1006
|
+
model;
|
|
1007
|
+
logger;
|
|
1008
|
+
validator;
|
|
1009
|
+
promptBuilder;
|
|
1010
|
+
async design(request, matches) {
|
|
1011
|
+
const allIssues = [];
|
|
1012
|
+
const attemptMetadata = [];
|
|
1013
|
+
let attempts = 0;
|
|
1014
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
1015
|
+
attempts = attempt;
|
|
1016
|
+
const temperature = attempt === MAX_ATTEMPTS ? FINAL_TEMPERATURE : BASE_TEMPERATURE;
|
|
1017
|
+
const built = this.promptBuilder.build(request, matches);
|
|
1018
|
+
let userMessage;
|
|
1019
|
+
if (attempt === 1) {
|
|
1020
|
+
userMessage = built.userMessage;
|
|
1021
|
+
this.logger.debug("WorkflowDesigner: attempt 1", { description: request.description });
|
|
1022
|
+
} else {
|
|
1023
|
+
const issueLines = allIssues.map(
|
|
1024
|
+
(i) => `- [Rule ${i.rule}] ${i.message}${i.nodeId ? ` (node: ${i.nodeId})` : ""}`
|
|
1025
|
+
);
|
|
1026
|
+
userMessage = this.promptBuilder.buildCorrectionMessage(request, matches, issueLines, attempt - 1);
|
|
1027
|
+
this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: allIssues.length });
|
|
1028
|
+
}
|
|
1029
|
+
const start = Date.now();
|
|
1030
|
+
const message = await this.callClaude(built.system, userMessage, temperature);
|
|
1031
|
+
const durationMs = Date.now() - start;
|
|
1032
|
+
const parsed = this.extractToolUse(message);
|
|
1033
|
+
if (parsed.error) {
|
|
1034
|
+
throw new GenerationError(`Claude declined to generate workflow: ${parsed.error}`);
|
|
1035
|
+
}
|
|
1036
|
+
const validation = this.validator.validate(parsed.workflow);
|
|
1037
|
+
const errors = validation.issues.filter((i) => i.severity === "error");
|
|
1038
|
+
attemptMetadata.push({
|
|
1039
|
+
attempt,
|
|
1040
|
+
temperature,
|
|
1041
|
+
durationMs,
|
|
1042
|
+
tokensInput: message.usage.input_tokens,
|
|
1043
|
+
tokensOutput: message.usage.output_tokens,
|
|
1044
|
+
validationPassed: validation.valid,
|
|
1045
|
+
issues: errors
|
|
1046
|
+
});
|
|
1047
|
+
if (validation.valid) {
|
|
1048
|
+
return { workflow: parsed.workflow, credentialsNeeded: parsed.credentialsNeeded, attempts, attemptMetadata };
|
|
1049
|
+
}
|
|
1050
|
+
allIssues.push(...errors);
|
|
1051
|
+
this.logger.warn(`WorkflowDesigner: validation failed on attempt ${attempt}`, {
|
|
1052
|
+
newErrors: errors.length,
|
|
1053
|
+
totalErrors: allIssues.length
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
throw new ValidationError(
|
|
1057
|
+
`Workflow failed validation after ${MAX_ATTEMPTS} attempts (${allIssues.length} total errors)`,
|
|
1058
|
+
allIssues
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
async callClaude(system, userMessage, temperature) {
|
|
1062
|
+
try {
|
|
1063
|
+
return await this.anthropic.messages.create({
|
|
1064
|
+
model: this.model,
|
|
1065
|
+
max_tokens: 8192,
|
|
1066
|
+
temperature,
|
|
1067
|
+
system,
|
|
1068
|
+
messages: [{ role: "user", content: userMessage }],
|
|
1069
|
+
tools: [GENERATE_WORKFLOW_TOOL],
|
|
1070
|
+
tool_choice: { type: "tool", name: "generate_workflow" }
|
|
1071
|
+
});
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
throw new GenerationError("Anthropic API call failed", err);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
extractToolUse(message) {
|
|
1077
|
+
const toolUseBlock = message.content.find(
|
|
1078
|
+
(block) => block.type === "tool_use"
|
|
1079
|
+
);
|
|
1080
|
+
if (!toolUseBlock) {
|
|
1081
|
+
throw new ResponseParseError(
|
|
1082
|
+
"Claude response contained no tool_use block \u2014 forced tool_choice failed unexpectedly"
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const input = toolUseBlock.input;
|
|
1086
|
+
if (typeof input["error"] === "string") {
|
|
1087
|
+
return {
|
|
1088
|
+
workflow: { name: "", nodes: [], connections: {} },
|
|
1089
|
+
credentialsNeeded: [],
|
|
1090
|
+
error: input["error"]
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
if (!input["workflow"] || typeof input["workflow"] !== "object") {
|
|
1094
|
+
throw new ResponseParseError("generate_workflow tool call missing workflow field");
|
|
1095
|
+
}
|
|
1096
|
+
const workflow = input["workflow"];
|
|
1097
|
+
const credentialsNeeded = input["credentialsNeeded"] ?? [];
|
|
1098
|
+
return { workflow, credentialsNeeded };
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
// src/telemetry/collector.ts
|
|
1103
|
+
var import_promises = require("fs/promises");
|
|
1104
|
+
var import_node_path = require("path");
|
|
1105
|
+
var import_node_os = require("os");
|
|
1106
|
+
var TelemetryCollector = class {
|
|
1107
|
+
dir;
|
|
1108
|
+
sessionId;
|
|
1109
|
+
constructor(dir) {
|
|
1110
|
+
this.dir = dir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "telemetry");
|
|
1111
|
+
this.sessionId = generateUUID();
|
|
1112
|
+
}
|
|
1113
|
+
async emit(eventType, data) {
|
|
1114
|
+
const event = {
|
|
1115
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1116
|
+
sessionId: this.sessionId,
|
|
1117
|
+
eventType,
|
|
1118
|
+
data
|
|
1119
|
+
};
|
|
1120
|
+
await (0, import_promises.mkdir)(this.dir, { recursive: true });
|
|
1121
|
+
const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
|
|
1122
|
+
const filepath = (0, import_node_path.join)(this.dir, filename);
|
|
1123
|
+
await (0, import_promises.appendFile)(filepath, JSON.stringify(event) + "\n", "utf-8");
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
// src/utils/logger.ts
|
|
1128
|
+
var nullLogger = {
|
|
1129
|
+
debug() {
|
|
1130
|
+
},
|
|
1131
|
+
info() {
|
|
1132
|
+
},
|
|
1133
|
+
warn() {
|
|
1134
|
+
},
|
|
1135
|
+
error() {
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// src/client.ts
|
|
1140
|
+
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1141
|
+
var Kairos = class {
|
|
1142
|
+
provider;
|
|
1143
|
+
designer;
|
|
1144
|
+
validator;
|
|
1145
|
+
library;
|
|
1146
|
+
logger;
|
|
1147
|
+
telemetry;
|
|
1148
|
+
model;
|
|
1149
|
+
constructor(options) {
|
|
1150
|
+
const logger = options.logger ?? nullLogger;
|
|
1151
|
+
this.model = options.model ?? DEFAULT_MODEL;
|
|
1152
|
+
const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
|
|
1153
|
+
const apiClient = new N8nApiClient(options.n8nBaseUrl, options.n8nApiKey, logger);
|
|
1154
|
+
const stripper = new N8nFieldStripper();
|
|
1155
|
+
this.provider = new N8nProvider(apiClient, stripper);
|
|
1156
|
+
this.designer = new WorkflowDesigner(anthropic, this.model, logger);
|
|
1157
|
+
this.validator = new N8nValidator();
|
|
1158
|
+
this.library = options.library ?? new NullLibrary();
|
|
1159
|
+
this.logger = logger;
|
|
1160
|
+
if (options.telemetry === true) {
|
|
1161
|
+
this.telemetry = new TelemetryCollector();
|
|
1162
|
+
} else if (typeof options.telemetry === "string") {
|
|
1163
|
+
this.telemetry = new TelemetryCollector(options.telemetry);
|
|
1164
|
+
} else {
|
|
1165
|
+
this.telemetry = null;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
async build(description, options) {
|
|
1169
|
+
this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
|
|
1170
|
+
const buildStart = Date.now();
|
|
1171
|
+
await this.telemetry?.emit("build_start", {
|
|
1172
|
+
description,
|
|
1173
|
+
model: this.model,
|
|
1174
|
+
dryRun: options?.dryRun ?? false
|
|
1175
|
+
});
|
|
1176
|
+
await this.library.initialize();
|
|
1177
|
+
const matches = await this.library.search(description);
|
|
1178
|
+
const designResult = await this.designer.design(
|
|
1179
|
+
{ description, ...options?.name ? { name: options.name } : {} },
|
|
1180
|
+
matches
|
|
1181
|
+
);
|
|
1182
|
+
for (const meta of designResult.attemptMetadata) {
|
|
1183
|
+
await this.telemetry?.emit("generation_attempt", {
|
|
1184
|
+
description,
|
|
1185
|
+
attempt: meta.attempt,
|
|
1186
|
+
temperature: meta.temperature,
|
|
1187
|
+
durationMs: meta.durationMs,
|
|
1188
|
+
tokensInput: meta.tokensInput,
|
|
1189
|
+
tokensOutput: meta.tokensOutput,
|
|
1190
|
+
validationPassed: meta.validationPassed,
|
|
1191
|
+
issueCount: meta.issues.length,
|
|
1192
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message }))
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
|
|
1196
|
+
if (options?.dryRun) {
|
|
1197
|
+
const result2 = {
|
|
1198
|
+
workflowId: null,
|
|
1199
|
+
name: workflow.name,
|
|
1200
|
+
credentialsNeeded: designResult.credentialsNeeded,
|
|
1201
|
+
activationRequired: true,
|
|
1202
|
+
generationAttempts: designResult.attempts,
|
|
1203
|
+
dryRun: true
|
|
1204
|
+
};
|
|
1205
|
+
const totalTokensInput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
1206
|
+
const totalTokensOutput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
1207
|
+
await this.telemetry?.emit("build_complete", {
|
|
1208
|
+
description,
|
|
1209
|
+
success: true,
|
|
1210
|
+
totalAttempts: designResult.attempts,
|
|
1211
|
+
totalDurationMs: Date.now() - buildStart,
|
|
1212
|
+
totalTokensInput: totalTokensInput2,
|
|
1213
|
+
totalTokensOutput: totalTokensOutput2,
|
|
1214
|
+
workflowName: workflow.name,
|
|
1215
|
+
workflowId: null,
|
|
1216
|
+
dryRun: true,
|
|
1217
|
+
credentialsNeeded: designResult.credentialsNeeded.length
|
|
1218
|
+
});
|
|
1219
|
+
return result2;
|
|
1220
|
+
}
|
|
1221
|
+
const deployed = await this.provider.deploy(workflow);
|
|
1222
|
+
this.library.save(workflow, { description }).catch((err) => {
|
|
1223
|
+
this.logger.warn("Failed to save workflow to library (non-fatal)", { err: String(err) });
|
|
1224
|
+
});
|
|
1225
|
+
if (options?.activate) {
|
|
1226
|
+
await this.provider.activate(deployed.workflowId);
|
|
1227
|
+
}
|
|
1228
|
+
const result = {
|
|
1229
|
+
workflowId: deployed.workflowId,
|
|
1230
|
+
name: deployed.name,
|
|
1231
|
+
credentialsNeeded: designResult.credentialsNeeded,
|
|
1232
|
+
activationRequired: !options?.activate,
|
|
1233
|
+
generationAttempts: designResult.attempts,
|
|
1234
|
+
dryRun: false
|
|
1235
|
+
};
|
|
1236
|
+
const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
1237
|
+
const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
1238
|
+
await this.telemetry?.emit("build_complete", {
|
|
1239
|
+
description,
|
|
1240
|
+
success: true,
|
|
1241
|
+
totalAttempts: designResult.attempts,
|
|
1242
|
+
totalDurationMs: Date.now() - buildStart,
|
|
1243
|
+
totalTokensInput,
|
|
1244
|
+
totalTokensOutput,
|
|
1245
|
+
workflowName: deployed.name,
|
|
1246
|
+
workflowId: deployed.workflowId,
|
|
1247
|
+
dryRun: false,
|
|
1248
|
+
credentialsNeeded: designResult.credentialsNeeded.length
|
|
1249
|
+
});
|
|
1250
|
+
return result;
|
|
1251
|
+
}
|
|
1252
|
+
async update(id, description) {
|
|
1253
|
+
this.logger.info("Kairos.update", { id, description });
|
|
1254
|
+
const matches = await this.library.search(description);
|
|
1255
|
+
const designResult = await this.designer.design({ description }, matches);
|
|
1256
|
+
const deployed = await this.provider.update(id, designResult.workflow);
|
|
1257
|
+
return {
|
|
1258
|
+
workflowId: deployed.workflowId,
|
|
1259
|
+
name: deployed.name,
|
|
1260
|
+
credentialsNeeded: designResult.credentialsNeeded,
|
|
1261
|
+
activationRequired: true,
|
|
1262
|
+
generationAttempts: designResult.attempts,
|
|
1263
|
+
dryRun: false
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
async get(id) {
|
|
1267
|
+
return this.provider.get(id);
|
|
1268
|
+
}
|
|
1269
|
+
async list() {
|
|
1270
|
+
return this.provider.list();
|
|
1271
|
+
}
|
|
1272
|
+
async activate(id) {
|
|
1273
|
+
await this.provider.activate(id);
|
|
1274
|
+
}
|
|
1275
|
+
async deactivate(id) {
|
|
1276
|
+
await this.provider.deactivate(id);
|
|
1277
|
+
}
|
|
1278
|
+
async delete(id, options) {
|
|
1279
|
+
await this.provider.delete(id, options);
|
|
1280
|
+
}
|
|
1281
|
+
async executions(workflowId, filter) {
|
|
1282
|
+
return this.provider.executions(workflowId, filter);
|
|
1283
|
+
}
|
|
1284
|
+
async execution(id) {
|
|
1285
|
+
return this.provider.execution(id);
|
|
1286
|
+
}
|
|
1287
|
+
async listTags() {
|
|
1288
|
+
return this.provider.listTags();
|
|
1289
|
+
}
|
|
1290
|
+
async createTag(name) {
|
|
1291
|
+
return this.provider.createTag(name);
|
|
1292
|
+
}
|
|
1293
|
+
async tag(workflowId, tagIds) {
|
|
1294
|
+
await this.provider.tag(workflowId, tagIds);
|
|
1295
|
+
}
|
|
1296
|
+
async untag(workflowId, tagIds) {
|
|
1297
|
+
await this.provider.untag(workflowId, tagIds);
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
// src/cli.ts
|
|
1302
|
+
var HELP = `
|
|
1303
|
+
Kairos SDK \u2014 LLM-powered n8n workflow generation
|
|
1304
|
+
|
|
1305
|
+
Usage:
|
|
1306
|
+
kairos build <description> [options]
|
|
1307
|
+
kairos list
|
|
1308
|
+
kairos get <id>
|
|
1309
|
+
kairos activate <id>
|
|
1310
|
+
kairos deactivate <id>
|
|
1311
|
+
kairos delete <id> --confirm
|
|
1312
|
+
|
|
1313
|
+
Build options:
|
|
1314
|
+
--dry-run Generate and validate without deploying
|
|
1315
|
+
--name <name> Override the generated workflow name
|
|
1316
|
+
--activate Activate the workflow after deployment
|
|
1317
|
+
|
|
1318
|
+
Environment variables:
|
|
1319
|
+
ANTHROPIC_API_KEY Anthropic API key (required)
|
|
1320
|
+
N8N_BASE_URL n8n instance URL (required)
|
|
1321
|
+
N8N_API_KEY n8n API key (required)
|
|
1322
|
+
KAIROS_MODEL Claude model override (default: claude-sonnet-4-6)
|
|
1323
|
+
KAIROS_TELEMETRY Set to "true" or a directory path to enable telemetry logging
|
|
1324
|
+
`;
|
|
1325
|
+
function getEnvOrExit(name) {
|
|
1326
|
+
const val = process.env[name];
|
|
1327
|
+
if (!val) {
|
|
1328
|
+
console.error(`Missing required environment variable: ${name}`);
|
|
1329
|
+
process.exit(1);
|
|
1330
|
+
}
|
|
1331
|
+
return val;
|
|
1332
|
+
}
|
|
1333
|
+
function parseArgs(argv) {
|
|
1334
|
+
const args = argv.slice(2);
|
|
1335
|
+
const command = args[0] ?? "";
|
|
1336
|
+
const positional = [];
|
|
1337
|
+
const flags = {};
|
|
1338
|
+
for (let i = 1; i < args.length; i++) {
|
|
1339
|
+
const arg = args[i];
|
|
1340
|
+
if (arg.startsWith("--")) {
|
|
1341
|
+
const key = arg.slice(2);
|
|
1342
|
+
const next = args[i + 1];
|
|
1343
|
+
if (next && !next.startsWith("--")) {
|
|
1344
|
+
flags[key] = next;
|
|
1345
|
+
i++;
|
|
1346
|
+
} else {
|
|
1347
|
+
flags[key] = true;
|
|
1348
|
+
}
|
|
1349
|
+
} else {
|
|
1350
|
+
positional.push(arg);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return { command, positional, flags };
|
|
1354
|
+
}
|
|
1355
|
+
function createClient() {
|
|
1356
|
+
const telemetryEnv = process.env["KAIROS_TELEMETRY"];
|
|
1357
|
+
let telemetry;
|
|
1358
|
+
if (telemetryEnv === "true") {
|
|
1359
|
+
telemetry = true;
|
|
1360
|
+
} else if (telemetryEnv && telemetryEnv !== "false") {
|
|
1361
|
+
telemetry = telemetryEnv;
|
|
1362
|
+
}
|
|
1363
|
+
return new Kairos({
|
|
1364
|
+
anthropicApiKey: getEnvOrExit("ANTHROPIC_API_KEY"),
|
|
1365
|
+
n8nBaseUrl: getEnvOrExit("N8N_BASE_URL"),
|
|
1366
|
+
n8nApiKey: getEnvOrExit("N8N_API_KEY"),
|
|
1367
|
+
...process.env["KAIROS_MODEL"] ? { model: process.env["KAIROS_MODEL"] } : {},
|
|
1368
|
+
...telemetry !== void 0 ? { telemetry } : {},
|
|
1369
|
+
logger: {
|
|
1370
|
+
debug: () => {
|
|
1371
|
+
},
|
|
1372
|
+
info: () => {
|
|
1373
|
+
},
|
|
1374
|
+
warn: (msg) => console.error(`[warn] ${msg}`),
|
|
1375
|
+
error: (msg) => console.error(`[error] ${msg}`)
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
async function handleBuild(positional, flags) {
|
|
1380
|
+
const description = positional[0];
|
|
1381
|
+
if (!description) {
|
|
1382
|
+
console.error("Usage: kairos build <description> [--dry-run] [--name <name>] [--activate]");
|
|
1383
|
+
process.exit(1);
|
|
1384
|
+
}
|
|
1385
|
+
const kairos = createClient();
|
|
1386
|
+
const start = Date.now();
|
|
1387
|
+
console.error(`Generating workflow...`);
|
|
1388
|
+
const result = await kairos.build(description, {
|
|
1389
|
+
dryRun: flags["dry-run"] === true,
|
|
1390
|
+
...typeof flags["name"] === "string" ? { name: flags["name"] } : {},
|
|
1391
|
+
activate: flags["activate"] === true
|
|
1392
|
+
});
|
|
1393
|
+
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
1394
|
+
console.error(`Done in ${elapsed}s (${result.generationAttempts} attempt${result.generationAttempts > 1 ? "s" : ""})`);
|
|
1395
|
+
console.error("");
|
|
1396
|
+
console.log(JSON.stringify({
|
|
1397
|
+
workflowId: result.workflowId,
|
|
1398
|
+
name: result.name,
|
|
1399
|
+
generationAttempts: result.generationAttempts,
|
|
1400
|
+
activationRequired: result.activationRequired,
|
|
1401
|
+
dryRun: result.dryRun,
|
|
1402
|
+
credentialsNeeded: result.credentialsNeeded
|
|
1403
|
+
}, null, 2));
|
|
1404
|
+
}
|
|
1405
|
+
async function handleList() {
|
|
1406
|
+
const kairos = createClient();
|
|
1407
|
+
const workflows = await kairos.list();
|
|
1408
|
+
if (workflows.length === 0) {
|
|
1409
|
+
console.log("No workflows found.");
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
for (const w of workflows) {
|
|
1413
|
+
const status = w.active ? "active" : "inactive";
|
|
1414
|
+
console.log(` ${w.id} ${status.padEnd(8)} ${w.name}`);
|
|
1415
|
+
}
|
|
1416
|
+
console.log(`
|
|
1417
|
+
${workflows.length} workflow(s)`);
|
|
1418
|
+
}
|
|
1419
|
+
async function handleGet(positional) {
|
|
1420
|
+
const id = positional[0];
|
|
1421
|
+
if (!id) {
|
|
1422
|
+
console.error("Usage: kairos get <workflow-id>");
|
|
1423
|
+
process.exit(1);
|
|
1424
|
+
}
|
|
1425
|
+
const kairos = createClient();
|
|
1426
|
+
const workflow = await kairos.get(id);
|
|
1427
|
+
console.log(JSON.stringify(workflow, null, 2));
|
|
1428
|
+
}
|
|
1429
|
+
async function handleActivate(positional) {
|
|
1430
|
+
const id = positional[0];
|
|
1431
|
+
if (!id) {
|
|
1432
|
+
console.error("Usage: kairos activate <workflow-id>");
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
const kairos = createClient();
|
|
1436
|
+
await kairos.activate(id);
|
|
1437
|
+
console.log(`Activated workflow ${id}`);
|
|
1438
|
+
}
|
|
1439
|
+
async function handleDeactivate(positional) {
|
|
1440
|
+
const id = positional[0];
|
|
1441
|
+
if (!id) {
|
|
1442
|
+
console.error("Usage: kairos deactivate <workflow-id>");
|
|
1443
|
+
process.exit(1);
|
|
1444
|
+
}
|
|
1445
|
+
const kairos = createClient();
|
|
1446
|
+
await kairos.deactivate(id);
|
|
1447
|
+
console.log(`Deactivated workflow ${id}`);
|
|
1448
|
+
}
|
|
1449
|
+
async function handleDelete(positional, flags) {
|
|
1450
|
+
const id = positional[0];
|
|
1451
|
+
if (!id) {
|
|
1452
|
+
console.error("Usage: kairos delete <workflow-id> --confirm");
|
|
1453
|
+
process.exit(1);
|
|
1454
|
+
}
|
|
1455
|
+
if (flags["confirm"] !== true) {
|
|
1456
|
+
console.error("Refusing to delete without --confirm flag.");
|
|
1457
|
+
process.exit(1);
|
|
1458
|
+
}
|
|
1459
|
+
const kairos = createClient();
|
|
1460
|
+
await kairos.delete(id, { confirm: true });
|
|
1461
|
+
console.log(`Deleted workflow ${id}`);
|
|
1462
|
+
}
|
|
1463
|
+
async function main() {
|
|
1464
|
+
const { command, positional, flags } = parseArgs(process.argv);
|
|
1465
|
+
if (!command || command === "help" || flags["help"] === true) {
|
|
1466
|
+
console.log(HELP);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
switch (command) {
|
|
1470
|
+
case "build":
|
|
1471
|
+
await handleBuild(positional, flags);
|
|
1472
|
+
break;
|
|
1473
|
+
case "list":
|
|
1474
|
+
await handleList();
|
|
1475
|
+
break;
|
|
1476
|
+
case "get":
|
|
1477
|
+
await handleGet(positional);
|
|
1478
|
+
break;
|
|
1479
|
+
case "activate":
|
|
1480
|
+
await handleActivate(positional);
|
|
1481
|
+
break;
|
|
1482
|
+
case "deactivate":
|
|
1483
|
+
await handleDeactivate(positional);
|
|
1484
|
+
break;
|
|
1485
|
+
case "delete":
|
|
1486
|
+
await handleDelete(positional, flags);
|
|
1487
|
+
break;
|
|
1488
|
+
default:
|
|
1489
|
+
console.error(`Unknown command: ${command}`);
|
|
1490
|
+
console.log(HELP);
|
|
1491
|
+
process.exit(1);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
main().catch((err) => {
|
|
1495
|
+
if (err instanceof Error) {
|
|
1496
|
+
console.error(`Error: ${err.message}`);
|
|
1497
|
+
if ("issues" in err) {
|
|
1498
|
+
const issues = err.issues;
|
|
1499
|
+
for (const issue of issues) {
|
|
1500
|
+
console.error(` [Rule ${issue.rule}] ${issue.message}`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
} else {
|
|
1504
|
+
console.error(err);
|
|
1505
|
+
}
|
|
1506
|
+
process.exit(1);
|
|
1507
|
+
});
|
|
1508
|
+
//# sourceMappingURL=cli.cjs.map
|