@kairos-sdk/core 0.2.0 → 0.3.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/README.md +111 -5
- package/dist/{chunk-QQJDLS5A.js → chunk-DDV7ZART.js} +102 -891
- package/dist/chunk-DDV7ZART.js.map +1 -0
- package/dist/chunk-Q77XA7UC.js +809 -0
- package/dist/chunk-Q77XA7UC.js.map +1 -0
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +12 -10
- package/dist/mcp-server.cjs +1936 -0
- package/dist/mcp-server.cjs.map +1 -0
- package/dist/mcp-server.d.cts +1 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +376 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +24 -5
- package/dist/chunk-QQJDLS5A.js.map +0 -1
|
@@ -0,0 +1,1936 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/mcp-server.ts
|
|
5
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
6
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7
|
+
var import_zod = require("zod");
|
|
8
|
+
|
|
9
|
+
// src/library/file-library.ts
|
|
10
|
+
var import_promises = require("fs/promises");
|
|
11
|
+
var import_node_path = require("path");
|
|
12
|
+
var import_node_os = require("os");
|
|
13
|
+
|
|
14
|
+
// src/utils/uuid.ts
|
|
15
|
+
function generateUUID() {
|
|
16
|
+
return crypto.randomUUID();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/utils/thresholds.ts
|
|
20
|
+
var DIRECT_THRESHOLD = 0.92;
|
|
21
|
+
var REFERENCE_THRESHOLD = 0.72;
|
|
22
|
+
function scoreToMode(score) {
|
|
23
|
+
if (score >= DIRECT_THRESHOLD) return "direct";
|
|
24
|
+
if (score >= REFERENCE_THRESHOLD) return "reference";
|
|
25
|
+
return "scratch";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/library/scorer.ts
|
|
29
|
+
var WEIGHTS = {
|
|
30
|
+
tfidf: 0.35,
|
|
31
|
+
nodeFingerprint: 0.3,
|
|
32
|
+
outcome: 0.2,
|
|
33
|
+
deploy: 0.15
|
|
34
|
+
};
|
|
35
|
+
var NODE_KEYWORDS = {
|
|
36
|
+
slack: ["slack", "slackApi"],
|
|
37
|
+
email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
|
|
38
|
+
webhook: ["webhook", "webhookTrigger"],
|
|
39
|
+
schedule: ["scheduleTrigger", "cron"],
|
|
40
|
+
http: ["httpRequest"],
|
|
41
|
+
sheets: ["googleSheets"],
|
|
42
|
+
github: ["github", "githubTrigger"],
|
|
43
|
+
telegram: ["telegram", "telegramTrigger"],
|
|
44
|
+
ai: ["agent", "openAi", "lmChatOpenAi", "lmChatAnthropic", "chainLlm", "chainSummarization"],
|
|
45
|
+
memory: ["memoryBufferWindow", "memoryXata", "memoryPostgres"],
|
|
46
|
+
vector: ["vectorStoreInMemory", "vectorStorePinecone", "vectorStoreQdrant"],
|
|
47
|
+
database: ["postgres", "mySql", "redis", "mongoDb"],
|
|
48
|
+
airtable: ["airtable"],
|
|
49
|
+
notion: ["notion"],
|
|
50
|
+
s3: ["awsS3"],
|
|
51
|
+
code: ["code"],
|
|
52
|
+
merge: ["merge"],
|
|
53
|
+
switch: ["switch"],
|
|
54
|
+
if: ["if"],
|
|
55
|
+
wait: ["wait"],
|
|
56
|
+
rss: ["rssFeedRead", "rssFeedReadTrigger"],
|
|
57
|
+
form: ["formTrigger"],
|
|
58
|
+
set: ["set"],
|
|
59
|
+
split: ["splitInBatches"],
|
|
60
|
+
filter: ["filter"],
|
|
61
|
+
telegram_trigger: ["telegramTrigger"],
|
|
62
|
+
stripe: ["stripe"]
|
|
63
|
+
};
|
|
64
|
+
function extractQueryFingerprint(description) {
|
|
65
|
+
const lower = description.toLowerCase();
|
|
66
|
+
const matches = /* @__PURE__ */ new Set();
|
|
67
|
+
for (const [keyword, nodeTypes] of Object.entries(NODE_KEYWORDS)) {
|
|
68
|
+
if (lower.includes(keyword)) {
|
|
69
|
+
for (const nt of nodeTypes) matches.add(nt);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (/\bevery\b|\bdaily\b|\bhourly\b|\bweekly\b|\bmonthly\b|\bcron\b|\bschedule\b|\bat \d/.test(lower)) {
|
|
73
|
+
matches.add("scheduleTrigger");
|
|
74
|
+
}
|
|
75
|
+
if (/\bwebhook\b|\breceive\b.*\bpost\b|\bpost\b.*\brequest\b/.test(lower)) {
|
|
76
|
+
matches.add("webhook");
|
|
77
|
+
}
|
|
78
|
+
if (/\bchat\b|\bchatbot\b|\bconversation\b/.test(lower)) {
|
|
79
|
+
matches.add("chatTrigger");
|
|
80
|
+
}
|
|
81
|
+
if (/\bai\b|\bllm\b|\bgpt\b|\bclaude\b|\bagent\b|\bsummariz/.test(lower)) {
|
|
82
|
+
matches.add("agent");
|
|
83
|
+
}
|
|
84
|
+
return matches;
|
|
85
|
+
}
|
|
86
|
+
function extractWorkflowFingerprint(w) {
|
|
87
|
+
const fp = /* @__PURE__ */ new Set();
|
|
88
|
+
for (const node of w.workflow.nodes) {
|
|
89
|
+
const bare = node.type.split(".").pop() ?? "";
|
|
90
|
+
fp.add(bare);
|
|
91
|
+
}
|
|
92
|
+
return fp;
|
|
93
|
+
}
|
|
94
|
+
function jaccardSimilarity(a, b) {
|
|
95
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
96
|
+
let intersection = 0;
|
|
97
|
+
for (const item of a) {
|
|
98
|
+
if (b.has(item)) intersection++;
|
|
99
|
+
}
|
|
100
|
+
const union = a.size + b.size - intersection;
|
|
101
|
+
return union > 0 ? intersection / union : 0;
|
|
102
|
+
}
|
|
103
|
+
function outcomeScore(w) {
|
|
104
|
+
const stats = w.outcomeStats;
|
|
105
|
+
if (!stats || stats.totalUses === 0) return 0.5;
|
|
106
|
+
const passRate = stats.firstTryPasses / stats.totalUses;
|
|
107
|
+
const avgAttempts = stats.totalAttempts / stats.totalUses;
|
|
108
|
+
const attemptPenalty = Math.max(0, 1 - (avgAttempts - 1) * 0.3);
|
|
109
|
+
return passRate * 0.6 + attemptPenalty * 0.4;
|
|
110
|
+
}
|
|
111
|
+
function deployScore(w) {
|
|
112
|
+
return 1 + Math.log(w.deployCount + 1) * 0.1;
|
|
113
|
+
}
|
|
114
|
+
function hybridScore(queryTokens, queryDescription, workflows, docTokenArrays, idf) {
|
|
115
|
+
const queryFp = extractQueryFingerprint(queryDescription);
|
|
116
|
+
const ceiling = queryTokens.reduce((sum, qt) => sum + (idf.get(qt) ?? 0), 0) || 1;
|
|
117
|
+
return workflows.map((w, i) => {
|
|
118
|
+
const docTokens = docTokenArrays[i];
|
|
119
|
+
let tfidfRaw = 0;
|
|
120
|
+
const docFreq = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const t of docTokens) {
|
|
122
|
+
docFreq.set(t, (docFreq.get(t) ?? 0) + 1);
|
|
123
|
+
}
|
|
124
|
+
for (const qt of queryTokens) {
|
|
125
|
+
const tf = docTokens.length > 0 ? (docFreq.get(qt) ?? 0) / docTokens.length : 0;
|
|
126
|
+
const idfVal = idf.get(qt) ?? 0;
|
|
127
|
+
tfidfRaw += tf * idfVal;
|
|
128
|
+
}
|
|
129
|
+
const tfidf = Math.min(tfidfRaw / ceiling, 1);
|
|
130
|
+
const workflowFp = extractWorkflowFingerprint(w);
|
|
131
|
+
const nodeFingerprint = queryFp.size > 0 ? jaccardSimilarity(queryFp, workflowFp) : 0;
|
|
132
|
+
const outcome = outcomeScore(w);
|
|
133
|
+
const deploy = Math.min(deployScore(w), 1.5) / 1.5;
|
|
134
|
+
const score = Math.min(
|
|
135
|
+
WEIGHTS.tfidf * tfidf + WEIGHTS.nodeFingerprint * nodeFingerprint + WEIGHTS.outcome * outcome + WEIGHTS.deploy * deploy,
|
|
136
|
+
1
|
|
137
|
+
);
|
|
138
|
+
return {
|
|
139
|
+
workflow: w,
|
|
140
|
+
score,
|
|
141
|
+
signals: { tfidf, nodeFingerprint, outcome, deploy }
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/library/cluster.ts
|
|
147
|
+
function getFingerprint(w) {
|
|
148
|
+
return w.workflow.nodes.map((n) => n.type.split(".").pop() ?? "").sort();
|
|
149
|
+
}
|
|
150
|
+
function fingerprintKey(fp) {
|
|
151
|
+
return fp.join("|");
|
|
152
|
+
}
|
|
153
|
+
function describePattern(fp) {
|
|
154
|
+
const triggers = fp.filter((n) => /trigger/i.test(n));
|
|
155
|
+
const outputs = fp.filter((n) => /slack|gmail|email|telegram|sheets|airtable|notion/i.test(n));
|
|
156
|
+
const ai = fp.filter((n) => /agent|openai|anthropic|chain|memory/i.test(n));
|
|
157
|
+
const core = fp.filter((n) => /httpRequest|code|merge|switch|if|set|filter/i.test(n));
|
|
158
|
+
const parts = [];
|
|
159
|
+
if (triggers.length > 0) parts.push(triggers[0]);
|
|
160
|
+
if (ai.length > 0) parts.push("AI");
|
|
161
|
+
if (core.length > 0) parts.push(core.slice(0, 2).join("+"));
|
|
162
|
+
if (outputs.length > 0) parts.push(outputs[0]);
|
|
163
|
+
return parts.length > 0 ? parts.join(" \u2192 ") : fp.slice(0, 3).join(" \u2192 ");
|
|
164
|
+
}
|
|
165
|
+
function clusterWorkflows(workflows) {
|
|
166
|
+
const groups = /* @__PURE__ */ new Map();
|
|
167
|
+
for (const w of workflows) {
|
|
168
|
+
const fp = getFingerprint(w);
|
|
169
|
+
const key = fingerprintKey(fp);
|
|
170
|
+
const existing = groups.get(key);
|
|
171
|
+
if (existing) {
|
|
172
|
+
existing.push(w);
|
|
173
|
+
} else {
|
|
174
|
+
groups.set(key, [w]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const clusters = [];
|
|
178
|
+
for (const [, members] of groups) {
|
|
179
|
+
if (members.length === 0) continue;
|
|
180
|
+
const fp = getFingerprint(members[0]);
|
|
181
|
+
const withStats = members.filter((m) => m.outcomeStats && m.outcomeStats.totalUses > 0);
|
|
182
|
+
let avgFirstTryPassRate = 0;
|
|
183
|
+
let avgAttempts = 0;
|
|
184
|
+
if (withStats.length > 0) {
|
|
185
|
+
avgFirstTryPassRate = withStats.reduce((sum, m) => {
|
|
186
|
+
const s = m.outcomeStats;
|
|
187
|
+
return sum + s.firstTryPasses / s.totalUses;
|
|
188
|
+
}, 0) / withStats.length;
|
|
189
|
+
avgAttempts = withStats.reduce((sum, m) => {
|
|
190
|
+
const s = m.outcomeStats;
|
|
191
|
+
return sum + s.totalAttempts / s.totalUses;
|
|
192
|
+
}, 0) / withStats.length;
|
|
193
|
+
}
|
|
194
|
+
const ruleCounts = /* @__PURE__ */ new Map();
|
|
195
|
+
let totalFailureInstances = 0;
|
|
196
|
+
for (const m of withStats) {
|
|
197
|
+
const rules = m.outcomeStats.failedRules;
|
|
198
|
+
for (const [rule, count] of Object.entries(rules)) {
|
|
199
|
+
const r = parseInt(rule, 10);
|
|
200
|
+
ruleCounts.set(r, (ruleCounts.get(r) ?? 0) + count);
|
|
201
|
+
totalFailureInstances += count;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const commonFailedRules = [...ruleCounts.entries()].map(([rule, count]) => ({
|
|
205
|
+
rule,
|
|
206
|
+
frequency: totalFailureInstances > 0 ? count / totalFailureInstances : 0
|
|
207
|
+
})).filter((r) => r.frequency >= 0.1).sort((a, b) => b.frequency - a.frequency);
|
|
208
|
+
clusters.push({
|
|
209
|
+
pattern: describePattern(fp),
|
|
210
|
+
fingerprint: fp,
|
|
211
|
+
members,
|
|
212
|
+
avgFirstTryPassRate,
|
|
213
|
+
avgAttempts,
|
|
214
|
+
commonFailedRules
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return clusters.sort((a, b) => b.members.length - a.members.length);
|
|
218
|
+
}
|
|
219
|
+
function rerank(candidates, clusters) {
|
|
220
|
+
const clusterMap = /* @__PURE__ */ new Map();
|
|
221
|
+
for (const cluster of clusters) {
|
|
222
|
+
for (const member of cluster.members) {
|
|
223
|
+
clusterMap.set(member.id, cluster);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return candidates.map((c) => {
|
|
227
|
+
const cluster = clusterMap.get(c.workflow.id);
|
|
228
|
+
let boost = 0;
|
|
229
|
+
if (cluster && cluster.avgFirstTryPassRate > 0) {
|
|
230
|
+
boost = (cluster.avgFirstTryPassRate - 0.5) * 0.1;
|
|
231
|
+
}
|
|
232
|
+
if (cluster && cluster.commonFailedRules.length > 0) {
|
|
233
|
+
boost -= cluster.commonFailedRules.length * 0.02;
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
workflow: c.workflow,
|
|
237
|
+
score: Math.max(0, Math.min(1, c.score + boost)),
|
|
238
|
+
...cluster ? { clusterPattern: cluster.pattern } : {}
|
|
239
|
+
};
|
|
240
|
+
}).sort((a, b) => b.score - a.score);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/library/file-library.ts
|
|
244
|
+
function tokenize(text) {
|
|
245
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2);
|
|
246
|
+
}
|
|
247
|
+
function buildSearchCorpus(w) {
|
|
248
|
+
const nodeTokens = w.workflow.nodes.map((n) => {
|
|
249
|
+
const bare = n.type.split(".").pop() ?? "";
|
|
250
|
+
const spaced = bare.replace(/([A-Z])/g, " $1").trim().toLowerCase();
|
|
251
|
+
return `${bare} ${spaced}`;
|
|
252
|
+
});
|
|
253
|
+
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
254
|
+
}
|
|
255
|
+
var MAX_LIBRARY_SIZE = 500;
|
|
256
|
+
var FileLibrary = class {
|
|
257
|
+
dir;
|
|
258
|
+
workflows = [];
|
|
259
|
+
initPromise = null;
|
|
260
|
+
writeQueue = Promise.resolve();
|
|
261
|
+
constructor(dir) {
|
|
262
|
+
this.dir = dir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "library");
|
|
263
|
+
}
|
|
264
|
+
async initialize() {
|
|
265
|
+
if (!this.initPromise) {
|
|
266
|
+
this.initPromise = this.doInitialize();
|
|
267
|
+
}
|
|
268
|
+
return this.initPromise;
|
|
269
|
+
}
|
|
270
|
+
async doInitialize() {
|
|
271
|
+
await (0, import_promises.mkdir)(this.dir, { recursive: true });
|
|
272
|
+
const indexPath = (0, import_node_path.join)(this.dir, "index.json");
|
|
273
|
+
try {
|
|
274
|
+
const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
|
|
275
|
+
const parsed = JSON.parse(raw);
|
|
276
|
+
if (!Array.isArray(parsed)) {
|
|
277
|
+
this.workflows = [];
|
|
278
|
+
} else {
|
|
279
|
+
this.workflows = parsed.filter(
|
|
280
|
+
(item) => typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(item.workflow.nodes)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
this.workflows = [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async search(description, options) {
|
|
288
|
+
const searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
|
|
289
|
+
if (searchable.length === 0) return [];
|
|
290
|
+
const limit = options?.limit ?? 3;
|
|
291
|
+
const queryTokens = tokenize(description);
|
|
292
|
+
if (queryTokens.length === 0) return [];
|
|
293
|
+
const docTokenArrays = searchable.map((w) => tokenize(buildSearchCorpus(w)));
|
|
294
|
+
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
295
|
+
const docCount = searchable.length;
|
|
296
|
+
const idf = /* @__PURE__ */ new Map();
|
|
297
|
+
const allTokens = new Set(queryTokens);
|
|
298
|
+
for (const token of allTokens) {
|
|
299
|
+
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
300
|
+
idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
|
|
301
|
+
}
|
|
302
|
+
const scored = hybridScore(queryTokens, description, searchable, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
303
|
+
const clusters = clusterWorkflows(searchable);
|
|
304
|
+
const reranked = rerank(scored, clusters).slice(0, limit);
|
|
305
|
+
const results = reranked.map((m) => {
|
|
306
|
+
return { workflow: m.workflow, score: m.score, mode: scoreToMode(m.score) };
|
|
307
|
+
});
|
|
308
|
+
if (results.length > 0) {
|
|
309
|
+
for (const r of results) {
|
|
310
|
+
r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
|
|
311
|
+
}
|
|
312
|
+
this.persist();
|
|
313
|
+
}
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
async save(workflow, metadata) {
|
|
317
|
+
const id = generateUUID();
|
|
318
|
+
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
319
|
+
const stored = {
|
|
320
|
+
id,
|
|
321
|
+
workflow,
|
|
322
|
+
description: metadata.description,
|
|
323
|
+
tags: metadata.tags ?? [],
|
|
324
|
+
platform: metadata.platform ?? "n8n",
|
|
325
|
+
deployCount: 0,
|
|
326
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
327
|
+
...failurePatterns?.length ? { failurePatterns } : {},
|
|
328
|
+
...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
|
|
329
|
+
...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
|
|
330
|
+
...metadata.topMatchScore != null ? { topMatchScore: metadata.topMatchScore } : {},
|
|
331
|
+
...metadata.generationAttempts != null ? { generationAttempts: metadata.generationAttempts } : {},
|
|
332
|
+
...metadata.credentialsNeeded?.length ? { credentialsNeeded: metadata.credentialsNeeded } : {},
|
|
333
|
+
...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
|
|
334
|
+
...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
|
|
335
|
+
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
336
|
+
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
337
|
+
};
|
|
338
|
+
this.workflows.push(stored);
|
|
339
|
+
if (this.workflows.length > MAX_LIBRARY_SIZE) {
|
|
340
|
+
this.workflows.sort((a, b) => (b.deployCount ?? 1) - (a.deployCount ?? 1));
|
|
341
|
+
this.workflows = this.workflows.slice(0, MAX_LIBRARY_SIZE);
|
|
342
|
+
}
|
|
343
|
+
await this.persist();
|
|
344
|
+
return id;
|
|
345
|
+
}
|
|
346
|
+
async recordDeployment(id) {
|
|
347
|
+
const w = this.workflows.find((w2) => w2.id === id);
|
|
348
|
+
if (w) {
|
|
349
|
+
w.deployCount++;
|
|
350
|
+
w.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
351
|
+
await this.persist();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async recordOutcome(id, outcome) {
|
|
355
|
+
const w = this.workflows.find((w2) => w2.id === id);
|
|
356
|
+
if (!w) return;
|
|
357
|
+
if (outcome.mode === "direct") {
|
|
358
|
+
w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
|
|
359
|
+
} else {
|
|
360
|
+
w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
|
|
361
|
+
}
|
|
362
|
+
const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
|
|
363
|
+
stats.totalUses++;
|
|
364
|
+
stats.totalAttempts += outcome.attempts;
|
|
365
|
+
if (outcome.firstTryPass) stats.firstTryPasses++;
|
|
366
|
+
for (const rule of outcome.failedRules) {
|
|
367
|
+
const key = String(rule);
|
|
368
|
+
stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
|
|
369
|
+
}
|
|
370
|
+
w.outcomeStats = stats;
|
|
371
|
+
await this.persist();
|
|
372
|
+
}
|
|
373
|
+
async drain() {
|
|
374
|
+
await this.writeQueue;
|
|
375
|
+
}
|
|
376
|
+
async get(id) {
|
|
377
|
+
return this.workflows.find((w) => w.id === id) ?? null;
|
|
378
|
+
}
|
|
379
|
+
async list(filters) {
|
|
380
|
+
let result = this.workflows;
|
|
381
|
+
if (filters?.platform) {
|
|
382
|
+
result = result.filter((w) => w.platform === filters.platform);
|
|
383
|
+
}
|
|
384
|
+
if (filters?.tags && filters.tags.length > 0) {
|
|
385
|
+
result = result.filter((w) => filters.tags.some((t) => w.tags.includes(t)));
|
|
386
|
+
}
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
deduplicateFailurePatterns(patterns) {
|
|
390
|
+
if (!patterns?.length) return void 0;
|
|
391
|
+
const map = /* @__PURE__ */ new Map();
|
|
392
|
+
for (const fp of patterns) {
|
|
393
|
+
const existing = map.get(fp.rule);
|
|
394
|
+
if (existing) {
|
|
395
|
+
existing.occurrences++;
|
|
396
|
+
} else {
|
|
397
|
+
map.set(fp.rule, { rule: fp.rule, message: fp.message, occurrences: 1 });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return [...map.values()];
|
|
401
|
+
}
|
|
402
|
+
persist() {
|
|
403
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
404
|
+
const indexPath = (0, import_node_path.join)(this.dir, "index.json");
|
|
405
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
406
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
|
|
407
|
+
await (0, import_promises.rename)(tmpPath, indexPath);
|
|
408
|
+
});
|
|
409
|
+
return this.writeQueue;
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// src/validation/registry.ts
|
|
414
|
+
var DEFAULT_REGISTRY = [
|
|
415
|
+
// Trigger nodes
|
|
416
|
+
{ type: "n8n-nodes-base.manualTrigger", safeTypeVersions: [1], requiredParams: [], isTrigger: true },
|
|
417
|
+
{ type: "n8n-nodes-base.scheduleTrigger", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], isTrigger: true },
|
|
418
|
+
{ type: "n8n-nodes-base.webhook", safeTypeVersions: [1, 1.1, 2], requiredParams: ["httpMethod", "path"], isTrigger: true },
|
|
419
|
+
{ type: "n8n-nodes-base.formTrigger", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [], isTrigger: true },
|
|
420
|
+
{ type: "n8n-nodes-base.emailReadImap", safeTypeVersions: [2], requiredParams: [], credentialType: "imap", isTrigger: true },
|
|
421
|
+
{ type: "n8n-nodes-base.errorTrigger", safeTypeVersions: [1], requiredParams: [], isTrigger: true },
|
|
422
|
+
{ type: "n8n-nodes-base.executeWorkflowTrigger", safeTypeVersions: [1, 1.1], requiredParams: [], isTrigger: true },
|
|
423
|
+
{ type: "n8n-nodes-base.gmailTrigger", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], credentialType: "gmailOAuth2", isTrigger: true },
|
|
424
|
+
{ type: "n8n-nodes-base.googleDriveTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "googleDriveOAuth2Api", isTrigger: true },
|
|
425
|
+
{ type: "n8n-nodes-base.googleSheetsTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "googleSheetsTriggerOAuth2Api", isTrigger: true },
|
|
426
|
+
{ type: "n8n-nodes-base.slackTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "slackApi", isTrigger: true },
|
|
427
|
+
{ type: "n8n-nodes-base.telegramTrigger", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], credentialType: "telegramApi", isTrigger: true },
|
|
428
|
+
{ type: "n8n-nodes-base.githubTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "githubApi", isTrigger: true },
|
|
429
|
+
{ type: "n8n-nodes-base.stripeTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "stripeApi", isTrigger: true },
|
|
430
|
+
{ type: "n8n-nodes-base.airtableTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "airtableTokenApi", isTrigger: true },
|
|
431
|
+
{ type: "n8n-nodes-base.notionTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "notionApi", isTrigger: true },
|
|
432
|
+
{ type: "@n8n/n8n-nodes-langchain.chatTrigger", safeTypeVersions: [1, 1.1], requiredParams: [], isTrigger: true },
|
|
433
|
+
// Core logic nodes
|
|
434
|
+
{ type: "n8n-nodes-base.code", safeTypeVersions: [1, 2], requiredParams: [] },
|
|
435
|
+
{ type: "n8n-nodes-base.httpRequest", safeTypeVersions: [1, 2, 3, 4, 4.1, 4.2], requiredParams: ["url"] },
|
|
436
|
+
{ type: "n8n-nodes-base.set", safeTypeVersions: [1, 2, 3, 3.1, 3.2, 3.3, 3.4], requiredParams: [] },
|
|
437
|
+
{ type: "n8n-nodes-base.if", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [] },
|
|
438
|
+
{ type: "n8n-nodes-base.switch", safeTypeVersions: [1, 2, 3, 3.1, 3.2], requiredParams: [] },
|
|
439
|
+
{ type: "n8n-nodes-base.filter", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [] },
|
|
440
|
+
{ type: "n8n-nodes-base.merge", safeTypeVersions: [1, 2, 2.1, 3], requiredParams: [] },
|
|
441
|
+
{ type: "n8n-nodes-base.splitInBatches", safeTypeVersions: [1, 2, 3], requiredParams: [] },
|
|
442
|
+
{ type: "n8n-nodes-base.wait", safeTypeVersions: [1, 1.1], requiredParams: [] },
|
|
443
|
+
{ type: "n8n-nodes-base.executeWorkflow", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [] },
|
|
444
|
+
{ type: "n8n-nodes-base.respondToWebhook", safeTypeVersions: [1, 1.1], requiredParams: [] },
|
|
445
|
+
{ type: "n8n-nodes-base.noOp", safeTypeVersions: [1], requiredParams: [] },
|
|
446
|
+
{ type: "n8n-nodes-base.stopAndError", safeTypeVersions: [1], requiredParams: [] },
|
|
447
|
+
{ type: "n8n-nodes-base.splitOut", safeTypeVersions: [1], requiredParams: [] },
|
|
448
|
+
{ type: "n8n-nodes-base.aggregate", safeTypeVersions: [1], requiredParams: [] },
|
|
449
|
+
{ type: "n8n-nodes-base.stickyNote", safeTypeVersions: [1], requiredParams: [] },
|
|
450
|
+
// Email / messaging
|
|
451
|
+
{ type: "n8n-nodes-base.emailSend", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "smtp" },
|
|
452
|
+
{ type: "n8n-nodes-base.slack", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [], credentialType: "slackOAuth2Api" },
|
|
453
|
+
{ type: "n8n-nodes-base.telegram", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], credentialType: "telegramApi" },
|
|
454
|
+
{ type: "n8n-nodes-base.discord", safeTypeVersions: [1, 2], requiredParams: [], credentialType: "discordWebhookApi" },
|
|
455
|
+
// Google
|
|
456
|
+
{ type: "n8n-nodes-base.gmail", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "gmailOAuth2" },
|
|
457
|
+
{ type: "n8n-nodes-base.googleSheets", safeTypeVersions: [1, 2, 3, 4, 4.1, 4.2, 4.3, 4.4, 4.5], requiredParams: [], credentialType: "googleSheetsOAuth2Api" },
|
|
458
|
+
{ type: "n8n-nodes-base.googleDrive", safeTypeVersions: [1, 2, 3], requiredParams: [], credentialType: "googleDriveOAuth2Api" },
|
|
459
|
+
{ type: "n8n-nodes-base.googleCalendar", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [], credentialType: "googleCalendarOAuth2Api" },
|
|
460
|
+
// Project management / CRM
|
|
461
|
+
{ type: "n8n-nodes-base.notion", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [], credentialType: "notionApi" },
|
|
462
|
+
{ type: "n8n-nodes-base.airtable", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "airtableTokenApi" },
|
|
463
|
+
{ type: "n8n-nodes-base.github", safeTypeVersions: [1, 1.1], requiredParams: [], credentialType: "githubApi" },
|
|
464
|
+
{ type: "n8n-nodes-base.jira", safeTypeVersions: [1], requiredParams: [], credentialType: "jiraSoftwareCloudApi" },
|
|
465
|
+
{ type: "n8n-nodes-base.hubspot", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "hubspotOAuth2Api" },
|
|
466
|
+
// Databases
|
|
467
|
+
{ type: "n8n-nodes-base.postgres", safeTypeVersions: [1, 2, 2.1, 2.2, 2.3, 2.4, 2.5], requiredParams: [], credentialType: "postgres" },
|
|
468
|
+
{ type: "n8n-nodes-base.mySql", safeTypeVersions: [1, 2, 2.1, 2.2, 2.3, 2.4], requiredParams: [], credentialType: "mySql" },
|
|
469
|
+
{ type: "n8n-nodes-base.redis", safeTypeVersions: [1], requiredParams: [], credentialType: "redis" },
|
|
470
|
+
{ type: "n8n-nodes-base.supabase", safeTypeVersions: [1], requiredParams: [], credentialType: "supabaseApi" },
|
|
471
|
+
// Cloud
|
|
472
|
+
{ type: "n8n-nodes-base.awsS3", safeTypeVersions: [1, 2], requiredParams: [], credentialType: "aws" },
|
|
473
|
+
// Payment / commerce
|
|
474
|
+
{ type: "n8n-nodes-base.stripe", safeTypeVersions: [1], requiredParams: [], credentialType: "stripeApi" },
|
|
475
|
+
// AI / LangChain root nodes
|
|
476
|
+
{ 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: [] },
|
|
477
|
+
{ type: "@n8n/n8n-nodes-langchain.chainLlm", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5], requiredParams: [] },
|
|
478
|
+
{ type: "@n8n/n8n-nodes-langchain.chainRetrievalQa", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4], requiredParams: [] },
|
|
479
|
+
{ type: "@n8n/n8n-nodes-langchain.openAi", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8], requiredParams: [], credentialType: "openAiApi" },
|
|
480
|
+
{ type: "@n8n/n8n-nodes-langchain.anthropic", safeTypeVersions: [1], requiredParams: [], credentialType: "anthropicApi" },
|
|
481
|
+
{ type: "@n8n/n8n-nodes-langchain.informationExtractor", safeTypeVersions: [1], requiredParams: [] },
|
|
482
|
+
{ type: "@n8n/n8n-nodes-langchain.textClassifier", safeTypeVersions: [1], requiredParams: [] },
|
|
483
|
+
// AI / LangChain sub-nodes (models)
|
|
484
|
+
{ 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" },
|
|
485
|
+
{ type: "@n8n/n8n-nodes-langchain.lmChatAnthropic", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [], credentialType: "anthropicApi" },
|
|
486
|
+
{ type: "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", safeTypeVersions: [1], requiredParams: [], credentialType: "googlePalmApi" },
|
|
487
|
+
// AI / LangChain sub-nodes (memory, tools, etc.)
|
|
488
|
+
{ type: "@n8n/n8n-nodes-langchain.memoryBufferWindow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
|
|
489
|
+
{ type: "@n8n/n8n-nodes-langchain.toolWorkflow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
|
|
490
|
+
{ type: "@n8n/n8n-nodes-langchain.toolCode", safeTypeVersions: [1, 1.1], requiredParams: [] },
|
|
491
|
+
{ type: "@n8n/n8n-nodes-langchain.toolHttpRequest", safeTypeVersions: [1, 1.1], requiredParams: [] },
|
|
492
|
+
{ type: "@n8n/n8n-nodes-langchain.toolCalculator", safeTypeVersions: [1], requiredParams: [] }
|
|
493
|
+
];
|
|
494
|
+
var NodeRegistry = class {
|
|
495
|
+
byType;
|
|
496
|
+
constructor(definitions = DEFAULT_REGISTRY) {
|
|
497
|
+
this.byType = new Map(definitions.map((d) => [d.type, d]));
|
|
498
|
+
}
|
|
499
|
+
get(type) {
|
|
500
|
+
return this.byType.get(type);
|
|
501
|
+
}
|
|
502
|
+
isTrigger(type) {
|
|
503
|
+
return this.byType.get(type)?.isTrigger === true;
|
|
504
|
+
}
|
|
505
|
+
isKnown(type) {
|
|
506
|
+
return this.byType.has(type);
|
|
507
|
+
}
|
|
508
|
+
isVersionSafe(type, version) {
|
|
509
|
+
const def = this.byType.get(type);
|
|
510
|
+
if (!def) return true;
|
|
511
|
+
return def.safeTypeVersions.includes(version);
|
|
512
|
+
}
|
|
513
|
+
getRequiredParams(type) {
|
|
514
|
+
return this.byType.get(type)?.requiredParams ?? [];
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// src/providers/n8n/types.ts
|
|
519
|
+
var FORBIDDEN_ON_CREATE = [
|
|
520
|
+
"id",
|
|
521
|
+
"createdAt",
|
|
522
|
+
"updatedAt",
|
|
523
|
+
"versionId",
|
|
524
|
+
"meta",
|
|
525
|
+
"isArchived",
|
|
526
|
+
"activeVersionId",
|
|
527
|
+
"activeVersion",
|
|
528
|
+
"active",
|
|
529
|
+
"pinData",
|
|
530
|
+
"triggerCount",
|
|
531
|
+
"shared",
|
|
532
|
+
"staticData"
|
|
533
|
+
];
|
|
534
|
+
var FORBIDDEN_ON_UPDATE = FORBIDDEN_ON_CREATE.filter((f) => f !== "id");
|
|
535
|
+
|
|
536
|
+
// src/validation/validator.ts
|
|
537
|
+
var AI_CONNECTION_TYPES = [
|
|
538
|
+
"ai_languageModel",
|
|
539
|
+
"ai_memory",
|
|
540
|
+
"ai_tool",
|
|
541
|
+
"ai_outputParser",
|
|
542
|
+
"ai_embedding",
|
|
543
|
+
"ai_document",
|
|
544
|
+
"ai_textSplitter",
|
|
545
|
+
"ai_retriever",
|
|
546
|
+
"ai_vectorStore"
|
|
547
|
+
];
|
|
548
|
+
var TRIGGER_TYPE_PATTERNS = [/trigger/i, /Trigger$/];
|
|
549
|
+
var NODE_TYPE_PATTERN = /^(@[a-z0-9-]+\/[a-z0-9-]+\.|n8n-nodes-[a-z0-9-]+\.)[a-zA-Z][a-zA-Z0-9-]+$/;
|
|
550
|
+
var N8nValidator = class {
|
|
551
|
+
registry;
|
|
552
|
+
constructor(registry = new NodeRegistry(DEFAULT_REGISTRY)) {
|
|
553
|
+
this.registry = registry;
|
|
554
|
+
}
|
|
555
|
+
validate(workflow) {
|
|
556
|
+
const issues = [];
|
|
557
|
+
this.checkRule1(workflow, issues);
|
|
558
|
+
this.checkRule2(workflow, issues);
|
|
559
|
+
this.checkRule3(workflow, issues);
|
|
560
|
+
this.checkRule4(workflow, issues);
|
|
561
|
+
this.checkRule5(workflow, issues);
|
|
562
|
+
this.checkRule6(workflow, issues);
|
|
563
|
+
this.checkRule7(workflow, issues);
|
|
564
|
+
this.checkRule8(workflow, issues);
|
|
565
|
+
this.checkRule9(workflow, issues);
|
|
566
|
+
this.checkRule10(workflow, issues);
|
|
567
|
+
this.checkRule11(workflow, issues);
|
|
568
|
+
this.checkRule12(workflow, issues);
|
|
569
|
+
this.checkRule13(workflow, issues);
|
|
570
|
+
this.checkRule14(workflow, issues);
|
|
571
|
+
this.checkRule15(workflow, issues);
|
|
572
|
+
this.checkRule16(workflow, issues);
|
|
573
|
+
this.checkRule17(workflow, issues);
|
|
574
|
+
this.checkRule18(workflow, issues);
|
|
575
|
+
this.checkRule19(workflow, issues);
|
|
576
|
+
this.checkRule20(workflow, issues);
|
|
577
|
+
this.checkRule21(workflow, issues);
|
|
578
|
+
this.checkRule22(workflow, issues);
|
|
579
|
+
this.checkRule23(workflow, issues);
|
|
580
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
581
|
+
return { valid: errors.length === 0, issues };
|
|
582
|
+
}
|
|
583
|
+
err(issues, rule, message, nodeId) {
|
|
584
|
+
const issue = { rule, severity: "error", message };
|
|
585
|
+
if (nodeId !== void 0) issue.nodeId = nodeId;
|
|
586
|
+
issues.push(issue);
|
|
587
|
+
}
|
|
588
|
+
warn(issues, rule, message, nodeId) {
|
|
589
|
+
const issue = { rule, severity: "warn", message };
|
|
590
|
+
if (nodeId !== void 0) issue.nodeId = nodeId;
|
|
591
|
+
issues.push(issue);
|
|
592
|
+
}
|
|
593
|
+
isTriggerNode(node) {
|
|
594
|
+
if (this.registry.isTrigger(node.type)) return true;
|
|
595
|
+
return TRIGGER_TYPE_PATTERNS.some((p) => p.test(node.type));
|
|
596
|
+
}
|
|
597
|
+
// Rule 1: name is a non-empty string
|
|
598
|
+
checkRule1(w, issues) {
|
|
599
|
+
if (typeof w.name !== "string" || w.name.trim() === "") {
|
|
600
|
+
this.err(issues, 1, "Workflow name is required and must be a non-empty string");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// Rule 2: nodes is an array with at least one element
|
|
604
|
+
checkRule2(w, issues) {
|
|
605
|
+
if (!Array.isArray(w.nodes) || w.nodes.length === 0) {
|
|
606
|
+
this.err(issues, 2, "Workflow must have at least one node");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// Rule 3: every node has a non-empty id
|
|
610
|
+
checkRule3(w, issues) {
|
|
611
|
+
if (!Array.isArray(w.nodes)) return;
|
|
612
|
+
for (const node of w.nodes) {
|
|
613
|
+
if (typeof node.id !== "string" || node.id.trim() === "") {
|
|
614
|
+
this.err(issues, 3, `Node "${node.name ?? "unknown"}" is missing a valid id`, node.id);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Rule 4: node ids are unique
|
|
619
|
+
checkRule4(w, issues) {
|
|
620
|
+
if (!Array.isArray(w.nodes)) return;
|
|
621
|
+
const seen = /* @__PURE__ */ new Set();
|
|
622
|
+
for (const node of w.nodes) {
|
|
623
|
+
if (!node.id) continue;
|
|
624
|
+
if (seen.has(node.id)) {
|
|
625
|
+
this.err(issues, 4, `Duplicate node id: "${node.id}"`, node.id);
|
|
626
|
+
}
|
|
627
|
+
seen.add(node.id);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Rule 5: every node has a non-empty type string
|
|
631
|
+
checkRule5(w, issues) {
|
|
632
|
+
if (!Array.isArray(w.nodes)) return;
|
|
633
|
+
for (const node of w.nodes) {
|
|
634
|
+
if (typeof node.type !== "string" || node.type.trim() === "") {
|
|
635
|
+
this.err(issues, 5, `Node "${node.name ?? node.id}" is missing a type`, node.id);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Rule 6: every node has a positive typeVersion number
|
|
640
|
+
checkRule6(w, issues) {
|
|
641
|
+
if (!Array.isArray(w.nodes)) return;
|
|
642
|
+
for (const node of w.nodes) {
|
|
643
|
+
if (typeof node.typeVersion !== "number" || node.typeVersion <= 0) {
|
|
644
|
+
this.err(issues, 6, `Node "${node.name}" has invalid typeVersion: ${String(node.typeVersion)}`, node.id);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// Rule 7: every node has a valid [x, y] position
|
|
649
|
+
checkRule7(w, issues) {
|
|
650
|
+
if (!Array.isArray(w.nodes)) return;
|
|
651
|
+
for (const node of w.nodes) {
|
|
652
|
+
const pos = node.position;
|
|
653
|
+
if (!Array.isArray(pos) || pos.length !== 2 || typeof pos[0] !== "number" || typeof pos[1] !== "number") {
|
|
654
|
+
this.err(issues, 7, `Node "${node.name}" has invalid position (must be [x, y])`, node.id);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Rule 8: every node has a non-empty name
|
|
659
|
+
checkRule8(w, issues) {
|
|
660
|
+
if (!Array.isArray(w.nodes)) return;
|
|
661
|
+
for (const node of w.nodes) {
|
|
662
|
+
if (typeof node.name !== "string" || node.name.trim() === "") {
|
|
663
|
+
this.err(issues, 8, `Node with id "${node.id}" is missing a name`, node.id);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// Rule 9: connections is a plain object
|
|
668
|
+
checkRule9(w, issues) {
|
|
669
|
+
if (typeof w.connections !== "object" || w.connections === null || Array.isArray(w.connections)) {
|
|
670
|
+
this.err(issues, 9, "connections must be a plain object (use {} for single-node workflows)");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// Rule 10: every connection target node name exists in nodes
|
|
674
|
+
checkRule10(w, issues) {
|
|
675
|
+
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
676
|
+
const nodeNames = new Set(w.nodes.map((n) => n.name));
|
|
677
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
678
|
+
if (!nodeNames.has(sourceName)) {
|
|
679
|
+
this.err(issues, 10, `Connection source "${sourceName}" does not exist in nodes`);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (typeof outputs !== "object" || outputs === null) continue;
|
|
683
|
+
for (const portGroup of Object.values(outputs)) {
|
|
684
|
+
if (!Array.isArray(portGroup)) continue;
|
|
685
|
+
for (const targets of portGroup) {
|
|
686
|
+
if (!Array.isArray(targets)) continue;
|
|
687
|
+
for (const target of targets) {
|
|
688
|
+
const t = target;
|
|
689
|
+
if (typeof t?.node === "string" && !nodeNames.has(t.node)) {
|
|
690
|
+
this.err(issues, 10, `Connection target "${t.node}" does not exist in nodes`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// Rule 11 (WARN): every non-trigger node has at least one incoming connection
|
|
698
|
+
checkRule11(w, issues) {
|
|
699
|
+
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
700
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
701
|
+
for (const [, outputs] of Object.entries(w.connections)) {
|
|
702
|
+
if (typeof outputs !== "object" || outputs === null) continue;
|
|
703
|
+
for (const portGroup of Object.values(outputs)) {
|
|
704
|
+
if (!Array.isArray(portGroup)) continue;
|
|
705
|
+
for (const targets of portGroup) {
|
|
706
|
+
if (!Array.isArray(targets)) continue;
|
|
707
|
+
for (const target of targets) {
|
|
708
|
+
const t = target;
|
|
709
|
+
if (typeof t?.node === "string") reachable.add(t.node);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
for (const node of w.nodes) {
|
|
715
|
+
if (node.type.includes("stickyNote")) continue;
|
|
716
|
+
if (!this.isTriggerNode(node) && !reachable.has(node.name)) {
|
|
717
|
+
this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// Rule 12: forbidden fields absent from workflow root
|
|
722
|
+
checkRule12(w, issues) {
|
|
723
|
+
const wObj = w;
|
|
724
|
+
for (const field of FORBIDDEN_ON_CREATE) {
|
|
725
|
+
if (field in wObj) {
|
|
726
|
+
this.err(issues, 12, `Forbidden field "${field}" present in workflow \u2014 remove it before deploying`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
// Rule 13: settings, if present, is a plain object
|
|
731
|
+
checkRule13(w, issues) {
|
|
732
|
+
if (w.settings !== void 0) {
|
|
733
|
+
if (typeof w.settings !== "object" || w.settings === null || Array.isArray(w.settings)) {
|
|
734
|
+
this.err(issues, 13, "workflow.settings must be a plain object");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// Rule 14: at least one trigger node is present
|
|
739
|
+
checkRule14(w, issues) {
|
|
740
|
+
if (!Array.isArray(w.nodes)) return;
|
|
741
|
+
const hasTrigger = w.nodes.some((n) => this.isTriggerNode(n));
|
|
742
|
+
if (!hasTrigger) {
|
|
743
|
+
this.err(issues, 14, "Workflow must contain at least one trigger node");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// Rule 15: node type string matches expected format
|
|
747
|
+
checkRule15(w, issues) {
|
|
748
|
+
if (!Array.isArray(w.nodes)) return;
|
|
749
|
+
for (const node of w.nodes) {
|
|
750
|
+
if (typeof node.type !== "string") continue;
|
|
751
|
+
if (!NODE_TYPE_PATTERN.test(node.type)) {
|
|
752
|
+
this.err(issues, 15, `Node "${node.name}" has malformed type string: "${node.type}"`, node.id);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// Rule 16: node names are unique within the workflow
|
|
757
|
+
checkRule16(w, issues) {
|
|
758
|
+
if (!Array.isArray(w.nodes)) return;
|
|
759
|
+
const seen = /* @__PURE__ */ new Set();
|
|
760
|
+
for (const node of w.nodes) {
|
|
761
|
+
if (!node.name) continue;
|
|
762
|
+
if (seen.has(node.name)) {
|
|
763
|
+
this.err(issues, 16, `Duplicate node name: "${node.name}"`, node.id);
|
|
764
|
+
}
|
|
765
|
+
seen.add(node.name);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// Rule 17: credentials shape — each entry has id and name
|
|
769
|
+
checkRule17(w, issues) {
|
|
770
|
+
if (!Array.isArray(w.nodes)) return;
|
|
771
|
+
for (const node of w.nodes) {
|
|
772
|
+
if (!node.credentials) continue;
|
|
773
|
+
for (const [credType, credRef] of Object.entries(node.credentials)) {
|
|
774
|
+
if (typeof credRef !== "object" || credRef === null) {
|
|
775
|
+
this.err(issues, 17, `Node "${node.name}" credential "${credType}" must be an object with id and name`, node.id);
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
const ref = credRef;
|
|
779
|
+
if (typeof ref["id"] !== "string" || ref["id"].trim() === "" || typeof ref["name"] !== "string" || ref["name"].trim() === "") {
|
|
780
|
+
this.err(issues, 17, `Node "${node.name}" credential "${credType}" must have non-empty string id and name fields`, node.id);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// Rule 18 (ERROR): AI connections must originate from sub-nodes, not the agent/chain root
|
|
786
|
+
checkRule18(w, issues) {
|
|
787
|
+
if (typeof w.connections !== "object" || w.connections === null) return;
|
|
788
|
+
const agentTypes = /* @__PURE__ */ new Set([
|
|
789
|
+
"@n8n/n8n-nodes-langchain.agent",
|
|
790
|
+
"@n8n/n8n-nodes-langchain.chainLlm",
|
|
791
|
+
"@n8n/n8n-nodes-langchain.chainRetrievalQa",
|
|
792
|
+
"@n8n/n8n-nodes-langchain.chainSummarization"
|
|
793
|
+
]);
|
|
794
|
+
if (!Array.isArray(w.nodes)) return;
|
|
795
|
+
const nodesByName = new Map(w.nodes.map((n) => [n.name, n]));
|
|
796
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
797
|
+
const sourceNode = nodesByName.get(sourceName);
|
|
798
|
+
if (!sourceNode) continue;
|
|
799
|
+
if (!agentTypes.has(sourceNode.type)) continue;
|
|
800
|
+
if (typeof outputs !== "object" || outputs === null) continue;
|
|
801
|
+
for (const connType of AI_CONNECTION_TYPES) {
|
|
802
|
+
if (connType in outputs) {
|
|
803
|
+
this.err(
|
|
804
|
+
issues,
|
|
805
|
+
18,
|
|
806
|
+
`Node "${sourceName}" uses AI connection type "${connType}" as a SOURCE \u2014 AI sub-nodes should be the source, not the agent/chain root`,
|
|
807
|
+
sourceNode.id
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// Rule 19 (WARN): typeVersion is within known safe range for registered node types
|
|
814
|
+
checkRule19(w, issues) {
|
|
815
|
+
if (!Array.isArray(w.nodes)) return;
|
|
816
|
+
for (const node of w.nodes) {
|
|
817
|
+
if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
|
|
818
|
+
if (!this.registry.isVersionSafe(node.type, node.typeVersion)) {
|
|
819
|
+
this.warn(
|
|
820
|
+
issues,
|
|
821
|
+
19,
|
|
822
|
+
`Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
|
|
823
|
+
node.id
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Rule 20 (WARN): cycle detection — no node should be reachable from itself
|
|
829
|
+
// Exempts splitInBatches loops which are an intentional n8n pattern
|
|
830
|
+
checkRule20(w, issues) {
|
|
831
|
+
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
832
|
+
const splitBatchNodes = new Set(
|
|
833
|
+
w.nodes.filter((n) => n.type.includes("splitInBatches")).map((n) => n.name)
|
|
834
|
+
);
|
|
835
|
+
const adj = /* @__PURE__ */ new Map();
|
|
836
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
837
|
+
if (typeof outputs !== "object" || outputs === null) continue;
|
|
838
|
+
const targets = [];
|
|
839
|
+
for (const portGroup of Object.values(outputs)) {
|
|
840
|
+
if (!Array.isArray(portGroup)) continue;
|
|
841
|
+
for (const conns of portGroup) {
|
|
842
|
+
if (!Array.isArray(conns)) continue;
|
|
843
|
+
for (const conn of conns) {
|
|
844
|
+
const t = conn;
|
|
845
|
+
if (typeof t?.node === "string") {
|
|
846
|
+
if (splitBatchNodes.has(t.node)) continue;
|
|
847
|
+
targets.push(t.node);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
adj.set(sourceName, targets);
|
|
853
|
+
}
|
|
854
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
855
|
+
const color = /* @__PURE__ */ new Map();
|
|
856
|
+
for (const node of w.nodes) color.set(node.name, WHITE);
|
|
857
|
+
const dfs = (name) => {
|
|
858
|
+
color.set(name, GRAY);
|
|
859
|
+
for (const neighbor of adj.get(name) ?? []) {
|
|
860
|
+
const c = color.get(neighbor);
|
|
861
|
+
if (c === GRAY) return true;
|
|
862
|
+
if (c === WHITE && dfs(neighbor)) return true;
|
|
863
|
+
}
|
|
864
|
+
color.set(name, BLACK);
|
|
865
|
+
return false;
|
|
866
|
+
};
|
|
867
|
+
for (const node of w.nodes) {
|
|
868
|
+
if (color.get(node.name) === WHITE && dfs(node.name)) {
|
|
869
|
+
this.warn(issues, 20, "Workflow contains a connection cycle \u2014 this may cause infinite loops");
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// Rule 22 (WARN): check requiredParams from registry
|
|
875
|
+
checkRule22(w, issues) {
|
|
876
|
+
if (!Array.isArray(w.nodes)) return;
|
|
877
|
+
for (const node of w.nodes) {
|
|
878
|
+
if (typeof node.type !== "string") continue;
|
|
879
|
+
const required = this.registry.getRequiredParams(node.type);
|
|
880
|
+
if (required.length === 0) continue;
|
|
881
|
+
const params = node.parameters ?? {};
|
|
882
|
+
for (const param of required) {
|
|
883
|
+
const value = params[param];
|
|
884
|
+
if (value === void 0 || value === null || value === "") {
|
|
885
|
+
this.warn(
|
|
886
|
+
issues,
|
|
887
|
+
22,
|
|
888
|
+
`Node "${node.name}" (${node.type}) is missing required parameter "${param}"`,
|
|
889
|
+
node.id
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// Rule 23 (WARN): unknown node types not in registry
|
|
896
|
+
checkRule23(w, issues) {
|
|
897
|
+
if (!Array.isArray(w.nodes)) return;
|
|
898
|
+
for (const node of w.nodes) {
|
|
899
|
+
if (typeof node.type !== "string") continue;
|
|
900
|
+
if (node.type.includes("stickyNote")) continue;
|
|
901
|
+
if (!NODE_TYPE_PATTERN.test(node.type)) continue;
|
|
902
|
+
if (!this.registry.isKnown(node.type)) {
|
|
903
|
+
this.warn(
|
|
904
|
+
issues,
|
|
905
|
+
23,
|
|
906
|
+
`Node "${node.name}" uses unknown type "${node.type}" \u2014 it may not exist in n8n`,
|
|
907
|
+
node.id
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
|
|
913
|
+
checkRule21(w, issues) {
|
|
914
|
+
if (!Array.isArray(w.nodes)) return;
|
|
915
|
+
const webhooksNeedingResponse = w.nodes.filter((n) => {
|
|
916
|
+
if (!n.type.includes("webhook")) return false;
|
|
917
|
+
const params = n.parameters;
|
|
918
|
+
return params?.responseMode === "responseNode";
|
|
919
|
+
});
|
|
920
|
+
if (webhooksNeedingResponse.length === 0) return;
|
|
921
|
+
const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
|
|
922
|
+
if (!hasRespondNode) {
|
|
923
|
+
for (const wh of webhooksNeedingResponse) {
|
|
924
|
+
this.warn(
|
|
925
|
+
issues,
|
|
926
|
+
21,
|
|
927
|
+
`Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
|
|
928
|
+
wh.id
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
// src/providers/n8n/stripper.ts
|
|
936
|
+
var N8nFieldStripper = class {
|
|
937
|
+
stripForCreate(workflow) {
|
|
938
|
+
return this.strip(workflow, FORBIDDEN_ON_CREATE);
|
|
939
|
+
}
|
|
940
|
+
stripForUpdate(workflow) {
|
|
941
|
+
return this.strip(workflow, FORBIDDEN_ON_UPDATE);
|
|
942
|
+
}
|
|
943
|
+
strip(workflow, forbidden) {
|
|
944
|
+
const result = { ...workflow };
|
|
945
|
+
for (const field of forbidden) {
|
|
946
|
+
delete result[field];
|
|
947
|
+
}
|
|
948
|
+
return result;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// src/errors/base.ts
|
|
953
|
+
var KairosError = class extends Error {
|
|
954
|
+
constructor(message, cause) {
|
|
955
|
+
super(message);
|
|
956
|
+
this.cause = cause;
|
|
957
|
+
this.name = "KairosError";
|
|
958
|
+
if (Error.captureStackTrace) {
|
|
959
|
+
Error.captureStackTrace(this, this.constructor);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
cause;
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// src/errors/api-error.ts
|
|
966
|
+
var ApiError = class extends KairosError {
|
|
967
|
+
constructor(message, statusCode, cause) {
|
|
968
|
+
super(message, cause);
|
|
969
|
+
this.statusCode = statusCode;
|
|
970
|
+
this.name = "ApiError";
|
|
971
|
+
}
|
|
972
|
+
statusCode;
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
// src/errors/provider-error.ts
|
|
976
|
+
var ProviderError = class extends KairosError {
|
|
977
|
+
constructor(message, cause) {
|
|
978
|
+
super(message, cause);
|
|
979
|
+
this.name = "ProviderError";
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// src/utils/retry.ts
|
|
984
|
+
async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
|
|
985
|
+
let lastError;
|
|
986
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
987
|
+
if (attempt > 0) {
|
|
988
|
+
const jitter = Math.random() * delayMs * 0.5;
|
|
989
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs * 2 ** (attempt - 1) + jitter));
|
|
990
|
+
}
|
|
991
|
+
try {
|
|
992
|
+
return await fn();
|
|
993
|
+
} catch (err) {
|
|
994
|
+
lastError = err;
|
|
995
|
+
if (shouldRetry && !shouldRetry(err)) throw err;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
throw lastError;
|
|
999
|
+
}
|
|
1000
|
+
function fetchWithTimeout(url, init, timeoutMs) {
|
|
1001
|
+
const controller = new AbortController();
|
|
1002
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1003
|
+
return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/providers/n8n/api-client.ts
|
|
1007
|
+
var EXECUTION_LIMIT_CAP = 100;
|
|
1008
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
1009
|
+
var RETRY_ATTEMPTS = 3;
|
|
1010
|
+
var RETRY_DELAY_MS = 1e3;
|
|
1011
|
+
var N8nApiClient = class {
|
|
1012
|
+
constructor(baseUrl, apiKey, logger) {
|
|
1013
|
+
this.baseUrl = baseUrl;
|
|
1014
|
+
this.apiKey = apiKey;
|
|
1015
|
+
this.logger = logger;
|
|
1016
|
+
}
|
|
1017
|
+
baseUrl;
|
|
1018
|
+
apiKey;
|
|
1019
|
+
logger;
|
|
1020
|
+
async request(method, path, body) {
|
|
1021
|
+
const url = `${this.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
|
|
1022
|
+
this.logger.debug(`n8n ${method} ${path}`);
|
|
1023
|
+
const isSafe = method === "GET";
|
|
1024
|
+
if (!isSafe) {
|
|
1025
|
+
return this.singleRequest(url, method, path, body);
|
|
1026
|
+
}
|
|
1027
|
+
return withRetry(
|
|
1028
|
+
() => this.singleRequest(url, method, path, body),
|
|
1029
|
+
RETRY_ATTEMPTS,
|
|
1030
|
+
RETRY_DELAY_MS,
|
|
1031
|
+
(err) => err instanceof ProviderError || err instanceof ApiError && err.statusCode === 429
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
async singleRequest(url, method, path, body) {
|
|
1035
|
+
let response;
|
|
1036
|
+
try {
|
|
1037
|
+
response = await fetchWithTimeout(url, {
|
|
1038
|
+
method,
|
|
1039
|
+
headers: {
|
|
1040
|
+
"X-N8N-API-KEY": this.apiKey,
|
|
1041
|
+
"Content-Type": "application/json",
|
|
1042
|
+
Accept: "application/json"
|
|
1043
|
+
},
|
|
1044
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
1045
|
+
}, REQUEST_TIMEOUT_MS);
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
throw new ProviderError(`Network error calling n8n API: ${path}`, err);
|
|
1048
|
+
}
|
|
1049
|
+
if (!response.ok) {
|
|
1050
|
+
let errorBody;
|
|
1051
|
+
try {
|
|
1052
|
+
errorBody = await response.json();
|
|
1053
|
+
} catch {
|
|
1054
|
+
errorBody = await response.text().catch(() => "");
|
|
1055
|
+
}
|
|
1056
|
+
this.logger.error(`n8n API error ${response.status} on ${method} ${path}`, {
|
|
1057
|
+
status: response.status,
|
|
1058
|
+
body: String(errorBody)
|
|
1059
|
+
});
|
|
1060
|
+
throw new ApiError(
|
|
1061
|
+
`n8n API returned ${response.status} for ${method} ${path}: ${JSON.stringify(errorBody)}`,
|
|
1062
|
+
response.status,
|
|
1063
|
+
errorBody
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
if (response.status === 204) return void 0;
|
|
1067
|
+
return response.json();
|
|
1068
|
+
}
|
|
1069
|
+
async createWorkflow(workflow) {
|
|
1070
|
+
return this.request("POST", "/workflows", workflow);
|
|
1071
|
+
}
|
|
1072
|
+
async updateWorkflow(id, workflow) {
|
|
1073
|
+
return this.request("PUT", `/workflows/${id}`, workflow);
|
|
1074
|
+
}
|
|
1075
|
+
async getWorkflow(id) {
|
|
1076
|
+
return this.request("GET", `/workflows/${id}`);
|
|
1077
|
+
}
|
|
1078
|
+
async listWorkflows() {
|
|
1079
|
+
const all = [];
|
|
1080
|
+
let path = "/workflows?limit=250";
|
|
1081
|
+
for (; ; ) {
|
|
1082
|
+
const response = await this.request("GET", path);
|
|
1083
|
+
for (const w of response.data) {
|
|
1084
|
+
all.push({
|
|
1085
|
+
id: w.id,
|
|
1086
|
+
name: w.name,
|
|
1087
|
+
active: w.active,
|
|
1088
|
+
createdAt: w.createdAt,
|
|
1089
|
+
updatedAt: w.updatedAt,
|
|
1090
|
+
...w.tags !== void 0 ? { tags: w.tags } : {}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
if (!response.nextCursor) break;
|
|
1094
|
+
path = `/workflows?limit=250&cursor=${response.nextCursor}`;
|
|
1095
|
+
}
|
|
1096
|
+
return all;
|
|
1097
|
+
}
|
|
1098
|
+
async deleteWorkflow(id) {
|
|
1099
|
+
await this.request("DELETE", `/workflows/${id}`);
|
|
1100
|
+
}
|
|
1101
|
+
async activateWorkflow(id) {
|
|
1102
|
+
await this.request("POST", `/workflows/${id}/activate`);
|
|
1103
|
+
}
|
|
1104
|
+
async deactivateWorkflow(id) {
|
|
1105
|
+
await this.request("POST", `/workflows/${id}/deactivate`);
|
|
1106
|
+
}
|
|
1107
|
+
async getExecutions(workflowId, filter) {
|
|
1108
|
+
const params = new URLSearchParams();
|
|
1109
|
+
if (workflowId) params.set("workflowId", workflowId);
|
|
1110
|
+
if (filter?.status) params.set("status", filter.status);
|
|
1111
|
+
const limit = Math.min(filter?.limit ?? 20, EXECUTION_LIMIT_CAP);
|
|
1112
|
+
params.set("limit", String(limit));
|
|
1113
|
+
if (filter?.cursor) params.set("cursor", filter.cursor);
|
|
1114
|
+
const qs = params.toString();
|
|
1115
|
+
const response = await this.request("GET", `/executions${qs ? `?${qs}` : ""}`);
|
|
1116
|
+
return response.data.map(this.mapExecution);
|
|
1117
|
+
}
|
|
1118
|
+
async getExecution(id) {
|
|
1119
|
+
const response = await this.request("GET", `/executions/${id}`);
|
|
1120
|
+
return { ...this.mapExecution(response), data: response.data, workflowData: response.workflowData };
|
|
1121
|
+
}
|
|
1122
|
+
async listTags() {
|
|
1123
|
+
const all = [];
|
|
1124
|
+
let path = "/tags?limit=250";
|
|
1125
|
+
for (; ; ) {
|
|
1126
|
+
const response = await this.request("GET", path);
|
|
1127
|
+
for (const t of response.data) {
|
|
1128
|
+
all.push({ id: t.id, name: t.name });
|
|
1129
|
+
}
|
|
1130
|
+
if (!response.nextCursor) break;
|
|
1131
|
+
path = `/tags?limit=250&cursor=${response.nextCursor}`;
|
|
1132
|
+
}
|
|
1133
|
+
return all;
|
|
1134
|
+
}
|
|
1135
|
+
async createTag(name) {
|
|
1136
|
+
const response = await this.request("POST", "/tags", { name });
|
|
1137
|
+
return { id: response.id, name: response.name };
|
|
1138
|
+
}
|
|
1139
|
+
async tagWorkflow(workflowId, tagIds) {
|
|
1140
|
+
await this.request("PUT", `/workflows/${workflowId}/tags`, tagIds.map((id) => ({ id })));
|
|
1141
|
+
}
|
|
1142
|
+
async untagWorkflow(workflowId, tagIds) {
|
|
1143
|
+
const current = await this.getWorkflow(workflowId);
|
|
1144
|
+
const remaining = (current.tags ?? []).filter((t) => !tagIds.includes(t.id)).map((t) => ({ id: t.id }));
|
|
1145
|
+
await this.request("PUT", `/workflows/${workflowId}/tags`, remaining);
|
|
1146
|
+
}
|
|
1147
|
+
mapExecution(e) {
|
|
1148
|
+
return {
|
|
1149
|
+
id: e.id,
|
|
1150
|
+
workflowId: e.workflowId,
|
|
1151
|
+
status: e.status,
|
|
1152
|
+
startedAt: e.startedAt,
|
|
1153
|
+
...e.stoppedAt !== void 0 ? { stoppedAt: e.stoppedAt } : {},
|
|
1154
|
+
mode: e.mode
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
// src/generation/prompts/v1.ts
|
|
1160
|
+
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.
|
|
1161
|
+
|
|
1162
|
+
## HARD RULES \u2014 violating any of these causes immediate deployment failure
|
|
1163
|
+
|
|
1164
|
+
### Forbidden fields \u2014 NEVER include these in the workflow object:
|
|
1165
|
+
id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId, activeVersion, pinData, triggerCount, shared, staticData
|
|
1166
|
+
|
|
1167
|
+
### Required top-level structure:
|
|
1168
|
+
{
|
|
1169
|
+
"name": "<descriptive name>",
|
|
1170
|
+
"nodes": [...],
|
|
1171
|
+
"connections": {...},
|
|
1172
|
+
"settings": {
|
|
1173
|
+
"saveExecutionProgress": true,
|
|
1174
|
+
"saveManualExecutions": true,
|
|
1175
|
+
"saveDataErrorExecution": "all",
|
|
1176
|
+
"saveDataSuccessExecution": "all",
|
|
1177
|
+
"executionTimeout": 3600,
|
|
1178
|
+
"timezone": "UTC",
|
|
1179
|
+
"executionOrder": "v1"
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
### Node IDs:
|
|
1184
|
+
- Every node.id must be a valid UUID v4 (random hex, format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
|
|
1185
|
+
- Never reuse IDs, never use sequential fake IDs like "node-1"
|
|
1186
|
+
|
|
1187
|
+
### Credentials:
|
|
1188
|
+
- Only reference credentials with exact type names (see catalog below)
|
|
1189
|
+
- If credential ID is unknown, OMIT the credentials block entirely \u2014 never invent credential IDs
|
|
1190
|
+
- Never put API keys or tokens in parameters when a credential type exists
|
|
1191
|
+
|
|
1192
|
+
### Node names:
|
|
1193
|
+
- All node names must be unique within the workflow
|
|
1194
|
+
- Use descriptive names: "Fetch Open Invoices" not "HTTP Request 2"
|
|
1195
|
+
|
|
1196
|
+
### Positioning:
|
|
1197
|
+
- Trigger node: [250, 300]
|
|
1198
|
+
- Each subsequent step: x + 220 minimum
|
|
1199
|
+
- Parallel branches: offset y by \xB1150
|
|
1200
|
+
- AI sub-nodes: place below their root node (y + 200)
|
|
1201
|
+
|
|
1202
|
+
---
|
|
1203
|
+
|
|
1204
|
+
## CONNECTION RULES \u2014 the most common source of errors
|
|
1205
|
+
|
|
1206
|
+
### Standard connections (main data flow):
|
|
1207
|
+
"NodeA": { "main": [ [ { "node": "NodeB", "type": "main", "index": 0 } ] ] }
|
|
1208
|
+
|
|
1209
|
+
### AI connections \u2014 CRITICAL: the SUB-NODE is the SOURCE, NOT the agent/chain:
|
|
1210
|
+
"OpenAI Chat Model": { "ai_languageModel": [ [ { "node": "AI Agent", "type": "ai_languageModel", "index": 0 } ] ] }
|
|
1211
|
+
"Simple Memory": { "ai_memory": [ [ { "node": "AI Agent", "type": "ai_memory", "index": 0 } ] ] }
|
|
1212
|
+
"Calculator Tool": { "ai_tool": [ [ { "node": "AI Agent", "type": "ai_tool", "index": 0 } ] ] }
|
|
1213
|
+
|
|
1214
|
+
The AI Agent node does NOT appear in connections as a source for ai_* types.
|
|
1215
|
+
Every AI Agent must have at least one ai_languageModel sub-node connected.
|
|
1216
|
+
|
|
1217
|
+
### IF node \u2014 two output ports (0 = true, 1 = false):
|
|
1218
|
+
"IF Check": { "main": [ [{ "node": "True Path", "type": "main", "index": 0 }], [{ "node": "False Path", "type": "main", "index": 0 }] ] }
|
|
1219
|
+
|
|
1220
|
+
### SplitInBatches \u2014 two output ports (0 = done/finished, 1 = loop body per batch):
|
|
1221
|
+
Connect output 0 to the node that runs AFTER all batches complete.
|
|
1222
|
+
Connect output 1 to the processing chain for each batch. The last node in the chain loops back to SplitInBatches via main input.
|
|
1223
|
+
|
|
1224
|
+
### Webhook + RespondToWebhook pattern:
|
|
1225
|
+
When webhook responseMode is "responseNode", you MUST include a respondToWebhook node in the flow.
|
|
1226
|
+
"Webhook": { "main": [[{ "node": "Process Data", "type": "main", "index": 0 }]] }
|
|
1227
|
+
"Process Data": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] }
|
|
1228
|
+
|
|
1229
|
+
### Triggers have no incoming connections.
|
|
1230
|
+
### Connection keys are NODE NAMES, never node IDs.
|
|
1231
|
+
|
|
1232
|
+
### Nested parameters:
|
|
1233
|
+
Node parameters like conditions, assignments, and rule intervals MUST include all required nested fields. Do not leave nested objects empty or partially filled.
|
|
1234
|
+
|
|
1235
|
+
---
|
|
1236
|
+
|
|
1237
|
+
## NODE CATALOG \u2014 exact type strings and safe typeVersions
|
|
1238
|
+
|
|
1239
|
+
### Triggers (always at least one required):
|
|
1240
|
+
n8n-nodes-base.manualTrigger typeVersion: 1 \u2014 testing only
|
|
1241
|
+
n8n-nodes-base.scheduleTrigger typeVersion: 1.2 \u2014 params: rule.interval[{field, ...}]
|
|
1242
|
+
n8n-nodes-base.webhook typeVersion: 2 \u2014 params: httpMethod, path, responseMode
|
|
1243
|
+
n8n-nodes-base.formTrigger typeVersion: 2.2
|
|
1244
|
+
n8n-nodes-base.emailReadImap typeVersion: 2 \u2014 cred: imap
|
|
1245
|
+
n8n-nodes-base.errorTrigger typeVersion: 1
|
|
1246
|
+
n8n-nodes-base.executeWorkflowTrigger typeVersion: 1.1
|
|
1247
|
+
n8n-nodes-base.gmailTrigger typeVersion: 1.2 \u2014 cred: gmailOAuth2
|
|
1248
|
+
n8n-nodes-base.slackTrigger typeVersion: 1 \u2014 cred: slackApi
|
|
1249
|
+
n8n-nodes-base.telegramTrigger typeVersion: 1.2 \u2014 cred: telegramApi
|
|
1250
|
+
n8n-nodes-base.githubTrigger typeVersion: 1 \u2014 cred: githubApi
|
|
1251
|
+
n8n-nodes-base.airtableTrigger typeVersion: 1 \u2014 cred: airtableTokenApi
|
|
1252
|
+
n8n-nodes-base.notionTrigger typeVersion: 1 \u2014 cred: notionApi
|
|
1253
|
+
@n8n/n8n-nodes-langchain.chatTrigger typeVersion: 1.1 \u2014 pairs with AI Agent
|
|
1254
|
+
|
|
1255
|
+
### Core logic:
|
|
1256
|
+
n8n-nodes-base.code typeVersion: 2 \u2014 params: mode, jsCode
|
|
1257
|
+
n8n-nodes-base.httpRequest typeVersion: 4.2 \u2014 params: method, url, [sendBody, jsonBody, sendHeaders, headerParameters]
|
|
1258
|
+
n8n-nodes-base.set typeVersion: 3.4 \u2014 params: assignments.assignments[{id, name, value, type}]
|
|
1259
|
+
n8n-nodes-base.if typeVersion: 2.2 \u2014 params: conditions.conditions[{id, leftValue, rightValue, operator}], combinator
|
|
1260
|
+
n8n-nodes-base.switch typeVersion: 3.2 \u2014 multi-branch routing
|
|
1261
|
+
n8n-nodes-base.filter typeVersion: 2.2 \u2014 params: conditions (same as IF), 1 output
|
|
1262
|
+
n8n-nodes-base.merge typeVersion: 3 \u2014 modes: append/combine/chooseBranch
|
|
1263
|
+
n8n-nodes-base.splitInBatches typeVersion: 3 \u2014 output 0=done, output 1=loop body
|
|
1264
|
+
n8n-nodes-base.wait typeVersion: 1.1
|
|
1265
|
+
n8n-nodes-base.executeWorkflow typeVersion: 1.2
|
|
1266
|
+
n8n-nodes-base.respondToWebhook typeVersion: 1.1 \u2014 required when webhook responseMode is "responseNode"
|
|
1267
|
+
n8n-nodes-base.noOp typeVersion: 1
|
|
1268
|
+
n8n-nodes-base.splitOut typeVersion: 1
|
|
1269
|
+
n8n-nodes-base.aggregate typeVersion: 1
|
|
1270
|
+
n8n-nodes-base.stickyNote typeVersion: 1 \u2014 never connected, canvas annotation only
|
|
1271
|
+
|
|
1272
|
+
### Email / messaging:
|
|
1273
|
+
n8n-nodes-base.emailSend typeVersion: 2.1 \u2014 cred: smtp
|
|
1274
|
+
n8n-nodes-base.slack typeVersion: 2.2 \u2014 cred: slackOAuth2Api \u2014 params: resource, operation, select, channelId{__rl}, text
|
|
1275
|
+
n8n-nodes-base.telegram typeVersion: 1.2 \u2014 cred: telegramApi
|
|
1276
|
+
n8n-nodes-base.discord typeVersion: 2 \u2014 cred: discordWebhookApi
|
|
1277
|
+
|
|
1278
|
+
### Google:
|
|
1279
|
+
n8n-nodes-base.gmail typeVersion: 2.1 \u2014 cred: gmailOAuth2 \u2014 params: resource, operation
|
|
1280
|
+
n8n-nodes-base.googleSheets typeVersion: 4.5 \u2014 cred: googleSheetsOAuth2Api \u2014 params: resource, operation, documentId{__rl}, sheetName{__rl}
|
|
1281
|
+
n8n-nodes-base.googleDrive typeVersion: 3 \u2014 cred: googleDriveOAuth2Api
|
|
1282
|
+
n8n-nodes-base.googleCalendar typeVersion: 1.3 \u2014 cred: googleCalendarOAuth2Api
|
|
1283
|
+
|
|
1284
|
+
### Productivity:
|
|
1285
|
+
n8n-nodes-base.notion typeVersion: 2.2 \u2014 cred: notionApi
|
|
1286
|
+
n8n-nodes-base.airtable typeVersion: 2.1 \u2014 cred: airtableTokenApi
|
|
1287
|
+
n8n-nodes-base.github typeVersion: 1.1 \u2014 cred: githubApi
|
|
1288
|
+
n8n-nodes-base.jira typeVersion: 1 \u2014 cred: jiraSoftwareCloudApi
|
|
1289
|
+
n8n-nodes-base.hubspot typeVersion: 2.1 \u2014 cred: hubspotOAuth2Api
|
|
1290
|
+
|
|
1291
|
+
### Databases:
|
|
1292
|
+
n8n-nodes-base.postgres typeVersion: 2.5 \u2014 cred: postgres
|
|
1293
|
+
n8n-nodes-base.mySql typeVersion: 2.4 \u2014 cred: mySql
|
|
1294
|
+
n8n-nodes-base.redis typeVersion: 1 \u2014 cred: redis
|
|
1295
|
+
n8n-nodes-base.supabase typeVersion: 1 \u2014 cred: supabaseApi
|
|
1296
|
+
n8n-nodes-base.awsS3 typeVersion: 2 \u2014 cred: aws
|
|
1297
|
+
|
|
1298
|
+
### AI \u2014 Root nodes (sit on main data flow, receive ai_* connections as TARGETS):
|
|
1299
|
+
@n8n/n8n-nodes-langchain.agent typeVersion: 1.9 \u2014 params: promptType, text (if define), options.systemMessage
|
|
1300
|
+
@n8n/n8n-nodes-langchain.chainLlm typeVersion: 1.5
|
|
1301
|
+
@n8n/n8n-nodes-langchain.chainRetrievalQa typeVersion: 1.4
|
|
1302
|
+
@n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi \u2014 standalone node, calls OpenAI directly without sub-nodes
|
|
1303
|
+
@n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi \u2014 standalone node, calls Anthropic directly without sub-nodes
|
|
1304
|
+
|
|
1305
|
+
### AI \u2014 Sub-nodes (sources of ai_* connections, wire INTO root nodes above):
|
|
1306
|
+
@n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
|
|
1307
|
+
@n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
|
|
1308
|
+
@n8n/n8n-nodes-langchain.lmChatGoogleGemini typeVersion: 1 \u2014 cred: googlePalmApi \u2014 ai_languageModel
|
|
1309
|
+
@n8n/n8n-nodes-langchain.memoryBufferWindow typeVersion: 1.3 \u2014 \u2014 ai_memory
|
|
1310
|
+
@n8n/n8n-nodes-langchain.toolWorkflow typeVersion: 2 \u2014 \u2014 ai_tool
|
|
1311
|
+
@n8n/n8n-nodes-langchain.toolCode typeVersion: 1.1 \u2014 \u2014 ai_tool
|
|
1312
|
+
@n8n/n8n-nodes-langchain.toolHttpRequest typeVersion: 1.1 \u2014 \u2014 ai_tool
|
|
1313
|
+
@n8n/n8n-nodes-langchain.toolCalculator typeVersion: 1 \u2014 \u2014 ai_tool
|
|
1314
|
+
|
|
1315
|
+
### Resource locator (__rl) format (Google / Slack / Notion modern nodes):
|
|
1316
|
+
{ "__rl": true, "mode": "id", "value": "ACTUAL_ID" }
|
|
1317
|
+
{ "__rl": true, "mode": "name", "value": "#channel-name" }
|
|
1318
|
+
|
|
1319
|
+
### App node parameter pattern:
|
|
1320
|
+
{ "resource": "message", "operation": "send", ...operation-specific fields }
|
|
1321
|
+
|
|
1322
|
+
### Schedule Trigger \u2014 daily at 9am example:
|
|
1323
|
+
{ "rule": { "interval": [{ "field": "days", "daysInterval": 1, "triggerAtHour": 9, "triggerAtMinute": 0 }] } }
|
|
1324
|
+
Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] } }
|
|
1325
|
+
|
|
1326
|
+
---
|
|
1327
|
+
|
|
1328
|
+
## PRE-DELIVERY SELF-CHECK (do this before calling the tool):
|
|
1329
|
+
1. Every connection source/target name exists in nodes array
|
|
1330
|
+
2. No duplicate node names
|
|
1331
|
+
3. No duplicate node IDs
|
|
1332
|
+
4. No forbidden fields at the workflow root
|
|
1333
|
+
5. At least one trigger node present
|
|
1334
|
+
6. Every AI Agent has an ai_languageModel sub-node
|
|
1335
|
+
7. settings block is complete with executionOrder: "v1"
|
|
1336
|
+
|
|
1337
|
+
---
|
|
1338
|
+
|
|
1339
|
+
Respond ONLY with a generate_workflow tool call. No prose. No markdown outside the tool call.
|
|
1340
|
+
If the request is impossible or unclear, set the error field instead of generating a workflow.`;
|
|
1341
|
+
|
|
1342
|
+
// src/generation/prompt-builder.ts
|
|
1343
|
+
var RULE_REMEDIES = {
|
|
1344
|
+
1: "Provide a non-empty workflow name string",
|
|
1345
|
+
2: "Include at least one node in the nodes array",
|
|
1346
|
+
3: "Every node must have a unique UUID v4 string as its id field",
|
|
1347
|
+
4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
|
|
1348
|
+
5: "Every node must have a non-empty type string",
|
|
1349
|
+
6: "Every node must have a positive integer typeVersion",
|
|
1350
|
+
7: "Every node must have a position array of exactly [x, y] numbers",
|
|
1351
|
+
8: "Every node must have a non-empty name string",
|
|
1352
|
+
9: "connections must be a plain object (use {} if no connections)",
|
|
1353
|
+
10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
|
|
1354
|
+
12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
|
|
1355
|
+
14: "Include at least one trigger node (e.g. webhook, scheduleTrigger, manualTrigger)",
|
|
1356
|
+
15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
|
|
1357
|
+
16: "All node names must be unique within the workflow",
|
|
1358
|
+
17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
|
|
1359
|
+
18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
|
|
1360
|
+
19: "Use known safe typeVersion values for each node type",
|
|
1361
|
+
20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
|
|
1362
|
+
21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
|
|
1363
|
+
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
|
|
1364
|
+
};
|
|
1365
|
+
var PromptBuilder = class {
|
|
1366
|
+
build(request, matches, globalFailureRates = []) {
|
|
1367
|
+
const mode = this.resolveMode(matches);
|
|
1368
|
+
const system = this.buildSystem(matches, mode, globalFailureRates);
|
|
1369
|
+
const userMessage = this.buildUserMessage(request, matches, mode);
|
|
1370
|
+
return { system, userMessage, mode, matches };
|
|
1371
|
+
}
|
|
1372
|
+
buildCorrectionMessage(request, matches, allIssues, attempt) {
|
|
1373
|
+
const base = this.buildUserMessage(request, matches, this.resolveMode(matches));
|
|
1374
|
+
return `${base}
|
|
1375
|
+
|
|
1376
|
+
IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
|
|
1377
|
+
${allIssues.join("\n")}
|
|
1378
|
+
|
|
1379
|
+
Fix ALL of the above issues in your new response. Do not repeat any of these mistakes.`;
|
|
1380
|
+
}
|
|
1381
|
+
resolveMode(matches) {
|
|
1382
|
+
if (matches.length === 0) return "scratch";
|
|
1383
|
+
const top = matches[0];
|
|
1384
|
+
if (!top) return "scratch";
|
|
1385
|
+
return scoreToMode(top.score);
|
|
1386
|
+
}
|
|
1387
|
+
buildSystem(matches, mode, globalFailureRates = []) {
|
|
1388
|
+
const blocks = [
|
|
1389
|
+
{
|
|
1390
|
+
type: "text",
|
|
1391
|
+
text: SYSTEM_PROMPT_V1,
|
|
1392
|
+
cache_control: { type: "ephemeral" }
|
|
1393
|
+
}
|
|
1394
|
+
];
|
|
1395
|
+
if (mode === "reference" && matches.length > 0) {
|
|
1396
|
+
const refText = matches.slice(0, 3).map((m) => {
|
|
1397
|
+
const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1398
|
+
return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
|
|
1399
|
+
Nodes:
|
|
1400
|
+
${nodes}`;
|
|
1401
|
+
}).join("\n\n");
|
|
1402
|
+
blocks.push({
|
|
1403
|
+
type: "text",
|
|
1404
|
+
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1405
|
+
|
|
1406
|
+
${refText}`
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
if (mode === "direct" && matches[0]) {
|
|
1410
|
+
const match = matches[0];
|
|
1411
|
+
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1412
|
+
if (json.length > 3e4) {
|
|
1413
|
+
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1414
|
+
blocks.push({
|
|
1415
|
+
type: "text",
|
|
1416
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
|
|
1417
|
+
Nodes:
|
|
1418
|
+
${nodes}`
|
|
1419
|
+
});
|
|
1420
|
+
} else {
|
|
1421
|
+
blocks.push({
|
|
1422
|
+
type: "text",
|
|
1423
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
|
|
1424
|
+
|
|
1425
|
+
${json}`
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
|
|
1430
|
+
const hint = matches[0];
|
|
1431
|
+
const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
|
|
1432
|
+
blocks.push({
|
|
1433
|
+
type: "text",
|
|
1434
|
+
text: `## Weak Structural Hint
|
|
1435
|
+
A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
const warnings = this.buildFailureWarnings(matches, globalFailureRates);
|
|
1439
|
+
if (warnings) {
|
|
1440
|
+
blocks.push({ type: "text", text: warnings });
|
|
1441
|
+
}
|
|
1442
|
+
return blocks;
|
|
1443
|
+
}
|
|
1444
|
+
buildFailureWarnings(matches, globalFailureRates) {
|
|
1445
|
+
const lines = [];
|
|
1446
|
+
for (const match of matches) {
|
|
1447
|
+
const patterns = match.workflow.failurePatterns;
|
|
1448
|
+
if (!patterns?.length) continue;
|
|
1449
|
+
for (const fp of patterns) {
|
|
1450
|
+
const remedy = RULE_REMEDIES[fp.rule];
|
|
1451
|
+
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1452
|
+
lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
|
|
1456
|
+
for (const rule of highFreqRules) {
|
|
1457
|
+
const remedy = RULE_REMEDIES[rule.rule];
|
|
1458
|
+
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1459
|
+
lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
|
|
1460
|
+
}
|
|
1461
|
+
if (lines.length === 0) return null;
|
|
1462
|
+
const unique = [...new Set(lines)];
|
|
1463
|
+
return `## Known Failure Patterns \u2014 AVOID THESE
|
|
1464
|
+
|
|
1465
|
+
Previous builds frequently failed the following validation rules. Ensure your output does NOT repeat these mistakes:
|
|
1466
|
+
${unique.join("\n")}`;
|
|
1467
|
+
}
|
|
1468
|
+
buildUserMessage(request, _matches, _mode) {
|
|
1469
|
+
const namePart = request.name ? `
|
|
1470
|
+
Workflow name: "${request.name}"` : "";
|
|
1471
|
+
return `Build a workflow that: ${request.description}${namePart}`;
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1475
|
+
// src/telemetry/reader.ts
|
|
1476
|
+
var import_promises2 = require("fs/promises");
|
|
1477
|
+
var import_node_path2 = require("path");
|
|
1478
|
+
var import_node_os2 = require("os");
|
|
1479
|
+
var TelemetryReader = class {
|
|
1480
|
+
dir;
|
|
1481
|
+
cache = null;
|
|
1482
|
+
cacheTime = 0;
|
|
1483
|
+
constructor(dir) {
|
|
1484
|
+
this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
|
|
1485
|
+
}
|
|
1486
|
+
async getFailureRates(days = 30) {
|
|
1487
|
+
const now = Date.now();
|
|
1488
|
+
if (this.cache && now - this.cacheTime < 5 * 60 * 1e3) {
|
|
1489
|
+
return this.cache;
|
|
1490
|
+
}
|
|
1491
|
+
const events = await this.readRecentEvents(days);
|
|
1492
|
+
const buildSessions = new Set(
|
|
1493
|
+
events.filter((e) => e.eventType === "build_complete" && !e.data.dryRun).map((e) => e.sessionId)
|
|
1494
|
+
);
|
|
1495
|
+
if (buildSessions.size === 0) return [];
|
|
1496
|
+
const ruleSessions = /* @__PURE__ */ new Map();
|
|
1497
|
+
for (const event of events) {
|
|
1498
|
+
if (event.eventType !== "generation_attempt") continue;
|
|
1499
|
+
if (!buildSessions.has(event.sessionId)) continue;
|
|
1500
|
+
const data = event.data;
|
|
1501
|
+
if (data.validationPassed || !data.issues) continue;
|
|
1502
|
+
for (const issue of data.issues) {
|
|
1503
|
+
const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
|
|
1504
|
+
entry.sessions.add(event.sessionId);
|
|
1505
|
+
entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
|
|
1506
|
+
ruleSessions.set(issue.rule, entry);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
const rates = [];
|
|
1510
|
+
for (const [rule, entry] of ruleSessions) {
|
|
1511
|
+
let topMessage = "";
|
|
1512
|
+
let topCount = 0;
|
|
1513
|
+
for (const [msg, count] of entry.messages) {
|
|
1514
|
+
if (count > topCount) {
|
|
1515
|
+
topMessage = msg;
|
|
1516
|
+
topCount = count;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
rates.push({
|
|
1520
|
+
rule,
|
|
1521
|
+
failureCount: entry.sessions.size,
|
|
1522
|
+
totalBuilds: buildSessions.size,
|
|
1523
|
+
rate: entry.sessions.size / buildSessions.size,
|
|
1524
|
+
commonMessage: topMessage
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
rates.sort((a, b) => b.rate - a.rate);
|
|
1528
|
+
this.cache = rates;
|
|
1529
|
+
this.cacheTime = now;
|
|
1530
|
+
return rates;
|
|
1531
|
+
}
|
|
1532
|
+
async readRecentEvents(days) {
|
|
1533
|
+
let files;
|
|
1534
|
+
try {
|
|
1535
|
+
files = await (0, import_promises2.readdir)(this.dir);
|
|
1536
|
+
} catch {
|
|
1537
|
+
return [];
|
|
1538
|
+
}
|
|
1539
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
1540
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
1541
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
1542
|
+
const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
1543
|
+
const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr).sort();
|
|
1544
|
+
const events = [];
|
|
1545
|
+
for (const file of recentFiles) {
|
|
1546
|
+
try {
|
|
1547
|
+
const content = await (0, import_promises2.readFile)((0, import_node_path2.join)(this.dir, file), "utf-8");
|
|
1548
|
+
for (const line of content.split("\n")) {
|
|
1549
|
+
if (!line.trim()) continue;
|
|
1550
|
+
try {
|
|
1551
|
+
events.push(JSON.parse(line));
|
|
1552
|
+
} catch {
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
} catch {
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return events;
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
// src/utils/logger.ts
|
|
1563
|
+
var nullLogger = {
|
|
1564
|
+
debug() {
|
|
1565
|
+
},
|
|
1566
|
+
info() {
|
|
1567
|
+
},
|
|
1568
|
+
warn() {
|
|
1569
|
+
},
|
|
1570
|
+
error() {
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
// src/mcp-server.ts
|
|
1575
|
+
var import_node_fs = require("fs");
|
|
1576
|
+
var import_node_path3 = require("path");
|
|
1577
|
+
var import_node_url = require("url");
|
|
1578
|
+
var import_meta = {};
|
|
1579
|
+
var __dirname = (0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
1580
|
+
var pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path3.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
1581
|
+
var library = new FileLibrary();
|
|
1582
|
+
var validator = new N8nValidator();
|
|
1583
|
+
var stripper = new N8nFieldStripper();
|
|
1584
|
+
var promptBuilder = new PromptBuilder();
|
|
1585
|
+
function getTelemetryReader() {
|
|
1586
|
+
try {
|
|
1587
|
+
return new TelemetryReader();
|
|
1588
|
+
} catch {
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function isAllowed(action) {
|
|
1593
|
+
const key = `KAIROS_MCP_ALLOW_${action.toUpperCase()}`;
|
|
1594
|
+
return process.env[key] === "true";
|
|
1595
|
+
}
|
|
1596
|
+
function getApiClient() {
|
|
1597
|
+
const baseUrl = process.env["N8N_BASE_URL"];
|
|
1598
|
+
const apiKey = process.env["N8N_API_KEY"];
|
|
1599
|
+
if (!baseUrl || !apiKey) {
|
|
1600
|
+
throw new Error("N8N_BASE_URL and N8N_API_KEY environment variables are required for n8n operations");
|
|
1601
|
+
}
|
|
1602
|
+
return new N8nApiClient(baseUrl, apiKey, nullLogger);
|
|
1603
|
+
}
|
|
1604
|
+
var server = new import_mcp.McpServer({
|
|
1605
|
+
name: "kairos",
|
|
1606
|
+
version: pkg.version
|
|
1607
|
+
});
|
|
1608
|
+
server.tool(
|
|
1609
|
+
"kairos_prompt",
|
|
1610
|
+
"Get the specialized n8n workflow generation context. Returns a system prompt with node catalog, connection rules, validation rules, plus library matches and failure patterns for the given description. Feed this to yourself as context, then generate the workflow JSON.",
|
|
1611
|
+
{
|
|
1612
|
+
description: import_zod.z.string().describe("Plain-English description of the workflow to build"),
|
|
1613
|
+
name: import_zod.z.string().optional().describe("Optional workflow name override")
|
|
1614
|
+
},
|
|
1615
|
+
async ({ description, name }) => {
|
|
1616
|
+
await library.initialize();
|
|
1617
|
+
const matches = await library.search(description);
|
|
1618
|
+
const telemetryReader = getTelemetryReader();
|
|
1619
|
+
const failureRates = await telemetryReader?.getFailureRates() ?? [];
|
|
1620
|
+
const request = { description, ...name ? { name } : {} };
|
|
1621
|
+
const built = promptBuilder.build(request, matches, failureRates);
|
|
1622
|
+
const systemText = built.system.map((block) => block.text).join("\n\n---\n\n");
|
|
1623
|
+
return {
|
|
1624
|
+
content: [{
|
|
1625
|
+
type: "text",
|
|
1626
|
+
text: JSON.stringify({
|
|
1627
|
+
mode: built.mode,
|
|
1628
|
+
matchCount: matches.length,
|
|
1629
|
+
topMatchScore: matches[0]?.score ?? null,
|
|
1630
|
+
systemPrompt: systemText,
|
|
1631
|
+
userMessage: built.userMessage,
|
|
1632
|
+
outputFormat: {
|
|
1633
|
+
description: "Generate a JSON object with this exact structure. The workflow field contains the n8n workflow. credentialsNeeded lists services requiring credentials.",
|
|
1634
|
+
schema: {
|
|
1635
|
+
workflow: {
|
|
1636
|
+
name: "string \u2014 descriptive workflow name",
|
|
1637
|
+
nodes: "array \u2014 n8n node objects with id (UUID v4), type, typeVersion, name, position, parameters",
|
|
1638
|
+
connections: "object \u2014 keyed by source node NAME, maps to target nodes",
|
|
1639
|
+
settings: 'object \u2014 include executionOrder: "v1"'
|
|
1640
|
+
},
|
|
1641
|
+
credentialsNeeded: [{
|
|
1642
|
+
service: 'string \u2014 e.g. "Slack"',
|
|
1643
|
+
credentialType: 'string \u2014 e.g. "slackOAuth2Api"',
|
|
1644
|
+
description: "string \u2014 what the user needs to set up"
|
|
1645
|
+
}]
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}, null, 2)
|
|
1649
|
+
}]
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
);
|
|
1653
|
+
server.tool(
|
|
1654
|
+
"kairos_validate",
|
|
1655
|
+
"Validate n8n workflow JSON against 23 structural rules. Returns pass/fail with specific issues. If validation fails, fix the issues and call this again. Errors block deployment; warnings are advisory.",
|
|
1656
|
+
{
|
|
1657
|
+
workflow: import_zod.z.string().describe("The workflow JSON string to validate")
|
|
1658
|
+
},
|
|
1659
|
+
async ({ workflow: workflowStr }) => {
|
|
1660
|
+
let parsed;
|
|
1661
|
+
try {
|
|
1662
|
+
parsed = JSON.parse(workflowStr);
|
|
1663
|
+
} catch (e) {
|
|
1664
|
+
return {
|
|
1665
|
+
content: [{
|
|
1666
|
+
type: "text",
|
|
1667
|
+
text: JSON.stringify({
|
|
1668
|
+
valid: false,
|
|
1669
|
+
error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`
|
|
1670
|
+
}, null, 2)
|
|
1671
|
+
}]
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
const result = validator.validate(parsed);
|
|
1675
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
1676
|
+
const warnings = result.issues.filter((i) => i.severity === "warn");
|
|
1677
|
+
return {
|
|
1678
|
+
content: [{
|
|
1679
|
+
type: "text",
|
|
1680
|
+
text: JSON.stringify({
|
|
1681
|
+
valid: result.valid,
|
|
1682
|
+
errorCount: errors.length,
|
|
1683
|
+
warningCount: warnings.length,
|
|
1684
|
+
errors: errors.map((i) => ({
|
|
1685
|
+
rule: i.rule,
|
|
1686
|
+
message: i.message,
|
|
1687
|
+
nodeId: i.nodeId ?? null
|
|
1688
|
+
})),
|
|
1689
|
+
warnings: warnings.map((i) => ({
|
|
1690
|
+
rule: i.rule,
|
|
1691
|
+
message: i.message,
|
|
1692
|
+
nodeId: i.nodeId ?? null
|
|
1693
|
+
})),
|
|
1694
|
+
deployable: errors.length === 0
|
|
1695
|
+
}, null, 2)
|
|
1696
|
+
}]
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
);
|
|
1700
|
+
server.tool(
|
|
1701
|
+
"kairos_deploy",
|
|
1702
|
+
"Deploy a validated workflow to n8n. Pass the workflow JSON that passed kairos_validate. Strips server-assigned fields automatically. Requires N8N_BASE_URL and N8N_API_KEY.",
|
|
1703
|
+
{
|
|
1704
|
+
workflow: import_zod.z.string().describe("The validated workflow JSON string to deploy"),
|
|
1705
|
+
activate: import_zod.z.boolean().default(false).describe("Activate the workflow immediately after deployment")
|
|
1706
|
+
},
|
|
1707
|
+
async ({ workflow: workflowStr, activate }) => {
|
|
1708
|
+
if (!isAllowed("deploy")) {
|
|
1709
|
+
return {
|
|
1710
|
+
content: [{
|
|
1711
|
+
type: "text",
|
|
1712
|
+
text: JSON.stringify({ error: "Deploy is disabled. Set KAIROS_MCP_ALLOW_DEPLOY=true to enable." })
|
|
1713
|
+
}],
|
|
1714
|
+
isError: true
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
let parsed;
|
|
1718
|
+
try {
|
|
1719
|
+
parsed = JSON.parse(workflowStr);
|
|
1720
|
+
} catch (e) {
|
|
1721
|
+
return {
|
|
1722
|
+
content: [{
|
|
1723
|
+
type: "text",
|
|
1724
|
+
text: JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` })
|
|
1725
|
+
}]
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
const validation = validator.validate(parsed);
|
|
1729
|
+
const errors = validation.issues.filter((i) => i.severity === "error");
|
|
1730
|
+
if (errors.length > 0) {
|
|
1731
|
+
return {
|
|
1732
|
+
content: [{
|
|
1733
|
+
type: "text",
|
|
1734
|
+
text: JSON.stringify({
|
|
1735
|
+
error: "Workflow has validation errors \u2014 fix them before deploying",
|
|
1736
|
+
errors: errors.map((i) => ({ rule: i.rule, message: i.message }))
|
|
1737
|
+
}, null, 2)
|
|
1738
|
+
}]
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
const client = getApiClient();
|
|
1742
|
+
const stripped = stripper.stripForCreate(parsed);
|
|
1743
|
+
const response = await client.createWorkflow(stripped);
|
|
1744
|
+
if (activate) {
|
|
1745
|
+
if (!isAllowed("activate")) {
|
|
1746
|
+
return {
|
|
1747
|
+
content: [{
|
|
1748
|
+
type: "text",
|
|
1749
|
+
text: JSON.stringify({
|
|
1750
|
+
workflowId: response.id,
|
|
1751
|
+
name: response.name,
|
|
1752
|
+
activated: false,
|
|
1753
|
+
warning: "Workflow deployed but activation is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable.",
|
|
1754
|
+
url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
|
|
1755
|
+
}, null, 2)
|
|
1756
|
+
}]
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
await client.activateWorkflow(response.id);
|
|
1760
|
+
}
|
|
1761
|
+
await library.initialize();
|
|
1762
|
+
await library.save(parsed, {
|
|
1763
|
+
description: parsed.name,
|
|
1764
|
+
generationMode: "scratch",
|
|
1765
|
+
generationAttempts: 1
|
|
1766
|
+
});
|
|
1767
|
+
return {
|
|
1768
|
+
content: [{
|
|
1769
|
+
type: "text",
|
|
1770
|
+
text: JSON.stringify({
|
|
1771
|
+
workflowId: response.id,
|
|
1772
|
+
name: response.name,
|
|
1773
|
+
activated: activate,
|
|
1774
|
+
url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
|
|
1775
|
+
}, null, 2)
|
|
1776
|
+
}]
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
);
|
|
1780
|
+
server.tool(
|
|
1781
|
+
"kairos_search",
|
|
1782
|
+
"Search the local workflow library for similar past builds. Returns matching workflows with scores, useful for finding examples and reusing patterns.",
|
|
1783
|
+
{
|
|
1784
|
+
query: import_zod.z.string().describe("Search query \u2014 a workflow description or keywords"),
|
|
1785
|
+
limit: import_zod.z.number().default(5).describe("Maximum number of results")
|
|
1786
|
+
},
|
|
1787
|
+
async ({ query, limit }) => {
|
|
1788
|
+
await library.initialize();
|
|
1789
|
+
const matches = await library.search(query);
|
|
1790
|
+
return {
|
|
1791
|
+
content: [{
|
|
1792
|
+
type: "text",
|
|
1793
|
+
text: JSON.stringify(
|
|
1794
|
+
matches.slice(0, limit).map((m) => ({
|
|
1795
|
+
score: Number(m.score.toFixed(3)),
|
|
1796
|
+
mode: m.mode,
|
|
1797
|
+
description: m.workflow.description,
|
|
1798
|
+
nodeCount: m.workflow.workflow.nodes.length,
|
|
1799
|
+
nodes: m.workflow.workflow.nodes.map((n) => n.name),
|
|
1800
|
+
failurePatterns: m.workflow.failurePatterns ?? []
|
|
1801
|
+
})),
|
|
1802
|
+
null,
|
|
1803
|
+
2
|
|
1804
|
+
)
|
|
1805
|
+
}]
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
);
|
|
1809
|
+
server.tool(
|
|
1810
|
+
"kairos_list",
|
|
1811
|
+
"List all workflows deployed on the connected n8n instance.",
|
|
1812
|
+
{},
|
|
1813
|
+
async () => {
|
|
1814
|
+
const client = getApiClient();
|
|
1815
|
+
const workflows = await client.listWorkflows();
|
|
1816
|
+
return {
|
|
1817
|
+
content: [{
|
|
1818
|
+
type: "text",
|
|
1819
|
+
text: JSON.stringify(workflows, null, 2)
|
|
1820
|
+
}]
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
);
|
|
1824
|
+
server.tool(
|
|
1825
|
+
"kairos_get",
|
|
1826
|
+
"Get the full JSON definition of a specific workflow by ID.",
|
|
1827
|
+
{
|
|
1828
|
+
workflow_id: import_zod.z.string().describe("The n8n workflow ID")
|
|
1829
|
+
},
|
|
1830
|
+
async ({ workflow_id }) => {
|
|
1831
|
+
const client = getApiClient();
|
|
1832
|
+
const workflow = await client.getWorkflow(workflow_id);
|
|
1833
|
+
return {
|
|
1834
|
+
content: [{
|
|
1835
|
+
type: "text",
|
|
1836
|
+
text: JSON.stringify(workflow, null, 2)
|
|
1837
|
+
}]
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
);
|
|
1841
|
+
server.tool(
|
|
1842
|
+
"kairos_activate",
|
|
1843
|
+
"Activate a deployed workflow so it starts running on triggers.",
|
|
1844
|
+
{
|
|
1845
|
+
workflow_id: import_zod.z.string().describe("The n8n workflow ID to activate")
|
|
1846
|
+
},
|
|
1847
|
+
async ({ workflow_id }) => {
|
|
1848
|
+
if (!isAllowed("activate")) {
|
|
1849
|
+
return {
|
|
1850
|
+
content: [{
|
|
1851
|
+
type: "text",
|
|
1852
|
+
text: JSON.stringify({ error: "Activate is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable." })
|
|
1853
|
+
}],
|
|
1854
|
+
isError: true
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
const client = getApiClient();
|
|
1858
|
+
await client.activateWorkflow(workflow_id);
|
|
1859
|
+
return {
|
|
1860
|
+
content: [{
|
|
1861
|
+
type: "text",
|
|
1862
|
+
text: `Activated workflow ${workflow_id}`
|
|
1863
|
+
}]
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
);
|
|
1867
|
+
server.tool(
|
|
1868
|
+
"kairos_deactivate",
|
|
1869
|
+
"Deactivate a running workflow.",
|
|
1870
|
+
{
|
|
1871
|
+
workflow_id: import_zod.z.string().describe("The n8n workflow ID to deactivate")
|
|
1872
|
+
},
|
|
1873
|
+
async ({ workflow_id }) => {
|
|
1874
|
+
const client = getApiClient();
|
|
1875
|
+
await client.deactivateWorkflow(workflow_id);
|
|
1876
|
+
return {
|
|
1877
|
+
content: [{
|
|
1878
|
+
type: "text",
|
|
1879
|
+
text: `Deactivated workflow ${workflow_id}`
|
|
1880
|
+
}]
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
);
|
|
1884
|
+
server.tool(
|
|
1885
|
+
"kairos_delete",
|
|
1886
|
+
"Delete a workflow from n8n. This is irreversible.",
|
|
1887
|
+
{
|
|
1888
|
+
workflow_id: import_zod.z.string().describe("The n8n workflow ID to delete")
|
|
1889
|
+
},
|
|
1890
|
+
async ({ workflow_id }) => {
|
|
1891
|
+
if (!isAllowed("delete")) {
|
|
1892
|
+
return {
|
|
1893
|
+
content: [{
|
|
1894
|
+
type: "text",
|
|
1895
|
+
text: JSON.stringify({ error: "Delete is disabled. Set KAIROS_MCP_ALLOW_DELETE=true to enable." })
|
|
1896
|
+
}],
|
|
1897
|
+
isError: true
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
const client = getApiClient();
|
|
1901
|
+
await client.deleteWorkflow(workflow_id);
|
|
1902
|
+
return {
|
|
1903
|
+
content: [{
|
|
1904
|
+
type: "text",
|
|
1905
|
+
text: `Deleted workflow ${workflow_id}`
|
|
1906
|
+
}]
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
);
|
|
1910
|
+
server.tool(
|
|
1911
|
+
"kairos_executions",
|
|
1912
|
+
"List recent executions for a workflow, showing status and timing.",
|
|
1913
|
+
{
|
|
1914
|
+
workflow_id: import_zod.z.string().optional().describe("Filter to a specific workflow ID (omit for all)"),
|
|
1915
|
+
limit: import_zod.z.number().default(20).describe("Maximum number of executions to return")
|
|
1916
|
+
},
|
|
1917
|
+
async ({ workflow_id, limit }) => {
|
|
1918
|
+
const client = getApiClient();
|
|
1919
|
+
const executions = await client.getExecutions(workflow_id, { limit });
|
|
1920
|
+
return {
|
|
1921
|
+
content: [{
|
|
1922
|
+
type: "text",
|
|
1923
|
+
text: JSON.stringify(executions, null, 2)
|
|
1924
|
+
}]
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
);
|
|
1928
|
+
async function main() {
|
|
1929
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
1930
|
+
await server.connect(transport);
|
|
1931
|
+
}
|
|
1932
|
+
main().catch((err) => {
|
|
1933
|
+
console.error("Kairos MCP server failed to start:", err);
|
|
1934
|
+
process.exit(1);
|
|
1935
|
+
});
|
|
1936
|
+
//# sourceMappingURL=mcp-server.cjs.map
|