@kairos-sdk/core 0.4.5 → 0.5.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 +18 -10
- package/dist/{chunk-6CLI43FI.js → chunk-5GAY7CSJ.js} +109 -13
- package/dist/chunk-5GAY7CSJ.js.map +1 -0
- package/dist/{chunk-CR2NHLOH.js → chunk-EVOAYH2K.js} +57 -11
- package/dist/chunk-EVOAYH2K.js.map +1 -0
- package/dist/{chunk-4TS6GW6O.js → chunk-HBGZTUUZ.js} +26 -13
- package/dist/chunk-HBGZTUUZ.js.map +1 -0
- package/dist/{chunk-6IXW3WCC.js → chunk-KIFT5LA7.js} +532 -76
- package/dist/chunk-KIFT5LA7.js.map +1 -0
- package/dist/cli.cjs +738 -111
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +31 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +711 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -5
- package/dist/mcp-server.cjs +875 -333
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +293 -251
- package/dist/mcp-server.js.map +1 -1
- package/dist/{reader-CpUcHhKW.d.ts → reader-B5mV20H6.d.cts} +34 -4
- package/dist/{reader-CpUcHhKW.d.cts → reader-B5mV20H6.d.ts} +34 -4
- package/dist/standalone.cjs +602 -84
- package/dist/standalone.cjs.map +1 -1
- package/dist/standalone.d.cts +2 -1
- package/dist/standalone.d.ts +2 -1
- package/dist/standalone.js +3 -3
- package/package.json +4 -1
- package/dist/chunk-4TS6GW6O.js.map +0 -1
- package/dist/chunk-6CLI43FI.js.map +0 -1
- package/dist/chunk-6IXW3WCC.js.map +0 -1
- package/dist/chunk-CR2NHLOH.js.map +0 -1
package/dist/mcp-server.cjs
CHANGED
|
@@ -13,7 +13,13 @@ var import_node_os = require("os");
|
|
|
13
13
|
|
|
14
14
|
// src/utils/uuid.ts
|
|
15
15
|
function generateUUID() {
|
|
16
|
-
|
|
16
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
17
|
+
return crypto.randomUUID();
|
|
18
|
+
}
|
|
19
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
20
|
+
const r = Math.random() * 16 | 0;
|
|
21
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
22
|
+
});
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
// src/utils/thresholds.ts
|
|
@@ -26,12 +32,32 @@ function scoreToMode(score) {
|
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
// src/library/scorer.ts
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
function loadWeights() {
|
|
36
|
+
const raw = {
|
|
37
|
+
tfidf: parseFloat(process.env["KAIROS_WEIGHT_TFIDF"] ?? ""),
|
|
38
|
+
nodeFingerprint: parseFloat(process.env["KAIROS_WEIGHT_JACCARD"] ?? ""),
|
|
39
|
+
outcome: parseFloat(process.env["KAIROS_WEIGHT_OUTCOME"] ?? ""),
|
|
40
|
+
deploy: parseFloat(process.env["KAIROS_WEIGHT_DEPLOY"] ?? "")
|
|
41
|
+
};
|
|
42
|
+
const defaults = { tfidf: 0.35, nodeFingerprint: 0.3, outcome: 0.2, deploy: 0.15 };
|
|
43
|
+
const anySet = Object.values(raw).some((v) => !isNaN(v) && v >= 0);
|
|
44
|
+
if (!anySet) return defaults;
|
|
45
|
+
const w = {
|
|
46
|
+
tfidf: !isNaN(raw.tfidf) && raw.tfidf >= 0 ? raw.tfidf : defaults.tfidf,
|
|
47
|
+
nodeFingerprint: !isNaN(raw.nodeFingerprint) && raw.nodeFingerprint >= 0 ? raw.nodeFingerprint : defaults.nodeFingerprint,
|
|
48
|
+
outcome: !isNaN(raw.outcome) && raw.outcome >= 0 ? raw.outcome : defaults.outcome,
|
|
49
|
+
deploy: !isNaN(raw.deploy) && raw.deploy >= 0 ? raw.deploy : defaults.deploy
|
|
50
|
+
};
|
|
51
|
+
const total = w.tfidf + w.nodeFingerprint + w.outcome + w.deploy;
|
|
52
|
+
if (total <= 0) return defaults;
|
|
53
|
+
return {
|
|
54
|
+
tfidf: w.tfidf / total,
|
|
55
|
+
nodeFingerprint: w.nodeFingerprint / total,
|
|
56
|
+
outcome: w.outcome / total,
|
|
57
|
+
deploy: w.deploy / total
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
var WEIGHTS = loadWeights();
|
|
35
61
|
var NODE_KEYWORDS = {
|
|
36
62
|
slack: ["slack", "slackApi"],
|
|
37
63
|
email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
|
|
@@ -216,6 +242,8 @@ function clusterWorkflows(workflows) {
|
|
|
216
242
|
}
|
|
217
243
|
return clusters.sort((a, b) => b.members.length - a.members.length);
|
|
218
244
|
}
|
|
245
|
+
var NOVELTY_BOOST = 0.05;
|
|
246
|
+
var NOVELTY_PENALTY = 0.03;
|
|
219
247
|
function rerank(candidates, clusters) {
|
|
220
248
|
const clusterMap = /* @__PURE__ */ new Map();
|
|
221
249
|
for (const cluster of clusters) {
|
|
@@ -223,7 +251,7 @@ function rerank(candidates, clusters) {
|
|
|
223
251
|
clusterMap.set(member.id, cluster);
|
|
224
252
|
}
|
|
225
253
|
}
|
|
226
|
-
|
|
254
|
+
const pass1 = candidates.map((c) => {
|
|
227
255
|
const cluster = clusterMap.get(c.workflow.id);
|
|
228
256
|
let boost = 0;
|
|
229
257
|
if (cluster && cluster.avgFirstTryPassRate > 0) {
|
|
@@ -235,7 +263,25 @@ function rerank(candidates, clusters) {
|
|
|
235
263
|
return {
|
|
236
264
|
workflow: c.workflow,
|
|
237
265
|
score: Math.max(0, Math.min(1, c.score + boost)),
|
|
238
|
-
|
|
266
|
+
cluster
|
|
267
|
+
};
|
|
268
|
+
}).sort((a, b) => b.score - a.score);
|
|
269
|
+
const seenFingerprints = /* @__PURE__ */ new Set();
|
|
270
|
+
return pass1.map((c) => {
|
|
271
|
+
const fpKey = c.cluster ? fingerprintKey(c.cluster.fingerprint) : null;
|
|
272
|
+
let noveltyAdjust = 0;
|
|
273
|
+
if (fpKey !== null) {
|
|
274
|
+
if (!seenFingerprints.has(fpKey)) {
|
|
275
|
+
seenFingerprints.add(fpKey);
|
|
276
|
+
noveltyAdjust = NOVELTY_BOOST;
|
|
277
|
+
} else {
|
|
278
|
+
noveltyAdjust = -NOVELTY_PENALTY;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
workflow: c.workflow,
|
|
283
|
+
score: Math.max(0, Math.min(1, c.score + noveltyAdjust)),
|
|
284
|
+
...c.cluster ? { clusterPattern: c.cluster.pattern } : {}
|
|
239
285
|
};
|
|
240
286
|
}).sort((a, b) => b.score - a.score);
|
|
241
287
|
}
|
|
@@ -252,7 +298,11 @@ function buildSearchCorpus(w) {
|
|
|
252
298
|
});
|
|
253
299
|
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
254
300
|
}
|
|
255
|
-
var
|
|
301
|
+
var _rawSize = parseInt(process.env["KAIROS_LIBRARY_SIZE"] ?? "500", 10);
|
|
302
|
+
var MAX_LIBRARY_SIZE = Number.isFinite(_rawSize) && _rawSize >= 10 ? _rawSize : 500;
|
|
303
|
+
function evictionScore(m) {
|
|
304
|
+
return (m.deployCount ?? 0) * 3 + (m.timesRetrieved ?? 0) + (m.outcomeStats?.totalUses ?? 0);
|
|
305
|
+
}
|
|
256
306
|
function isValidMeta(item) {
|
|
257
307
|
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
258
308
|
}
|
|
@@ -300,6 +350,7 @@ var FileLibrary = class {
|
|
|
300
350
|
} catch {
|
|
301
351
|
this.meta = [];
|
|
302
352
|
}
|
|
353
|
+
await this.scanForOrphansAndCleanup();
|
|
303
354
|
} else {
|
|
304
355
|
try {
|
|
305
356
|
const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
|
|
@@ -314,6 +365,31 @@ var FileLibrary = class {
|
|
|
314
365
|
await (0, import_promises.mkdir)(this.workflowsDir, { recursive: true });
|
|
315
366
|
}
|
|
316
367
|
}
|
|
368
|
+
async scanForOrphansAndCleanup() {
|
|
369
|
+
let entries;
|
|
370
|
+
try {
|
|
371
|
+
entries = await (0, import_promises.readdir)(this.workflowsDir);
|
|
372
|
+
} catch {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const indexedIds = new Set(this.meta.map((m) => m.id));
|
|
376
|
+
const orphanIds = [];
|
|
377
|
+
for (const filename of entries) {
|
|
378
|
+
if (filename.endsWith(".tmp")) {
|
|
379
|
+
await (0, import_promises.unlink)((0, import_node_path.join)(this.workflowsDir, filename)).catch(() => {
|
|
380
|
+
});
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (!filename.endsWith(".json")) continue;
|
|
384
|
+
const id = filename.slice(0, -5);
|
|
385
|
+
if (!indexedIds.has(id)) {
|
|
386
|
+
orphanIds.push(id);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (orphanIds.length > 0) {
|
|
390
|
+
console.warn(`[FileLibrary] Found ${orphanIds.length} orphaned workflow file(s) not in index: ${orphanIds.join(", ")}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
317
393
|
/**
|
|
318
394
|
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
319
395
|
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
@@ -384,10 +460,12 @@ var FileLibrary = class {
|
|
|
384
460
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
385
461
|
const docCount = shells.length;
|
|
386
462
|
const idf = /* @__PURE__ */ new Map();
|
|
463
|
+
const idfCeiling = Math.log(docCount + 1) + 1;
|
|
387
464
|
const allTokens = new Set(queryTokens);
|
|
388
465
|
for (const token of allTokens) {
|
|
389
466
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
390
|
-
|
|
467
|
+
const rawIdf = Math.log((docCount + 1) / (docsWithToken + 1)) + 1;
|
|
468
|
+
idf.set(token, rawIdf / idfCeiling);
|
|
391
469
|
}
|
|
392
470
|
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
393
471
|
const clusters = clusterWorkflows(shells);
|
|
@@ -413,6 +491,27 @@ var FileLibrary = class {
|
|
|
413
491
|
return results.filter((r) => r !== null);
|
|
414
492
|
}
|
|
415
493
|
async save(workflow, metadata) {
|
|
494
|
+
const existingByN8nId = metadata.n8nWorkflowId ? this.meta.find((m) => m.n8nWorkflowId === metadata.n8nWorkflowId) : void 0;
|
|
495
|
+
const normalizedDesc = metadata.description.trim().toLowerCase();
|
|
496
|
+
const existing = existingByN8nId ?? this.meta.find((m) => m.description.trim().toLowerCase() === normalizedDesc);
|
|
497
|
+
if (existing) {
|
|
498
|
+
existing.description = metadata.description;
|
|
499
|
+
existing.workflowName = workflow.name;
|
|
500
|
+
existing.cachedNodeTypes = workflow.nodes.map((n) => n.type);
|
|
501
|
+
if (metadata.n8nWorkflowId) existing.n8nWorkflowId = metadata.n8nWorkflowId;
|
|
502
|
+
if (metadata.generationAttempts != null) {
|
|
503
|
+
existing.generationAttempts = metadata.generationAttempts;
|
|
504
|
+
}
|
|
505
|
+
if (metadata.failurePatterns?.length) {
|
|
506
|
+
existing.failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
507
|
+
}
|
|
508
|
+
if (metadata.tags?.length) {
|
|
509
|
+
existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...metadata.tags])];
|
|
510
|
+
}
|
|
511
|
+
await this.writeWorkflowFile(existing.id, workflow);
|
|
512
|
+
await this.persist();
|
|
513
|
+
return existing.id;
|
|
514
|
+
}
|
|
416
515
|
const id = generateUUID();
|
|
417
516
|
await this.writeWorkflowFile(id, workflow);
|
|
418
517
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
@@ -434,25 +533,27 @@ var FileLibrary = class {
|
|
|
434
533
|
...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
|
|
435
534
|
...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
|
|
436
535
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
437
|
-
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
536
|
+
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {},
|
|
537
|
+
...metadata.n8nWorkflowId ? { n8nWorkflowId: metadata.n8nWorkflowId } : {}
|
|
438
538
|
};
|
|
439
539
|
this.meta.push(meta);
|
|
440
540
|
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
441
541
|
this.meta.sort((a, b) => {
|
|
442
542
|
if (a.id === id) return -1;
|
|
443
543
|
if (b.id === id) return 1;
|
|
444
|
-
return (b
|
|
544
|
+
return evictionScore(b) - evictionScore(a);
|
|
445
545
|
});
|
|
446
546
|
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
447
547
|
}
|
|
448
548
|
await this.persist();
|
|
449
549
|
return id;
|
|
450
550
|
}
|
|
451
|
-
async recordDeployment(id) {
|
|
551
|
+
async recordDeployment(id, n8nWorkflowId) {
|
|
452
552
|
const m = this.meta.find((m2) => m2.id === id);
|
|
453
553
|
if (m) {
|
|
454
554
|
m.deployCount++;
|
|
455
555
|
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
556
|
+
if (n8nWorkflowId) m.n8nWorkflowId = n8nWorkflowId;
|
|
456
557
|
await this.persist();
|
|
457
558
|
}
|
|
458
559
|
}
|
|
@@ -515,37 +616,98 @@ var FileLibrary = class {
|
|
|
515
616
|
}
|
|
516
617
|
return [...map.values()];
|
|
517
618
|
}
|
|
619
|
+
// ── Cross-process file locking ────────────────────────────────────────────
|
|
620
|
+
// Uses O_EXCL (exclusive create) which is atomic on POSIX and Windows NTFS.
|
|
621
|
+
// Protects the read-modify-write cycle in persist() from concurrent writers
|
|
622
|
+
// in separate OS processes (e.g. MCP server + CLI running simultaneously).
|
|
623
|
+
get lockPath() {
|
|
624
|
+
return (0, import_node_path.join)(this.dir, ".index.lock");
|
|
625
|
+
}
|
|
626
|
+
async acquireLock(timeoutMs = 3e3) {
|
|
627
|
+
const deadline = Date.now() + timeoutMs;
|
|
628
|
+
let delayMs = 10;
|
|
629
|
+
while (true) {
|
|
630
|
+
try {
|
|
631
|
+
const fh = await (0, import_promises.open)(this.lockPath, "wx");
|
|
632
|
+
await fh.writeFile(String(process.pid));
|
|
633
|
+
await fh.close();
|
|
634
|
+
return async () => {
|
|
635
|
+
await (0, import_promises.unlink)(this.lockPath).catch(() => {
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
} catch {
|
|
639
|
+
try {
|
|
640
|
+
const content = await (0, import_promises.readFile)(this.lockPath, "utf-8");
|
|
641
|
+
const lockPid = parseInt(content.trim(), 10);
|
|
642
|
+
const fileStat = await (0, import_promises.stat)(this.lockPath);
|
|
643
|
+
const ageMs = Date.now() - fileStat.mtimeMs;
|
|
644
|
+
if (ageMs > 1e4) {
|
|
645
|
+
await (0, import_promises.unlink)(this.lockPath).catch(() => {
|
|
646
|
+
});
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (!isNaN(lockPid)) {
|
|
650
|
+
try {
|
|
651
|
+
process.kill(lockPid, 0);
|
|
652
|
+
} catch {
|
|
653
|
+
await (0, import_promises.unlink)(this.lockPath).catch(() => {
|
|
654
|
+
});
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
} catch {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (Date.now() > deadline) {
|
|
662
|
+
return async () => {
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
666
|
+
delayMs = Math.min(delayMs * 1.5, 200);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
518
670
|
/**
|
|
519
671
|
* Direct write used only during migration (before writeQueue is needed).
|
|
520
672
|
*/
|
|
521
673
|
async persistNow() {
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
674
|
+
const releaseLock = await this.acquireLock();
|
|
675
|
+
try {
|
|
676
|
+
const indexPath = (0, import_node_path.join)(this.dir, "index.json");
|
|
677
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
678
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
679
|
+
await (0, import_promises.rename)(tmpPath, indexPath);
|
|
680
|
+
} finally {
|
|
681
|
+
await releaseLock();
|
|
682
|
+
}
|
|
526
683
|
}
|
|
527
684
|
persist() {
|
|
528
685
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
529
|
-
const
|
|
530
|
-
let onDisk = [];
|
|
686
|
+
const releaseLock = await this.acquireLock();
|
|
531
687
|
try {
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
688
|
+
const indexPath = (0, import_node_path.join)(this.dir, "index.json");
|
|
689
|
+
let onDisk = [];
|
|
690
|
+
try {
|
|
691
|
+
const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
|
|
692
|
+
const parsed = JSON.parse(raw);
|
|
693
|
+
if (Array.isArray(parsed)) {
|
|
694
|
+
onDisk = parsed.filter(isValidMeta);
|
|
695
|
+
}
|
|
696
|
+
} catch {
|
|
536
697
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
698
|
+
const ourIds = new Set(this.meta.map((m) => m.id));
|
|
699
|
+
const external = onDisk.filter((m) => !ourIds.has(m.id));
|
|
700
|
+
let merged = [...this.meta, ...external];
|
|
701
|
+
if (merged.length > MAX_LIBRARY_SIZE) {
|
|
702
|
+
merged.sort((a, b) => evictionScore(b) - evictionScore(a));
|
|
703
|
+
merged = merged.slice(0, MAX_LIBRARY_SIZE);
|
|
704
|
+
}
|
|
705
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
706
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
707
|
+
await (0, import_promises.rename)(tmpPath, indexPath);
|
|
708
|
+
} finally {
|
|
709
|
+
await releaseLock();
|
|
545
710
|
}
|
|
546
|
-
const tmpPath = `${indexPath}.tmp`;
|
|
547
|
-
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
548
|
-
await (0, import_promises.rename)(tmpPath, indexPath);
|
|
549
711
|
});
|
|
550
712
|
return this.writeQueue;
|
|
551
713
|
}
|
|
@@ -651,6 +813,14 @@ var NodeRegistry = class {
|
|
|
651
813
|
if (!def) return true;
|
|
652
814
|
return def.safeTypeVersions.includes(version);
|
|
653
815
|
}
|
|
816
|
+
// Returns true when the version is a positive integer greater than the highest
|
|
817
|
+
// known safe version — indicates a newer release rather than a bad value.
|
|
818
|
+
isVersionNewer(type, version) {
|
|
819
|
+
const def = this.byType.get(type);
|
|
820
|
+
if (!def || def.safeTypeVersions.length === 0) return false;
|
|
821
|
+
const max = Math.max(...def.safeTypeVersions);
|
|
822
|
+
return Number.isInteger(version) && version > max;
|
|
823
|
+
}
|
|
654
824
|
getRequiredParams(type) {
|
|
655
825
|
return this.byType.get(type)?.requiredParams ?? [];
|
|
656
826
|
}
|
|
@@ -721,6 +891,14 @@ var N8nValidator = class {
|
|
|
721
891
|
this.checkRule24(workflow, issues);
|
|
722
892
|
this.checkRule25(workflow, issues);
|
|
723
893
|
this.checkRule26(workflow, issues);
|
|
894
|
+
this.checkRule27(workflow, issues);
|
|
895
|
+
this.checkRule28(workflow, issues);
|
|
896
|
+
this.checkRule29(workflow, issues);
|
|
897
|
+
this.checkRule30(workflow, issues);
|
|
898
|
+
this.checkRule31(workflow, issues);
|
|
899
|
+
this.checkRule32(workflow, issues);
|
|
900
|
+
this.checkRule33(workflow, issues);
|
|
901
|
+
this.checkRule34(workflow, issues);
|
|
724
902
|
if (Array.isArray(workflow.nodes)) {
|
|
725
903
|
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
726
904
|
for (const issue of issues) {
|
|
@@ -972,19 +1150,22 @@ var N8nValidator = class {
|
|
|
972
1150
|
}
|
|
973
1151
|
}
|
|
974
1152
|
}
|
|
975
|
-
// Rule 19 (WARN): typeVersion is within known safe range for registered node types
|
|
1153
|
+
// Rule 19 (WARN): typeVersion is within known safe range for registered node types.
|
|
1154
|
+
// In lenient mode (KAIROS_REGISTRY_STRICT != 'true'), versions higher than the known
|
|
1155
|
+
// max are allowed — they likely represent newer n8n releases Kairos hasn't catalogued yet.
|
|
976
1156
|
checkRule19(w, issues) {
|
|
977
1157
|
if (!Array.isArray(w.nodes)) return;
|
|
1158
|
+
const strict = process.env["KAIROS_REGISTRY_STRICT"] === "true";
|
|
978
1159
|
for (const node of w.nodes) {
|
|
979
1160
|
if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
|
|
980
|
-
if (
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1161
|
+
if (this.registry.isVersionSafe(node.type, node.typeVersion)) continue;
|
|
1162
|
+
if (!strict && this.registry.isVersionNewer(node.type, node.typeVersion)) continue;
|
|
1163
|
+
this.warn(
|
|
1164
|
+
issues,
|
|
1165
|
+
19,
|
|
1166
|
+
`Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
|
|
1167
|
+
node.id
|
|
1168
|
+
);
|
|
988
1169
|
}
|
|
989
1170
|
}
|
|
990
1171
|
// Rule 20 (WARN): cycle detection — no node should be reachable from itself
|
|
@@ -1033,6 +1214,27 @@ var N8nValidator = class {
|
|
|
1033
1214
|
}
|
|
1034
1215
|
}
|
|
1035
1216
|
}
|
|
1217
|
+
// Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
|
|
1218
|
+
checkRule21(w, issues) {
|
|
1219
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1220
|
+
const webhooksNeedingResponse = w.nodes.filter((n) => {
|
|
1221
|
+
if (!n.type.includes("webhook")) return false;
|
|
1222
|
+
const params = n.parameters;
|
|
1223
|
+
return params?.responseMode === "responseNode";
|
|
1224
|
+
});
|
|
1225
|
+
if (webhooksNeedingResponse.length === 0) return;
|
|
1226
|
+
const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
|
|
1227
|
+
if (!hasRespondNode) {
|
|
1228
|
+
for (const wh of webhooksNeedingResponse) {
|
|
1229
|
+
this.warn(
|
|
1230
|
+
issues,
|
|
1231
|
+
21,
|
|
1232
|
+
`Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
|
|
1233
|
+
wh.id
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1036
1238
|
// Rule 22 (WARN): check requiredParams from registry
|
|
1037
1239
|
checkRule22(w, issues) {
|
|
1038
1240
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -1141,23 +1343,162 @@ var N8nValidator = class {
|
|
|
1141
1343
|
walk(params);
|
|
1142
1344
|
return expressions;
|
|
1143
1345
|
}
|
|
1144
|
-
// Rule
|
|
1145
|
-
|
|
1346
|
+
// Rule 27 (WARN): httpRequest URL is a placeholder
|
|
1347
|
+
checkRule27(w, issues) {
|
|
1146
1348
|
if (!Array.isArray(w.nodes)) return;
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1349
|
+
const PLACEHOLDER_RE = [
|
|
1350
|
+
/^https?:\/\/example\.com/i,
|
|
1351
|
+
/your[-_]?(api[-_]?)?url/i,
|
|
1352
|
+
/^https?:\/\/$/,
|
|
1353
|
+
/^<.+>$/,
|
|
1354
|
+
/placeholder/i
|
|
1355
|
+
];
|
|
1356
|
+
for (const node of w.nodes) {
|
|
1357
|
+
if (node.type !== "n8n-nodes-base.httpRequest") continue;
|
|
1358
|
+
const params = node.parameters;
|
|
1359
|
+
const url = params?.["url"];
|
|
1360
|
+
if (typeof url !== "string" || url.trim() === "") continue;
|
|
1361
|
+
if (PLACEHOLDER_RE.some((re) => re.test(url.trim()))) {
|
|
1156
1362
|
this.warn(
|
|
1157
1363
|
issues,
|
|
1158
|
-
|
|
1159
|
-
`
|
|
1160
|
-
|
|
1364
|
+
27,
|
|
1365
|
+
`Node "${node.name}" httpRequest URL appears to be a placeholder: "${url}" \u2014 replace with your actual endpoint`,
|
|
1366
|
+
node.id
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
// Rule 28 (WARN): code node with empty or comment-only code
|
|
1372
|
+
checkRule28(w, issues) {
|
|
1373
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1374
|
+
for (const node of w.nodes) {
|
|
1375
|
+
if (node.type !== "n8n-nodes-base.code") continue;
|
|
1376
|
+
const params = node.parameters;
|
|
1377
|
+
const jsCode = typeof params?.["jsCode"] === "string" ? params["jsCode"] : "";
|
|
1378
|
+
const pythonCode = typeof params?.["pythonCode"] === "string" ? params["pythonCode"] : "";
|
|
1379
|
+
const code = jsCode || pythonCode;
|
|
1380
|
+
const stripped = code.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/#[^\n]*/g, "").trim();
|
|
1381
|
+
if (!stripped) {
|
|
1382
|
+
this.warn(issues, 28, `Node "${node.name}" code node has no executable code`, node.id);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
// Rule 29 (WARN): slack node message operation missing channel
|
|
1387
|
+
checkRule29(w, issues) {
|
|
1388
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1389
|
+
for (const node of w.nodes) {
|
|
1390
|
+
if (node.type !== "n8n-nodes-base.slack") continue;
|
|
1391
|
+
const params = node.parameters;
|
|
1392
|
+
const resource = params?.["resource"];
|
|
1393
|
+
const operation = params?.["operation"];
|
|
1394
|
+
const isMessageOp = resource === "message" || operation === "sendMessage" || operation === "post";
|
|
1395
|
+
if (!isMessageOp) continue;
|
|
1396
|
+
const channel = params?.["channel"] ?? params?.["channelId"];
|
|
1397
|
+
const rlValue = typeof channel === "object" && channel !== null ? channel["value"] : void 0;
|
|
1398
|
+
const isEmpty = channel === void 0 || channel === null || typeof channel === "string" && channel.trim() === "" || typeof channel === "object" && (!rlValue || typeof rlValue === "string" && rlValue.trim() === "");
|
|
1399
|
+
if (isEmpty) {
|
|
1400
|
+
this.warn(issues, 29, `Node "${node.name}" Slack message has no channel specified`, node.id);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// Rule 30 (WARN): gmail node send operation missing recipient
|
|
1405
|
+
checkRule30(w, issues) {
|
|
1406
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1407
|
+
for (const node of w.nodes) {
|
|
1408
|
+
if (node.type !== "n8n-nodes-base.gmail") continue;
|
|
1409
|
+
const params = node.parameters;
|
|
1410
|
+
const operation = params?.["operation"];
|
|
1411
|
+
if (operation !== "send") continue;
|
|
1412
|
+
const to = params?.["to"] ?? params?.["toList"];
|
|
1413
|
+
const isEmpty = to === void 0 || to === null || typeof to === "string" && to.trim() === "" || Array.isArray(to) && to.length === 0;
|
|
1414
|
+
if (isEmpty) {
|
|
1415
|
+
this.warn(issues, 30, `Node "${node.name}" gmail send has no recipient (to) specified`, node.id);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
// Rule 31 (WARN): if node with empty conditions
|
|
1420
|
+
checkRule31(w, issues) {
|
|
1421
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1422
|
+
for (const node of w.nodes) {
|
|
1423
|
+
if (node.type !== "n8n-nodes-base.if") continue;
|
|
1424
|
+
const params = node.parameters;
|
|
1425
|
+
const conditions = params?.["conditions"];
|
|
1426
|
+
if (conditions === void 0 || conditions === null) {
|
|
1427
|
+
this.warn(issues, 31, `Node "${node.name}" if node has no conditions defined`, node.id);
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
if (typeof conditions === "object" && !Array.isArray(conditions)) {
|
|
1431
|
+
const conds = conditions["conditions"];
|
|
1432
|
+
if (!Array.isArray(conds) || conds.length === 0) {
|
|
1433
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1434
|
+
}
|
|
1435
|
+
} else if (Array.isArray(conditions) && conditions.length === 0) {
|
|
1436
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
// Rule 32 (WARN): set node with no assignments
|
|
1441
|
+
checkRule32(w, issues) {
|
|
1442
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1443
|
+
for (const node of w.nodes) {
|
|
1444
|
+
if (node.type !== "n8n-nodes-base.set") continue;
|
|
1445
|
+
const params = node.parameters;
|
|
1446
|
+
const assignmentsObj = params?.["assignments"];
|
|
1447
|
+
const assignmentsArr = assignmentsObj?.["assignments"];
|
|
1448
|
+
const valuesObj = params?.["values"];
|
|
1449
|
+
const hasV1 = valuesObj && Object.values(valuesObj).some((v) => Array.isArray(v) && v.length > 0);
|
|
1450
|
+
const hasV3 = Array.isArray(assignmentsArr) && assignmentsArr.length > 0;
|
|
1451
|
+
if (!hasV1 && !hasV3) {
|
|
1452
|
+
this.warn(
|
|
1453
|
+
issues,
|
|
1454
|
+
32,
|
|
1455
|
+
`Node "${node.name}" set node has no fields defined \u2014 it will pass data through unchanged`,
|
|
1456
|
+
node.id
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
// Rule 33 (WARN): scheduleTrigger with no schedule rules
|
|
1462
|
+
checkRule33(w, issues) {
|
|
1463
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1464
|
+
for (const node of w.nodes) {
|
|
1465
|
+
if (node.type !== "n8n-nodes-base.scheduleTrigger") continue;
|
|
1466
|
+
const params = node.parameters;
|
|
1467
|
+
const rule = params?.["rule"];
|
|
1468
|
+
const intervals = rule?.["interval"];
|
|
1469
|
+
if (!Array.isArray(intervals) || intervals.length === 0) {
|
|
1470
|
+
this.warn(issues, 33, `Node "${node.name}" scheduleTrigger has no schedule rules defined`, node.id);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
// Rule 34 (WARN): webhook path contains spaces, starts with slash, or looks like a full URL
|
|
1475
|
+
checkRule34(w, issues) {
|
|
1476
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1477
|
+
for (const node of w.nodes) {
|
|
1478
|
+
if (node.type !== "n8n-nodes-base.webhook") continue;
|
|
1479
|
+
const params = node.parameters;
|
|
1480
|
+
const path = params?.["path"];
|
|
1481
|
+
if (typeof path !== "string") continue;
|
|
1482
|
+
if (/\s/.test(path)) {
|
|
1483
|
+
this.warn(
|
|
1484
|
+
issues,
|
|
1485
|
+
34,
|
|
1486
|
+
`Node "${node.name}" webhook path contains spaces: "${path}" \u2014 use hyphens or underscores instead`,
|
|
1487
|
+
node.id
|
|
1488
|
+
);
|
|
1489
|
+
} else if (/^https?:\/\//i.test(path)) {
|
|
1490
|
+
this.warn(
|
|
1491
|
+
issues,
|
|
1492
|
+
34,
|
|
1493
|
+
`Node "${node.name}" webhook path looks like a full URL \u2014 it should be a relative path (e.g. "my-hook")`,
|
|
1494
|
+
node.id
|
|
1495
|
+
);
|
|
1496
|
+
} else if (path.startsWith("/")) {
|
|
1497
|
+
this.warn(
|
|
1498
|
+
issues,
|
|
1499
|
+
34,
|
|
1500
|
+
`Node "${node.name}" webhook path starts with "/" \u2014 n8n adds the leading slash automatically`,
|
|
1501
|
+
node.id
|
|
1161
1502
|
);
|
|
1162
1503
|
}
|
|
1163
1504
|
}
|
|
@@ -1212,7 +1553,26 @@ var ProviderError = class extends KairosError {
|
|
|
1212
1553
|
}
|
|
1213
1554
|
};
|
|
1214
1555
|
|
|
1556
|
+
// src/errors/guard-error.ts
|
|
1557
|
+
var GuardError = class extends KairosError {
|
|
1558
|
+
constructor(message) {
|
|
1559
|
+
super(message);
|
|
1560
|
+
this.name = "GuardError";
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1215
1564
|
// src/utils/retry.ts
|
|
1565
|
+
function isTransientNetworkError(err) {
|
|
1566
|
+
const TRANSIENT_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "ENOTFOUND", "ECONNABORTED"]);
|
|
1567
|
+
let current = err;
|
|
1568
|
+
for (let i = 0; i < 4; i++) {
|
|
1569
|
+
if (current === null || typeof current !== "object") break;
|
|
1570
|
+
const code = current.code;
|
|
1571
|
+
if (typeof code === "string" && TRANSIENT_CODES.has(code)) return true;
|
|
1572
|
+
current = current.cause;
|
|
1573
|
+
}
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1216
1576
|
async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
|
|
1217
1577
|
let lastError;
|
|
1218
1578
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
@@ -1237,6 +1597,7 @@ function fetchWithTimeout(url, init, timeoutMs) {
|
|
|
1237
1597
|
|
|
1238
1598
|
// src/providers/n8n/api-client.ts
|
|
1239
1599
|
var EXECUTION_LIMIT_CAP = 100;
|
|
1600
|
+
var N8N_API_PAGE_SIZE = 250;
|
|
1240
1601
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
1241
1602
|
var RETRY_ATTEMPTS = 3;
|
|
1242
1603
|
var RETRY_DELAY_MS = 1e3;
|
|
@@ -1245,6 +1606,17 @@ var N8nApiClient = class {
|
|
|
1245
1606
|
this.baseUrl = baseUrl;
|
|
1246
1607
|
this.apiKey = apiKey;
|
|
1247
1608
|
this.logger = logger;
|
|
1609
|
+
if (!baseUrl || typeof baseUrl !== "string") {
|
|
1610
|
+
throw new GuardError("N8nApiClient: baseUrl must be a non-empty string");
|
|
1611
|
+
}
|
|
1612
|
+
try {
|
|
1613
|
+
new URL(baseUrl);
|
|
1614
|
+
} catch {
|
|
1615
|
+
throw new GuardError(`N8nApiClient: baseUrl is not a valid URL: "${baseUrl}"`);
|
|
1616
|
+
}
|
|
1617
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
1618
|
+
throw new GuardError("N8nApiClient: apiKey must be a non-empty string");
|
|
1619
|
+
}
|
|
1248
1620
|
}
|
|
1249
1621
|
baseUrl;
|
|
1250
1622
|
apiKey;
|
|
@@ -1254,7 +1626,12 @@ var N8nApiClient = class {
|
|
|
1254
1626
|
this.logger.debug(`n8n ${method} ${path}`);
|
|
1255
1627
|
const isSafe = method === "GET";
|
|
1256
1628
|
if (!isSafe) {
|
|
1257
|
-
return
|
|
1629
|
+
return withRetry(
|
|
1630
|
+
() => this.singleRequest(url, method, path, body),
|
|
1631
|
+
2,
|
|
1632
|
+
RETRY_DELAY_MS,
|
|
1633
|
+
isTransientNetworkError
|
|
1634
|
+
);
|
|
1258
1635
|
}
|
|
1259
1636
|
return withRetry(
|
|
1260
1637
|
() => this.singleRequest(url, method, path, body),
|
|
@@ -1309,7 +1686,7 @@ var N8nApiClient = class {
|
|
|
1309
1686
|
}
|
|
1310
1687
|
async listWorkflows() {
|
|
1311
1688
|
const all = [];
|
|
1312
|
-
let path =
|
|
1689
|
+
let path = `/workflows?limit=${N8N_API_PAGE_SIZE}`;
|
|
1313
1690
|
for (; ; ) {
|
|
1314
1691
|
const response = await this.request("GET", path);
|
|
1315
1692
|
for (const w of response.data) {
|
|
@@ -1323,7 +1700,7 @@ var N8nApiClient = class {
|
|
|
1323
1700
|
});
|
|
1324
1701
|
}
|
|
1325
1702
|
if (!response.nextCursor) break;
|
|
1326
|
-
path = `/workflows?limit
|
|
1703
|
+
path = `/workflows?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
1327
1704
|
}
|
|
1328
1705
|
return all;
|
|
1329
1706
|
}
|
|
@@ -1353,14 +1730,14 @@ var N8nApiClient = class {
|
|
|
1353
1730
|
}
|
|
1354
1731
|
async listTags() {
|
|
1355
1732
|
const all = [];
|
|
1356
|
-
let path =
|
|
1733
|
+
let path = `/tags?limit=${N8N_API_PAGE_SIZE}`;
|
|
1357
1734
|
for (; ; ) {
|
|
1358
1735
|
const response = await this.request("GET", path);
|
|
1359
1736
|
for (const t of response.data) {
|
|
1360
1737
|
all.push({ id: t.id, name: t.name });
|
|
1361
1738
|
}
|
|
1362
1739
|
if (!response.nextCursor) break;
|
|
1363
|
-
path = `/tags?limit
|
|
1740
|
+
path = `/tags?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
1364
1741
|
}
|
|
1365
1742
|
return all;
|
|
1366
1743
|
}
|
|
@@ -1384,6 +1761,32 @@ var N8nApiClient = class {
|
|
|
1384
1761
|
return [];
|
|
1385
1762
|
}
|
|
1386
1763
|
}
|
|
1764
|
+
async triggerManual(workflowId) {
|
|
1765
|
+
const raw = await this.request("POST", `/workflows/${workflowId}/run`);
|
|
1766
|
+
const inner = raw["data"];
|
|
1767
|
+
const execId = inner?.["executionId"] ?? raw["executionId"];
|
|
1768
|
+
if (execId === void 0 || execId === null) {
|
|
1769
|
+
throw new ProviderError(
|
|
1770
|
+
`n8n trigger response missing executionId \u2014 got: ${JSON.stringify(raw)}`
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
return String(execId);
|
|
1774
|
+
}
|
|
1775
|
+
async triggerWebhookTest(path) {
|
|
1776
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
1777
|
+
const url = `${this.baseUrl.replace(/\/$/, "")}/webhook-test${cleanPath}`;
|
|
1778
|
+
this.logger.debug(`n8n POST webhook-test ${cleanPath}`);
|
|
1779
|
+
try {
|
|
1780
|
+
const response = await fetchWithTimeout(
|
|
1781
|
+
url,
|
|
1782
|
+
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) },
|
|
1783
|
+
REQUEST_TIMEOUT_MS
|
|
1784
|
+
);
|
|
1785
|
+
return response.status;
|
|
1786
|
+
} catch (err) {
|
|
1787
|
+
throw new ProviderError(`Webhook test request failed for path "${path}"`, err);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1387
1790
|
mapExecution(e) {
|
|
1388
1791
|
return {
|
|
1389
1792
|
id: e.id,
|
|
@@ -1600,6 +2003,14 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
|
|
|
1600
2003
|
8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
|
|
1601
2004
|
9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
|
|
1602
2005
|
10. No bare $('NodeName').json \u2014 always use .first().json.field or .all()
|
|
2006
|
+
11. httpRequest URL is a real endpoint (not "example.com" or "YOUR_URL")
|
|
2007
|
+
12. code nodes contain actual logic \u2014 not empty or comment-only
|
|
2008
|
+
13. Slack message nodes have a channel specified (channelId or channel)
|
|
2009
|
+
14. Gmail send nodes have a recipient (to field non-empty)
|
|
2010
|
+
15. if nodes have at least one condition in conditions.conditions[]
|
|
2011
|
+
16. set nodes have at least one entry in assignments.assignments[]
|
|
2012
|
+
17. scheduleTrigger has at least one rule in rule.interval[]
|
|
2013
|
+
18. webhook path is relative (no spaces, no leading slash, no http://)
|
|
1603
2014
|
|
|
1604
2015
|
---
|
|
1605
2016
|
|
|
@@ -1607,7 +2018,7 @@ Respond ONLY with a generate_workflow tool call. No prose. No markdown outside t
|
|
|
1607
2018
|
If the request is impossible or unclear, set the error field instead of generating a workflow.`;
|
|
1608
2019
|
|
|
1609
2020
|
// src/validation/rule-metadata.ts
|
|
1610
|
-
var VALIDATOR_RULE_IDS = Array.from({ length:
|
|
2021
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 34 }, (_, i) => i + 1);
|
|
1611
2022
|
var RULE_PIPELINE_STAGES = {
|
|
1612
2023
|
1: "node_generation",
|
|
1613
2024
|
2: "node_generation",
|
|
@@ -1634,7 +2045,15 @@ var RULE_PIPELINE_STAGES = {
|
|
|
1634
2045
|
23: "node_generation",
|
|
1635
2046
|
24: "expression_syntax",
|
|
1636
2047
|
25: "expression_syntax",
|
|
1637
|
-
26: "expression_syntax"
|
|
2048
|
+
26: "expression_syntax",
|
|
2049
|
+
27: "node_generation",
|
|
2050
|
+
28: "node_generation",
|
|
2051
|
+
29: "node_generation",
|
|
2052
|
+
30: "node_generation",
|
|
2053
|
+
31: "node_generation",
|
|
2054
|
+
32: "node_generation",
|
|
2055
|
+
33: "node_generation",
|
|
2056
|
+
34: "node_generation"
|
|
1638
2057
|
};
|
|
1639
2058
|
var RULE_EXAMPLES = {
|
|
1640
2059
|
17: {
|
|
@@ -1652,6 +2071,38 @@ var RULE_EXAMPLES = {
|
|
|
1652
2071
|
26: {
|
|
1653
2072
|
bad: "$('Fetch Data').json.email",
|
|
1654
2073
|
good: "$('Fetch Data').first().json.email"
|
|
2074
|
+
},
|
|
2075
|
+
27: {
|
|
2076
|
+
bad: '"url": "https://example.com/api/data"',
|
|
2077
|
+
good: '"url": "https://api.yourservice.com/v1/endpoint"'
|
|
2078
|
+
},
|
|
2079
|
+
28: {
|
|
2080
|
+
bad: '"jsCode": "// TODO: implement this"',
|
|
2081
|
+
good: '"jsCode": "return items.map(item => ({ json: { result: item.json.value * 2 } }))"'
|
|
2082
|
+
},
|
|
2083
|
+
29: {
|
|
2084
|
+
bad: '"channelId": ""',
|
|
2085
|
+
good: '"channelId": { "__rl": true, "value": "C0123456789", "mode": "id" }'
|
|
2086
|
+
},
|
|
2087
|
+
30: {
|
|
2088
|
+
bad: '"operation": "send", "to": ""',
|
|
2089
|
+
good: '"operation": "send", "to": "recipient@example.com"'
|
|
2090
|
+
},
|
|
2091
|
+
31: {
|
|
2092
|
+
bad: '"conditions": { "combinator": "and", "conditions": [] }',
|
|
2093
|
+
good: '"conditions": { "combinator": "and", "conditions": [{ "leftValue": "={{ $json.status }}", "rightValue": "active", "operator": { "type": "string", "operation": "equals" } }] }'
|
|
2094
|
+
},
|
|
2095
|
+
32: {
|
|
2096
|
+
bad: '"assignments": { "assignments": [] }',
|
|
2097
|
+
good: '"assignments": { "assignments": [{ "id": "f1", "name": "status", "value": "processed", "type": "string" }] }'
|
|
2098
|
+
},
|
|
2099
|
+
33: {
|
|
2100
|
+
bad: '"rule": { "interval": [] }',
|
|
2101
|
+
good: '"rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] }'
|
|
2102
|
+
},
|
|
2103
|
+
34: {
|
|
2104
|
+
bad: '"path": "/my webhook"',
|
|
2105
|
+
good: '"path": "my-webhook"'
|
|
1655
2106
|
}
|
|
1656
2107
|
};
|
|
1657
2108
|
var RULE_MITIGATIONS = {
|
|
@@ -1680,7 +2131,15 @@ var RULE_MITIGATIONS = {
|
|
|
1680
2131
|
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
1681
2132
|
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
1682
2133
|
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
1683
|
-
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
|
|
2134
|
+
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime',
|
|
2135
|
+
27: 'Replace placeholder URLs with your actual API endpoint \u2014 do not use "example.com" or "YOUR_URL" patterns',
|
|
2136
|
+
28: "Add executable code to the code node \u2014 empty or comment-only code nodes do nothing at runtime",
|
|
2137
|
+
29: "Set the channel parameter for Slack message operations (channelId with __rl object, or channel as string)",
|
|
2138
|
+
30: "Set the to parameter for Gmail send operations with at least one recipient email address",
|
|
2139
|
+
31: "Add at least one condition to the if node \u2014 conditions.conditions array must be non-empty",
|
|
2140
|
+
32: "Add field assignments to the set node \u2014 assignments.assignments array must be non-empty for typeVersion 3.x",
|
|
2141
|
+
33: "Add at least one schedule rule to scheduleTrigger \u2014 rule.interval array must have at least one entry",
|
|
2142
|
+
34: 'Webhook path must be a relative path without spaces, leading slashes, or protocol prefixes (e.g. "my-hook")'
|
|
1684
2143
|
};
|
|
1685
2144
|
|
|
1686
2145
|
// src/generation/prompt-builder.ts
|
|
@@ -1712,18 +2171,37 @@ var PromptBuilder = class {
|
|
|
1712
2171
|
}
|
|
1713
2172
|
build(request, matches, globalFailureRates = [], dynamicCatalog) {
|
|
1714
2173
|
const mode = this.resolveMode(matches);
|
|
1715
|
-
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
|
|
2174
|
+
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog, request.description);
|
|
1716
2175
|
const userMessage = this.buildUserMessage(request, matches, mode);
|
|
1717
2176
|
return { system, userMessage, mode, matches };
|
|
1718
2177
|
}
|
|
1719
|
-
buildCorrectionMessage(request, matches, allIssues, attempt) {
|
|
2178
|
+
buildCorrectionMessage(request, matches, allIssues, attempt, failingRuleIds) {
|
|
1720
2179
|
const base = this.buildUserMessage(request, matches, this.resolveMode(matches));
|
|
2180
|
+
let examplesSection = "";
|
|
2181
|
+
if (failingRuleIds && failingRuleIds.length > 0) {
|
|
2182
|
+
const uniqueRules = [...new Set(failingRuleIds)];
|
|
2183
|
+
const exampleLines = [];
|
|
2184
|
+
for (const rule of uniqueRules) {
|
|
2185
|
+
const ex = RULE_EXAMPLES[rule];
|
|
2186
|
+
if (ex) {
|
|
2187
|
+
exampleLines.push(`Rule ${rule}:
|
|
2188
|
+
Bad: ${ex.bad}
|
|
2189
|
+
Good: ${ex.good}`);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
if (exampleLines.length > 0) {
|
|
2193
|
+
examplesSection = `
|
|
2194
|
+
|
|
2195
|
+
## Concrete Fix Examples
|
|
2196
|
+
${exampleLines.join("\n\n")}`;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
1721
2199
|
return `${base}
|
|
1722
2200
|
|
|
1723
2201
|
IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
|
|
1724
2202
|
${allIssues.join("\n")}
|
|
1725
2203
|
|
|
1726
|
-
Fix ALL of the above issues in your new response. Do not repeat any of these mistakes
|
|
2204
|
+
Fix ALL of the above issues in your new response. Do not repeat any of these mistakes.${examplesSection}`;
|
|
1727
2205
|
}
|
|
1728
2206
|
resolveMode(matches) {
|
|
1729
2207
|
if (matches.length === 0) return "scratch";
|
|
@@ -1731,7 +2209,7 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1731
2209
|
if (!top) return "scratch";
|
|
1732
2210
|
return scoreToMode(top.score);
|
|
1733
2211
|
}
|
|
1734
|
-
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog) {
|
|
2212
|
+
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog, description) {
|
|
1735
2213
|
let basePrompt = SYSTEM_PROMPT_V1;
|
|
1736
2214
|
if (dynamicCatalog) {
|
|
1737
2215
|
basePrompt = basePrompt.replace(
|
|
@@ -1791,7 +2269,7 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1791
2269
|
});
|
|
1792
2270
|
}
|
|
1793
2271
|
}
|
|
1794
|
-
const warnings = this.buildFailureWarnings(matches, globalFailureRates);
|
|
2272
|
+
const warnings = this.buildFailureWarnings(matches, globalFailureRates, description);
|
|
1795
2273
|
if (warnings) {
|
|
1796
2274
|
blocks.push({ type: "text", text: warnings });
|
|
1797
2275
|
}
|
|
@@ -1818,15 +2296,34 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1818
2296
|
const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
|
|
1819
2297
|
return patterns.map((p) => p.rule);
|
|
1820
2298
|
}
|
|
1821
|
-
getActivePatterns(maxCount = 10) {
|
|
2299
|
+
getActivePatterns(maxCount = 10, description) {
|
|
1822
2300
|
const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
|
|
1823
2301
|
const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1824
2302
|
const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1825
2303
|
const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2304
|
+
const ordered = [...regressed, ...confirmed, ...drafts];
|
|
2305
|
+
if (this.profile === "minimal" && description) {
|
|
2306
|
+
return this.rankByRelevance(ordered, description).slice(0, maxCount);
|
|
2307
|
+
}
|
|
2308
|
+
return ordered.slice(0, maxCount);
|
|
2309
|
+
}
|
|
2310
|
+
rankByRelevance(patterns, description) {
|
|
2311
|
+
const lower = description.toLowerCase();
|
|
2312
|
+
const STAGE_KEYWORDS = {
|
|
2313
|
+
credential_injection: ["credential", "auth", "api key", "token", "oauth", "smtp", "imap", "password", "secret"],
|
|
2314
|
+
connection_wiring: ["connect", "link", "wire", "chain", "merge", "branch", "join"],
|
|
2315
|
+
expression_syntax: ["expression", "variable", "json", "field", "data", "$json", "item"],
|
|
2316
|
+
workflow_structure: ["trigger", "webhook", "schedule", "structure", "workflow"],
|
|
2317
|
+
node_generation: ["node", "generate", "create", "build", "send", "fetch", "email", "slack", "http"]
|
|
2318
|
+
};
|
|
2319
|
+
return patterns.map((p) => {
|
|
2320
|
+
const keywords = STAGE_KEYWORDS[p.pipelineStage] ?? [];
|
|
2321
|
+
const relevanceBoost = keywords.some((kw) => lower.includes(kw)) ? 1 : 0;
|
|
2322
|
+
return { pattern: p, sort: relevanceBoost * 10 + p.compositeScore };
|
|
2323
|
+
}).sort((a, b) => b.sort - a.sort).map((x) => x.pattern);
|
|
2324
|
+
}
|
|
2325
|
+
buildFailureWarnings(matches, globalFailureRates, description) {
|
|
2326
|
+
const richPatterns = this.getActivePatterns(this.resolveMaxPatterns(), description);
|
|
1830
2327
|
this._lastActivePatterns = richPatterns;
|
|
1831
2328
|
if (richPatterns.length > 0) {
|
|
1832
2329
|
return this.buildStageGroupedWarnings(richPatterns, matches);
|
|
@@ -1993,19 +2490,20 @@ var TelemetryReader = class {
|
|
|
1993
2490
|
}
|
|
1994
2491
|
const events = await this.readRecentEvents(days);
|
|
1995
2492
|
const buildSessions = new Set(
|
|
1996
|
-
events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
|
|
2493
|
+
events.filter((e) => e.eventType === "build_complete").map((e) => e.runId ?? e.sessionId)
|
|
1997
2494
|
);
|
|
1998
2495
|
const MIN_BUILDS_FOR_RATES = 3;
|
|
1999
2496
|
if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
|
|
2000
2497
|
const ruleSessions = /* @__PURE__ */ new Map();
|
|
2001
2498
|
for (const event of events) {
|
|
2002
2499
|
if (event.eventType !== "generation_attempt") continue;
|
|
2003
|
-
|
|
2500
|
+
const eventKey = event.runId ?? event.sessionId;
|
|
2501
|
+
if (!buildSessions.has(eventKey)) continue;
|
|
2004
2502
|
const data = event.data;
|
|
2005
2503
|
if (data.validationPassed || !data.issues) continue;
|
|
2006
2504
|
for (const issue of data.issues) {
|
|
2007
2505
|
const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
|
|
2008
|
-
entry.sessions.add(
|
|
2506
|
+
entry.sessions.add(eventKey);
|
|
2009
2507
|
entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
|
|
2010
2508
|
ruleSessions.set(issue.rule, entry);
|
|
2011
2509
|
}
|
|
@@ -2047,22 +2545,24 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2047
2545
|
telemetryDir;
|
|
2048
2546
|
outputDir;
|
|
2049
2547
|
_cachedEvents = null;
|
|
2548
|
+
_cachedPreviousPatterns = null;
|
|
2050
2549
|
constructor(telemetryDir) {
|
|
2051
2550
|
const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
|
|
2052
2551
|
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
2053
2552
|
this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
|
|
2054
2553
|
}
|
|
2055
2554
|
async loadPreviousPatterns() {
|
|
2555
|
+
if (this._cachedPreviousPatterns !== null) return this._cachedPreviousPatterns;
|
|
2056
2556
|
try {
|
|
2057
2557
|
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
|
|
2058
2558
|
const prev = JSON.parse(raw);
|
|
2059
2559
|
const version = prev.schemaVersion ?? 0;
|
|
2060
2560
|
const patterns = prev.topFailureRules ?? [];
|
|
2061
|
-
|
|
2062
|
-
return this.migratePatterns(patterns, version);
|
|
2561
|
+
this._cachedPreviousPatterns = version === PATTERN_SCHEMA_VERSION ? patterns : this.migratePatterns(patterns, version);
|
|
2063
2562
|
} catch {
|
|
2064
|
-
|
|
2563
|
+
this._cachedPreviousPatterns = [];
|
|
2065
2564
|
}
|
|
2565
|
+
return this._cachedPreviousPatterns;
|
|
2066
2566
|
}
|
|
2067
2567
|
migratePatterns(patterns, fromVersion) {
|
|
2068
2568
|
let migrated = patterns;
|
|
@@ -2362,6 +2862,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2362
2862
|
const tmpPath = `${outputPath}.tmp`;
|
|
2363
2863
|
await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
2364
2864
|
await (0, import_promises3.rename)(tmpPath, outputPath);
|
|
2865
|
+
this._cachedPreviousPatterns = null;
|
|
2365
2866
|
const historySummary = {
|
|
2366
2867
|
timestamp: analysis.generatedAt,
|
|
2367
2868
|
totalBuilds: analysis.summary.totalBuilds,
|
|
@@ -2410,7 +2911,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2410
2911
|
})
|
|
2411
2912
|
));
|
|
2412
2913
|
return {
|
|
2413
|
-
sessionId: bc.sessionId,
|
|
2914
|
+
sessionId: bc.runId ?? bc.sessionId,
|
|
2414
2915
|
date: bc.fileDate,
|
|
2415
2916
|
description: data.description ?? "",
|
|
2416
2917
|
workflowType: data.workflowType ?? null,
|
|
@@ -2443,7 +2944,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2443
2944
|
alerts.push({
|
|
2444
2945
|
type: "stale_pattern",
|
|
2445
2946
|
rule: p.rule,
|
|
2446
|
-
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-
|
|
2947
|
+
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-34)`
|
|
2447
2948
|
});
|
|
2448
2949
|
}
|
|
2449
2950
|
}
|
|
@@ -2683,9 +3184,13 @@ var import_meta = {};
|
|
|
2683
3184
|
var __dirname = (0, import_node_path7.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
2684
3185
|
var pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path7.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
2685
3186
|
var library = new FileLibrary();
|
|
2686
|
-
var
|
|
3187
|
+
var _validator = new N8nValidator();
|
|
3188
|
+
function getValidator() {
|
|
3189
|
+
return _validator;
|
|
3190
|
+
}
|
|
2687
3191
|
var nodeSyncer = new NodeSyncer();
|
|
2688
3192
|
var lastSync = null;
|
|
3193
|
+
var AUTO_SYNC_TIMEOUT_MS = 5e3;
|
|
2689
3194
|
var stripper = new N8nFieldStripper();
|
|
2690
3195
|
var promptBuilder = new PromptBuilder(getMcpPatternsPath());
|
|
2691
3196
|
function getMcpTelemetry() {
|
|
@@ -2720,11 +3225,23 @@ function isAllowed(action) {
|
|
|
2720
3225
|
const key = `KAIROS_MCP_ALLOW_${action.toUpperCase()}`;
|
|
2721
3226
|
return process.env[key] === "true";
|
|
2722
3227
|
}
|
|
3228
|
+
function mcpText(text) {
|
|
3229
|
+
return { content: [{ type: "text", text }] };
|
|
3230
|
+
}
|
|
3231
|
+
function mcpError(text) {
|
|
3232
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
3233
|
+
}
|
|
3234
|
+
function checkMcpAuth(provided) {
|
|
3235
|
+
const expected = process.env["KAIROS_MCP_SECRET"];
|
|
3236
|
+
if (!expected) return null;
|
|
3237
|
+
if (provided === expected) return null;
|
|
3238
|
+
return mcpError(JSON.stringify({ error: "Unauthorized: missing or incorrect kairos_secret" }));
|
|
3239
|
+
}
|
|
2723
3240
|
function getApiClient() {
|
|
2724
3241
|
const baseUrl = process.env["N8N_BASE_URL"];
|
|
2725
3242
|
const apiKey = process.env["N8N_API_KEY"];
|
|
2726
3243
|
if (!baseUrl || !apiKey) {
|
|
2727
|
-
throw new
|
|
3244
|
+
throw new GuardError("N8N_BASE_URL and N8N_API_KEY environment variables are required for n8n operations");
|
|
2728
3245
|
}
|
|
2729
3246
|
return new N8nApiClient(baseUrl, apiKey, nullLogger);
|
|
2730
3247
|
}
|
|
@@ -2738,7 +3255,7 @@ async function autoSync() {
|
|
|
2738
3255
|
const nodeTypes = await client.getNodeTypes();
|
|
2739
3256
|
if (nodeTypes.length === 0) return null;
|
|
2740
3257
|
lastSync = nodeSyncer.sync(nodeTypes);
|
|
2741
|
-
|
|
3258
|
+
_validator = new N8nValidator(lastSync.registry);
|
|
2742
3259
|
return lastSync;
|
|
2743
3260
|
} catch {
|
|
2744
3261
|
return null;
|
|
@@ -2760,21 +3277,21 @@ server.tool(
|
|
|
2760
3277
|
const baseUrl = process.env["N8N_BASE_URL"];
|
|
2761
3278
|
const apiKey = process.env["N8N_API_KEY"];
|
|
2762
3279
|
if (!baseUrl || !apiKey) {
|
|
2763
|
-
return {
|
|
2764
|
-
content: [{
|
|
2765
|
-
type: "text",
|
|
2766
|
-
text: JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required. Kairos needs to sync your n8n instance's node types to generate accurate workflows." })
|
|
2767
|
-
}],
|
|
2768
|
-
isError: true
|
|
2769
|
-
};
|
|
3280
|
+
return mcpError(JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required. Kairos needs to sync your n8n instance's node types to generate accurate workflows." }));
|
|
2770
3281
|
}
|
|
2771
3282
|
const runId = generateUUID();
|
|
2772
3283
|
const workflowType = inferWorkflowType(description);
|
|
3284
|
+
const syncPromise = autoSync();
|
|
3285
|
+
const syncTimeout = new Promise((resolve) => setTimeout(() => resolve(null), AUTO_SYNC_TIMEOUT_MS));
|
|
2773
3286
|
await library.initialize();
|
|
2774
|
-
const syncResult = await
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
3287
|
+
const [syncResult, matches, failureRates] = await Promise.all([
|
|
3288
|
+
Promise.race([syncPromise, syncTimeout]),
|
|
3289
|
+
library.search(description),
|
|
3290
|
+
(async () => {
|
|
3291
|
+
const reader = getTelemetryReader();
|
|
3292
|
+
return reader ? reader.getFailureRates() : [];
|
|
3293
|
+
})()
|
|
3294
|
+
]);
|
|
2778
3295
|
const request = { description, ...name ? { name } : {} };
|
|
2779
3296
|
const built = promptBuilder.build(request, matches, failureRates, syncResult?.catalogText);
|
|
2780
3297
|
if (mcpTelemetry) {
|
|
@@ -2788,43 +3305,38 @@ server.tool(
|
|
|
2788
3305
|
await mcpTelemetry.emit("build_start", { description, model: "mcp-decomposed", dryRun: false }, runId);
|
|
2789
3306
|
}
|
|
2790
3307
|
const systemText = built.system.map((block) => block.text).join("\n\n---\n\n");
|
|
2791
|
-
return {
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
}
|
|
2819
|
-
}
|
|
2820
|
-
}, null, 2)
|
|
2821
|
-
}]
|
|
2822
|
-
};
|
|
3308
|
+
return mcpText(JSON.stringify({
|
|
3309
|
+
kairos_run_id: runId,
|
|
3310
|
+
mode: built.mode,
|
|
3311
|
+
matchCount: matches.length,
|
|
3312
|
+
topMatchScore: matches[0]?.score ?? null,
|
|
3313
|
+
nodeCatalog: syncResult ? "synced" : "static",
|
|
3314
|
+
nodeCount: syncResult?.nodeCount ?? null,
|
|
3315
|
+
...syncResult ? {} : { syncWarning: "Could not sync node types from your n8n instance. Using static fallback catalog \u2014 generated workflows may not match your exact n8n setup." },
|
|
3316
|
+
systemPrompt: systemText,
|
|
3317
|
+
userMessage: built.userMessage,
|
|
3318
|
+
outputFormat: {
|
|
3319
|
+
description: "Generate a JSON object with this exact structure. The workflow field contains the n8n workflow. credentialsNeeded lists services requiring credentials.",
|
|
3320
|
+
schema: {
|
|
3321
|
+
workflow: {
|
|
3322
|
+
name: "string \u2014 descriptive workflow name",
|
|
3323
|
+
nodes: "array \u2014 n8n node objects with id (UUID v4), type, typeVersion, name, position, parameters",
|
|
3324
|
+
connections: "object \u2014 keyed by source node NAME, maps to target nodes",
|
|
3325
|
+
settings: 'object \u2014 include executionOrder: "v1"'
|
|
3326
|
+
},
|
|
3327
|
+
credentialsNeeded: [{
|
|
3328
|
+
service: 'string \u2014 e.g. "Slack"',
|
|
3329
|
+
credentialType: 'string \u2014 e.g. "slackOAuth2Api"',
|
|
3330
|
+
description: "string \u2014 what the user needs to set up"
|
|
3331
|
+
}]
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
}, null, 2));
|
|
2823
3335
|
}
|
|
2824
3336
|
);
|
|
2825
3337
|
server.tool(
|
|
2826
3338
|
"kairos_validate",
|
|
2827
|
-
"Validate n8n workflow JSON against
|
|
3339
|
+
"Validate n8n workflow JSON against 34 structural rules. Returns pass/fail with specific issues. If validation fails, fix the issues and call this again. Errors block deployment; warnings are advisory.",
|
|
2828
3340
|
{
|
|
2829
3341
|
workflow: import_zod.z.string().describe("The workflow JSON string to validate"),
|
|
2830
3342
|
kairos_run_id: import_zod.z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation")
|
|
@@ -2834,17 +3346,9 @@ server.tool(
|
|
|
2834
3346
|
try {
|
|
2835
3347
|
parsed = JSON.parse(workflowStr);
|
|
2836
3348
|
} catch (e) {
|
|
2837
|
-
return {
|
|
2838
|
-
content: [{
|
|
2839
|
-
type: "text",
|
|
2840
|
-
text: JSON.stringify({
|
|
2841
|
-
valid: false,
|
|
2842
|
-
error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`
|
|
2843
|
-
}, null, 2)
|
|
2844
|
-
}]
|
|
2845
|
-
};
|
|
3349
|
+
return mcpText(JSON.stringify({ valid: false, error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }, null, 2));
|
|
2846
3350
|
}
|
|
2847
|
-
const result =
|
|
3351
|
+
const result = getValidator().validate(parsed);
|
|
2848
3352
|
const errors = result.issues.filter((i) => i.severity === "error");
|
|
2849
3353
|
const warnings = result.issues.filter((i) => i.severity === "warn");
|
|
2850
3354
|
if (mcpTelemetry && kairos_run_id) {
|
|
@@ -2865,27 +3369,14 @@ server.tool(
|
|
|
2865
3369
|
}, kairos_run_id);
|
|
2866
3370
|
}
|
|
2867
3371
|
}
|
|
2868
|
-
return {
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
rule: i.rule,
|
|
2877
|
-
message: i.message,
|
|
2878
|
-
nodeId: i.nodeId ?? null
|
|
2879
|
-
})),
|
|
2880
|
-
warnings: warnings.map((i) => ({
|
|
2881
|
-
rule: i.rule,
|
|
2882
|
-
message: i.message,
|
|
2883
|
-
nodeId: i.nodeId ?? null
|
|
2884
|
-
})),
|
|
2885
|
-
deployable: errors.length === 0
|
|
2886
|
-
}, null, 2)
|
|
2887
|
-
}]
|
|
2888
|
-
};
|
|
3372
|
+
return mcpText(JSON.stringify({
|
|
3373
|
+
valid: result.valid,
|
|
3374
|
+
errorCount: errors.length,
|
|
3375
|
+
warningCount: warnings.length,
|
|
3376
|
+
errors: errors.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null })),
|
|
3377
|
+
warnings: warnings.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null })),
|
|
3378
|
+
deployable: errors.length === 0
|
|
3379
|
+
}, null, 2));
|
|
2889
3380
|
}
|
|
2890
3381
|
);
|
|
2891
3382
|
server.tool(
|
|
@@ -2894,101 +3385,146 @@ server.tool(
|
|
|
2894
3385
|
{
|
|
2895
3386
|
workflow: import_zod.z.string().describe("The validated workflow JSON string to deploy"),
|
|
2896
3387
|
activate: import_zod.z.boolean().default(false).describe("Activate the workflow immediately after deployment"),
|
|
2897
|
-
kairos_run_id: import_zod.z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation")
|
|
3388
|
+
kairos_run_id: import_zod.z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation"),
|
|
3389
|
+
kairos_secret: import_zod.z.string().optional().describe("Required when KAIROS_MCP_SECRET env var is set")
|
|
2898
3390
|
},
|
|
2899
|
-
async ({ workflow: workflowStr, activate, kairos_run_id }) => {
|
|
3391
|
+
async ({ workflow: workflowStr, activate, kairos_run_id, kairos_secret }) => {
|
|
3392
|
+
const authError = checkMcpAuth(kairos_secret);
|
|
3393
|
+
if (authError) return authError;
|
|
2900
3394
|
if (!isAllowed("deploy")) {
|
|
2901
|
-
return {
|
|
2902
|
-
content: [{
|
|
2903
|
-
type: "text",
|
|
2904
|
-
text: JSON.stringify({ error: "Deploy is disabled. Set KAIROS_MCP_ALLOW_DEPLOY=true to enable." })
|
|
2905
|
-
}],
|
|
2906
|
-
isError: true
|
|
2907
|
-
};
|
|
3395
|
+
return mcpError(JSON.stringify({ error: "Deploy is disabled. Set KAIROS_MCP_ALLOW_DEPLOY=true to enable." }));
|
|
2908
3396
|
}
|
|
2909
3397
|
let parsed;
|
|
2910
3398
|
try {
|
|
2911
3399
|
parsed = JSON.parse(workflowStr);
|
|
2912
3400
|
} catch (e) {
|
|
2913
|
-
return {
|
|
2914
|
-
content: [{
|
|
2915
|
-
type: "text",
|
|
2916
|
-
text: JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` })
|
|
2917
|
-
}]
|
|
2918
|
-
};
|
|
3401
|
+
return mcpError(JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }));
|
|
2919
3402
|
}
|
|
2920
|
-
const validation =
|
|
3403
|
+
const validation = getValidator().validate(parsed);
|
|
2921
3404
|
const errors = validation.issues.filter((i) => i.severity === "error");
|
|
2922
3405
|
if (errors.length > 0) {
|
|
2923
|
-
return {
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
error: "Workflow has validation errors \u2014 fix them before deploying",
|
|
2928
|
-
errors: errors.map((i) => ({ rule: i.rule, message: i.message }))
|
|
2929
|
-
}, null, 2)
|
|
2930
|
-
}]
|
|
2931
|
-
};
|
|
3406
|
+
return mcpError(JSON.stringify({
|
|
3407
|
+
error: "Workflow has validation errors \u2014 fix them before deploying",
|
|
3408
|
+
errors: errors.map((i) => ({ rule: i.rule, message: i.message }))
|
|
3409
|
+
}, null, 2));
|
|
2932
3410
|
}
|
|
2933
3411
|
const client = getApiClient();
|
|
2934
3412
|
const stripped = stripper.stripForCreate(parsed);
|
|
2935
3413
|
const response = await client.createWorkflow(stripped);
|
|
2936
3414
|
if (activate) {
|
|
2937
3415
|
if (!isAllowed("activate")) {
|
|
2938
|
-
return {
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
warning: "Workflow deployed but activation is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable.",
|
|
2946
|
-
url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
|
|
2947
|
-
}, null, 2)
|
|
2948
|
-
}]
|
|
2949
|
-
};
|
|
3416
|
+
return mcpText(JSON.stringify({
|
|
3417
|
+
workflowId: response.id,
|
|
3418
|
+
name: response.name,
|
|
3419
|
+
activated: false,
|
|
3420
|
+
warning: "Workflow deployed but activation is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable.",
|
|
3421
|
+
url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
|
|
3422
|
+
}, null, 2));
|
|
2950
3423
|
}
|
|
2951
3424
|
await client.activateWorkflow(response.id);
|
|
2952
3425
|
}
|
|
3426
|
+
const session = kairos_run_id ? mcpSessions.get(kairos_run_id) : void 0;
|
|
3427
|
+
const missingSessionWarning = kairos_run_id && !session ? `
|
|
3428
|
+
|
|
3429
|
+
Note: kairos_run_id "${kairos_run_id}" was provided but no active session was found. This usually means kairos_deploy was called without a prior kairos_prompt call, or the session expired. Telemetry and pattern learning for this build were skipped.` : "";
|
|
2953
3430
|
await library.initialize();
|
|
2954
3431
|
await library.save(parsed, {
|
|
2955
|
-
description: parsed.name,
|
|
3432
|
+
description: session?.description ?? parsed.name,
|
|
2956
3433
|
generationMode: "scratch",
|
|
2957
|
-
generationAttempts: 1
|
|
3434
|
+
generationAttempts: session?.validateAttempts ?? 1,
|
|
3435
|
+
n8nWorkflowId: response.id
|
|
2958
3436
|
});
|
|
2959
|
-
if (mcpTelemetry && kairos_run_id) {
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
PatternAnalyzer.fromEnv().analyzeAndSave().catch(() => {
|
|
2978
|
-
});
|
|
2979
|
-
}
|
|
3437
|
+
if (mcpTelemetry && kairos_run_id && session) {
|
|
3438
|
+
await mcpTelemetry.emit("build_complete", {
|
|
3439
|
+
description: session.description,
|
|
3440
|
+
success: true,
|
|
3441
|
+
totalAttempts: session.validateAttempts,
|
|
3442
|
+
totalDurationMs: Date.now() - session.startTime,
|
|
3443
|
+
totalTokensInput: 0,
|
|
3444
|
+
totalTokensOutput: 0,
|
|
3445
|
+
workflowName: response.name,
|
|
3446
|
+
workflowId: response.id,
|
|
3447
|
+
dryRun: false,
|
|
3448
|
+
credentialsNeeded: 0,
|
|
3449
|
+
warnedRules: session.warnedRules,
|
|
3450
|
+
workflowType: session.workflowType
|
|
3451
|
+
}, kairos_run_id);
|
|
3452
|
+
mcpSessions.delete(kairos_run_id);
|
|
3453
|
+
PatternAnalyzer.fromEnv().analyzeAndSave().catch(() => {
|
|
3454
|
+
});
|
|
2980
3455
|
}
|
|
2981
|
-
return {
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
3456
|
+
return mcpText(JSON.stringify({
|
|
3457
|
+
workflowId: response.id,
|
|
3458
|
+
name: response.name,
|
|
3459
|
+
activated: activate,
|
|
3460
|
+
url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
|
|
3461
|
+
}, null, 2) + missingSessionWarning);
|
|
3462
|
+
}
|
|
3463
|
+
);
|
|
3464
|
+
server.tool(
|
|
3465
|
+
"kairos_replace",
|
|
3466
|
+
"Replace an existing n8n workflow with a new version. Validates before updating. Use kairos_prompt \u2192 kairos_validate \u2192 kairos_replace for iteration on existing workflows.",
|
|
3467
|
+
{
|
|
3468
|
+
workflow_id: import_zod.z.string().describe("The n8n workflow ID to replace"),
|
|
3469
|
+
workflow: import_zod.z.string().describe("The validated workflow JSON string"),
|
|
3470
|
+
kairos_run_id: import_zod.z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation"),
|
|
3471
|
+
kairos_secret: import_zod.z.string().optional().describe("Required when KAIROS_MCP_SECRET env var is set")
|
|
3472
|
+
},
|
|
3473
|
+
async ({ workflow_id, workflow: workflowStr, kairos_run_id, kairos_secret }) => {
|
|
3474
|
+
const authError = checkMcpAuth(kairos_secret);
|
|
3475
|
+
if (authError) return authError;
|
|
3476
|
+
let parsed;
|
|
3477
|
+
try {
|
|
3478
|
+
parsed = JSON.parse(workflowStr);
|
|
3479
|
+
} catch (e) {
|
|
3480
|
+
return mcpError(JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }));
|
|
3481
|
+
}
|
|
3482
|
+
const validation = getValidator().validate(parsed);
|
|
3483
|
+
const errors = validation.issues.filter((i) => i.severity === "error");
|
|
3484
|
+
if (errors.length > 0) {
|
|
3485
|
+
return mcpError(JSON.stringify({
|
|
3486
|
+
error: "Workflow has validation errors \u2014 fix them before replacing",
|
|
3487
|
+
errors: errors.map((i) => ({ rule: i.rule, message: i.message }))
|
|
3488
|
+
}, null, 2));
|
|
3489
|
+
}
|
|
3490
|
+
const client = getApiClient();
|
|
3491
|
+
const stripped = stripper.stripForUpdate(parsed);
|
|
3492
|
+
const response = await client.updateWorkflow(workflow_id, stripped);
|
|
3493
|
+
const session = kairos_run_id ? mcpSessions.get(kairos_run_id) : void 0;
|
|
3494
|
+
const missingSessionWarning = kairos_run_id && !session ? `
|
|
3495
|
+
|
|
3496
|
+
Note: kairos_run_id "${kairos_run_id}" was provided but no active session was found.` : "";
|
|
3497
|
+
await library.initialize();
|
|
3498
|
+
await library.save(parsed, {
|
|
3499
|
+
description: session?.description ?? parsed.name,
|
|
3500
|
+
generationMode: "scratch",
|
|
3501
|
+
generationAttempts: session?.validateAttempts ?? 1,
|
|
3502
|
+
n8nWorkflowId: workflow_id
|
|
3503
|
+
});
|
|
3504
|
+
if (mcpTelemetry && kairos_run_id && session) {
|
|
3505
|
+
await mcpTelemetry.emit("build_complete", {
|
|
3506
|
+
description: session.description,
|
|
3507
|
+
success: true,
|
|
3508
|
+
totalAttempts: session.validateAttempts,
|
|
3509
|
+
totalDurationMs: Date.now() - session.startTime,
|
|
3510
|
+
totalTokensInput: 0,
|
|
3511
|
+
totalTokensOutput: 0,
|
|
3512
|
+
workflowName: response.name,
|
|
3513
|
+
workflowId: response.id,
|
|
3514
|
+
dryRun: false,
|
|
3515
|
+
credentialsNeeded: 0,
|
|
3516
|
+
warnedRules: session.warnedRules,
|
|
3517
|
+
workflowType: session.workflowType
|
|
3518
|
+
}, kairos_run_id);
|
|
3519
|
+
mcpSessions.delete(kairos_run_id);
|
|
3520
|
+
PatternAnalyzer.fromEnv().analyzeAndSave().catch(() => {
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
return mcpText(JSON.stringify({
|
|
3524
|
+
workflowId: response.id,
|
|
3525
|
+
name: response.name,
|
|
3526
|
+
url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
|
|
3527
|
+
}, null, 2) + missingSessionWarning);
|
|
2992
3528
|
}
|
|
2993
3529
|
);
|
|
2994
3530
|
server.tool(
|
|
@@ -3001,23 +3537,20 @@ server.tool(
|
|
|
3001
3537
|
async ({ query, limit }) => {
|
|
3002
3538
|
await library.initialize();
|
|
3003
3539
|
const matches = await library.search(query);
|
|
3004
|
-
return
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
)
|
|
3019
|
-
}]
|
|
3020
|
-
};
|
|
3540
|
+
return mcpText(JSON.stringify(
|
|
3541
|
+
matches.slice(0, limit).map((m) => ({
|
|
3542
|
+
id: m.workflow.id,
|
|
3543
|
+
score: Number(m.score.toFixed(3)),
|
|
3544
|
+
mode: m.mode,
|
|
3545
|
+
description: m.workflow.description,
|
|
3546
|
+
nodeCount: m.workflow.workflow.nodes.length,
|
|
3547
|
+
nodes: m.workflow.workflow.nodes.map((n) => n.name),
|
|
3548
|
+
n8nWorkflowId: m.workflow.n8nWorkflowId ?? null,
|
|
3549
|
+
failurePatterns: m.workflow.failurePatterns ?? []
|
|
3550
|
+
})),
|
|
3551
|
+
null,
|
|
3552
|
+
2
|
|
3553
|
+
));
|
|
3021
3554
|
}
|
|
3022
3555
|
);
|
|
3023
3556
|
server.tool(
|
|
@@ -3028,36 +3561,19 @@ server.tool(
|
|
|
3028
3561
|
const baseUrl = process.env["N8N_BASE_URL"];
|
|
3029
3562
|
const apiKey = process.env["N8N_API_KEY"];
|
|
3030
3563
|
if (!baseUrl || !apiKey) {
|
|
3031
|
-
return {
|
|
3032
|
-
content: [{
|
|
3033
|
-
type: "text",
|
|
3034
|
-
text: JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required for sync." })
|
|
3035
|
-
}],
|
|
3036
|
-
isError: true
|
|
3037
|
-
};
|
|
3564
|
+
return mcpError(JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required for sync." }));
|
|
3038
3565
|
}
|
|
3039
3566
|
lastSync = null;
|
|
3040
3567
|
const result = await autoSync();
|
|
3041
3568
|
if (!result) {
|
|
3042
|
-
return {
|
|
3043
|
-
content: [{
|
|
3044
|
-
type: "text",
|
|
3045
|
-
text: JSON.stringify({ error: "Failed to fetch node types from n8n. Check your credentials and that your instance is running." })
|
|
3046
|
-
}],
|
|
3047
|
-
isError: true
|
|
3048
|
-
};
|
|
3569
|
+
return mcpError(JSON.stringify({ error: "Failed to fetch node types from n8n. Check your credentials and that your instance is running." }));
|
|
3049
3570
|
}
|
|
3050
|
-
return {
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
newNodes: result.newNodes,
|
|
3057
|
-
message: `Synced ${result.nodeCount} node types from your n8n instance (${result.newNodes} not in default catalog).`
|
|
3058
|
-
}, null, 2)
|
|
3059
|
-
}]
|
|
3060
|
-
};
|
|
3571
|
+
return mcpText(JSON.stringify({
|
|
3572
|
+
synced: true,
|
|
3573
|
+
nodeCount: result.nodeCount,
|
|
3574
|
+
newNodes: result.newNodes,
|
|
3575
|
+
message: `Synced ${result.nodeCount} node types from your n8n instance (${result.newNodes} not in default catalog).`
|
|
3576
|
+
}, null, 2));
|
|
3061
3577
|
}
|
|
3062
3578
|
);
|
|
3063
3579
|
server.tool(
|
|
@@ -3073,12 +3589,72 @@ server.tool(
|
|
|
3073
3589
|
if (limit !== void 0 && limit > 0) {
|
|
3074
3590
|
analysis.topFailureRules = analysis.topFailureRules.slice(0, limit);
|
|
3075
3591
|
}
|
|
3076
|
-
return
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3592
|
+
return mcpText(JSON.stringify(analysis, null, 2));
|
|
3593
|
+
}
|
|
3594
|
+
);
|
|
3595
|
+
server.tool(
|
|
3596
|
+
"kairos_library",
|
|
3597
|
+
"Browse the local Kairos workflow library. Returns saved workflow metadata. Use the optional query to search, or omit it to list all entries.",
|
|
3598
|
+
{
|
|
3599
|
+
query: import_zod.z.string().optional().describe("Optional search query \u2014 omit to list all entries"),
|
|
3600
|
+
limit: import_zod.z.number().default(20).describe("Maximum entries to return")
|
|
3601
|
+
},
|
|
3602
|
+
async ({ query, limit }) => {
|
|
3603
|
+
await library.initialize();
|
|
3604
|
+
if (query) {
|
|
3605
|
+
const matches = await library.search(query);
|
|
3606
|
+
return mcpText(JSON.stringify(
|
|
3607
|
+
matches.slice(0, limit).map((m) => ({
|
|
3608
|
+
id: m.workflow.id,
|
|
3609
|
+
description: m.workflow.description,
|
|
3610
|
+
score: Number(m.score.toFixed(3)),
|
|
3611
|
+
mode: m.mode,
|
|
3612
|
+
nodeCount: m.workflow.workflow.nodes.length,
|
|
3613
|
+
nodes: m.workflow.workflow.nodes.map((n) => n.name),
|
|
3614
|
+
deployCount: m.workflow.deployCount,
|
|
3615
|
+
n8nWorkflowId: m.workflow.n8nWorkflowId ?? null,
|
|
3616
|
+
createdAt: m.workflow.createdAt
|
|
3617
|
+
})),
|
|
3618
|
+
null,
|
|
3619
|
+
2
|
|
3620
|
+
));
|
|
3621
|
+
}
|
|
3622
|
+
const all = await library.list();
|
|
3623
|
+
return mcpText(JSON.stringify(
|
|
3624
|
+
all.slice(0, limit).map((w) => ({
|
|
3625
|
+
id: w.id,
|
|
3626
|
+
description: w.description,
|
|
3627
|
+
nodeCount: w.workflow.nodes.length,
|
|
3628
|
+
nodes: w.workflow.nodes.map((n) => n.name),
|
|
3629
|
+
deployCount: w.deployCount,
|
|
3630
|
+
n8nWorkflowId: w.n8nWorkflowId ?? null,
|
|
3631
|
+
timesRetrieved: w.timesRetrieved ?? 0,
|
|
3632
|
+
createdAt: w.createdAt
|
|
3633
|
+
})),
|
|
3634
|
+
null,
|
|
3635
|
+
2
|
|
3636
|
+
));
|
|
3637
|
+
}
|
|
3638
|
+
);
|
|
3639
|
+
server.tool(
|
|
3640
|
+
"kairos_outcome",
|
|
3641
|
+
"Record the outcome of a workflow build against a library entry. Trains the pattern learning system to know what works and what fails over time.",
|
|
3642
|
+
{
|
|
3643
|
+
library_id: import_zod.z.string().describe("The Kairos library entry ID (returned by kairos_deploy, kairos_replace, or kairos_library)"),
|
|
3644
|
+
attempts: import_zod.z.number().describe("Number of generation+validation attempts before success"),
|
|
3645
|
+
first_try_pass: import_zod.z.boolean().describe("Whether the first attempt passed validation"),
|
|
3646
|
+
failed_rules: import_zod.z.array(import_zod.z.number()).describe("Validation rule IDs that failed during generation"),
|
|
3647
|
+
mode: import_zod.z.enum(["direct", "reference"]).describe("How the library entry was used during generation")
|
|
3648
|
+
},
|
|
3649
|
+
async ({ library_id, attempts, first_try_pass, failed_rules, mode }) => {
|
|
3650
|
+
await library.initialize();
|
|
3651
|
+
await library.recordOutcome(library_id, {
|
|
3652
|
+
attempts,
|
|
3653
|
+
firstTryPass: first_try_pass,
|
|
3654
|
+
failedRules: failed_rules,
|
|
3655
|
+
mode
|
|
3656
|
+
});
|
|
3657
|
+
return mcpText(JSON.stringify({ recorded: true, libraryId: library_id }));
|
|
3082
3658
|
}
|
|
3083
3659
|
);
|
|
3084
3660
|
server.tool(
|
|
@@ -3088,12 +3664,7 @@ server.tool(
|
|
|
3088
3664
|
async () => {
|
|
3089
3665
|
const client = getApiClient();
|
|
3090
3666
|
const workflows = await client.listWorkflows();
|
|
3091
|
-
return
|
|
3092
|
-
content: [{
|
|
3093
|
-
type: "text",
|
|
3094
|
-
text: JSON.stringify(workflows, null, 2)
|
|
3095
|
-
}]
|
|
3096
|
-
};
|
|
3667
|
+
return mcpText(JSON.stringify(workflows, null, 2));
|
|
3097
3668
|
}
|
|
3098
3669
|
);
|
|
3099
3670
|
server.tool(
|
|
@@ -3105,12 +3676,7 @@ server.tool(
|
|
|
3105
3676
|
async ({ workflow_id }) => {
|
|
3106
3677
|
const client = getApiClient();
|
|
3107
3678
|
const workflow = await client.getWorkflow(workflow_id);
|
|
3108
|
-
return
|
|
3109
|
-
content: [{
|
|
3110
|
-
type: "text",
|
|
3111
|
-
text: JSON.stringify(workflow, null, 2)
|
|
3112
|
-
}]
|
|
3113
|
-
};
|
|
3679
|
+
return mcpText(JSON.stringify(workflow, null, 2));
|
|
3114
3680
|
}
|
|
3115
3681
|
);
|
|
3116
3682
|
server.tool(
|
|
@@ -3121,22 +3687,11 @@ server.tool(
|
|
|
3121
3687
|
},
|
|
3122
3688
|
async ({ workflow_id }) => {
|
|
3123
3689
|
if (!isAllowed("activate")) {
|
|
3124
|
-
return {
|
|
3125
|
-
content: [{
|
|
3126
|
-
type: "text",
|
|
3127
|
-
text: JSON.stringify({ error: "Activate is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable." })
|
|
3128
|
-
}],
|
|
3129
|
-
isError: true
|
|
3130
|
-
};
|
|
3690
|
+
return mcpError(JSON.stringify({ error: "Activate is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable." }));
|
|
3131
3691
|
}
|
|
3132
3692
|
const client = getApiClient();
|
|
3133
3693
|
await client.activateWorkflow(workflow_id);
|
|
3134
|
-
return {
|
|
3135
|
-
content: [{
|
|
3136
|
-
type: "text",
|
|
3137
|
-
text: `Activated workflow ${workflow_id}`
|
|
3138
|
-
}]
|
|
3139
|
-
};
|
|
3694
|
+
return mcpText(`Activated workflow ${workflow_id}`);
|
|
3140
3695
|
}
|
|
3141
3696
|
);
|
|
3142
3697
|
server.tool(
|
|
@@ -3148,38 +3703,25 @@ server.tool(
|
|
|
3148
3703
|
async ({ workflow_id }) => {
|
|
3149
3704
|
const client = getApiClient();
|
|
3150
3705
|
await client.deactivateWorkflow(workflow_id);
|
|
3151
|
-
return {
|
|
3152
|
-
content: [{
|
|
3153
|
-
type: "text",
|
|
3154
|
-
text: `Deactivated workflow ${workflow_id}`
|
|
3155
|
-
}]
|
|
3156
|
-
};
|
|
3706
|
+
return mcpText(`Deactivated workflow ${workflow_id}`);
|
|
3157
3707
|
}
|
|
3158
3708
|
);
|
|
3159
3709
|
server.tool(
|
|
3160
3710
|
"kairos_delete",
|
|
3161
3711
|
"Delete a workflow from n8n. This is irreversible.",
|
|
3162
3712
|
{
|
|
3163
|
-
workflow_id: import_zod.z.string().describe("The n8n workflow ID to delete")
|
|
3713
|
+
workflow_id: import_zod.z.string().describe("The n8n workflow ID to delete"),
|
|
3714
|
+
kairos_secret: import_zod.z.string().optional().describe("Required when KAIROS_MCP_SECRET env var is set")
|
|
3164
3715
|
},
|
|
3165
|
-
async ({ workflow_id }) => {
|
|
3716
|
+
async ({ workflow_id, kairos_secret }) => {
|
|
3717
|
+
const authError = checkMcpAuth(kairos_secret);
|
|
3718
|
+
if (authError) return authError;
|
|
3166
3719
|
if (!isAllowed("delete")) {
|
|
3167
|
-
return {
|
|
3168
|
-
content: [{
|
|
3169
|
-
type: "text",
|
|
3170
|
-
text: JSON.stringify({ error: "Delete is disabled. Set KAIROS_MCP_ALLOW_DELETE=true to enable." })
|
|
3171
|
-
}],
|
|
3172
|
-
isError: true
|
|
3173
|
-
};
|
|
3720
|
+
return mcpError(JSON.stringify({ error: "Delete is disabled. Set KAIROS_MCP_ALLOW_DELETE=true to enable." }));
|
|
3174
3721
|
}
|
|
3175
3722
|
const client = getApiClient();
|
|
3176
3723
|
await client.deleteWorkflow(workflow_id);
|
|
3177
|
-
return {
|
|
3178
|
-
content: [{
|
|
3179
|
-
type: "text",
|
|
3180
|
-
text: `Deleted workflow ${workflow_id}`
|
|
3181
|
-
}]
|
|
3182
|
-
};
|
|
3724
|
+
return mcpText(`Deleted workflow ${workflow_id}`);
|
|
3183
3725
|
}
|
|
3184
3726
|
);
|
|
3185
3727
|
server.tool(
|
|
@@ -3192,15 +3734,15 @@ server.tool(
|
|
|
3192
3734
|
async ({ workflow_id, limit }) => {
|
|
3193
3735
|
const client = getApiClient();
|
|
3194
3736
|
const executions = await client.getExecutions(workflow_id, { limit });
|
|
3195
|
-
return
|
|
3196
|
-
content: [{
|
|
3197
|
-
type: "text",
|
|
3198
|
-
text: JSON.stringify(executions, null, 2)
|
|
3199
|
-
}]
|
|
3200
|
-
};
|
|
3737
|
+
return mcpText(JSON.stringify(executions, null, 2));
|
|
3201
3738
|
}
|
|
3202
3739
|
);
|
|
3203
3740
|
async function main() {
|
|
3741
|
+
if (!process.env["ANTHROPIC_API_KEY"]) {
|
|
3742
|
+
process.stderr.write(
|
|
3743
|
+
"[kairos-mcp] WARNING: ANTHROPIC_API_KEY is not set \u2014 kairos_prompt will fail. Set it before using workflow generation tools.\n"
|
|
3744
|
+
);
|
|
3745
|
+
}
|
|
3204
3746
|
const transport = new import_stdio.StdioServerTransport();
|
|
3205
3747
|
await server.connect(transport);
|
|
3206
3748
|
}
|