@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.
@@ -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