@kairos-sdk/core 0.4.0 → 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 +21 -10
- package/dist/chunk-5GAY7CSJ.js +411 -0
- package/dist/chunk-5GAY7CSJ.js.map +1 -0
- package/dist/chunk-6FOFWVMG.js +1 -0
- package/dist/chunk-6FOFWVMG.js.map +1 -0
- package/dist/chunk-EVOAYH2K.js +569 -0
- package/dist/chunk-EVOAYH2K.js.map +1 -0
- package/dist/{chunk-N6LRD2FN.js → chunk-HBGZTUUZ.js} +81 -380
- package/dist/chunk-HBGZTUUZ.js.map +1 -0
- package/dist/{chunk-NJ6QZBIC.js → chunk-KIFT5LA7.js} +971 -572
- package/dist/chunk-KIFT5LA7.js.map +1 -0
- package/dist/cli.cjs +1341 -236
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +83 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1259 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -540
- package/dist/index.d.ts +3 -540
- package/dist/index.js +9 -5
- package/dist/mcp-server.cjs +1473 -402
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +357 -232
- package/dist/mcp-server.js.map +1 -1
- package/dist/reader-B5mV20H6.d.cts +596 -0
- package/dist/reader-B5mV20H6.d.ts +596 -0
- package/dist/standalone.cjs +2978 -0
- package/dist/standalone.cjs.map +1 -0
- package/dist/standalone.d.cts +106 -0
- package/dist/standalone.d.ts +106 -0
- package/dist/standalone.js +58 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +9 -1
- package/dist/chunk-N6LRD2FN.js.map +0 -1
- package/dist/chunk-NJ6QZBIC.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,15 +298,33 @@ 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
|
+
}
|
|
306
|
+
function isValidMeta(item) {
|
|
307
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
308
|
+
}
|
|
309
|
+
function isValidOldEntry(item) {
|
|
310
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
|
|
311
|
+
item.workflow.nodes
|
|
312
|
+
);
|
|
313
|
+
}
|
|
256
314
|
var FileLibrary = class {
|
|
257
315
|
dir;
|
|
258
|
-
|
|
316
|
+
meta = [];
|
|
259
317
|
initPromise = null;
|
|
260
318
|
writeQueue = Promise.resolve();
|
|
261
319
|
constructor(dir) {
|
|
262
320
|
this.dir = dir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "library");
|
|
263
321
|
}
|
|
322
|
+
get workflowsDir() {
|
|
323
|
+
return (0, import_node_path.join)(this.dir, "workflows");
|
|
324
|
+
}
|
|
325
|
+
workflowFilePath(id) {
|
|
326
|
+
return (0, import_node_path.join)(this.workflowsDir, `${id}.json`);
|
|
327
|
+
}
|
|
264
328
|
async initialize() {
|
|
265
329
|
if (!this.initPromise) {
|
|
266
330
|
this.initPromise = this.doInitialize();
|
|
@@ -270,60 +334,196 @@ var FileLibrary = class {
|
|
|
270
334
|
async doInitialize() {
|
|
271
335
|
await (0, import_promises.mkdir)(this.dir, { recursive: true });
|
|
272
336
|
const indexPath = (0, import_node_path.join)(this.dir, "index.json");
|
|
337
|
+
let workflowsDirExists = false;
|
|
273
338
|
try {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
);
|
|
339
|
+
await (0, import_promises.stat)(this.workflowsDir);
|
|
340
|
+
workflowsDirExists = true;
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
if (workflowsDirExists) {
|
|
344
|
+
try {
|
|
345
|
+
const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
|
|
346
|
+
const parsed = JSON.parse(raw);
|
|
347
|
+
if (Array.isArray(parsed)) {
|
|
348
|
+
this.meta = parsed.filter(isValidMeta);
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
this.meta = [];
|
|
352
|
+
}
|
|
353
|
+
await this.scanForOrphansAndCleanup();
|
|
354
|
+
} else {
|
|
355
|
+
try {
|
|
356
|
+
const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
|
|
357
|
+
const parsed = JSON.parse(raw);
|
|
358
|
+
if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
|
|
359
|
+
await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
this.meta = [];
|
|
365
|
+
await (0, import_promises.mkdir)(this.workflowsDir, { recursive: true });
|
|
366
|
+
}
|
|
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;
|
|
282
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
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
395
|
+
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
396
|
+
* meta entry. Rewrites index.json in the new format.
|
|
397
|
+
*/
|
|
398
|
+
async migrateFromMonolithic(oldEntries) {
|
|
399
|
+
await (0, import_promises.mkdir)(this.workflowsDir, { recursive: true });
|
|
400
|
+
const newMeta = [];
|
|
401
|
+
for (const entry of oldEntries) {
|
|
402
|
+
const wfPath = this.workflowFilePath(entry.id);
|
|
403
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
404
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
|
|
405
|
+
await (0, import_promises.rename)(tmpPath, wfPath);
|
|
406
|
+
const { workflow, ...metaFields } = entry;
|
|
407
|
+
newMeta.push({
|
|
408
|
+
...metaFields,
|
|
409
|
+
workflowName: workflow.name,
|
|
410
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type)
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
this.meta = newMeta;
|
|
414
|
+
await this.persistNow();
|
|
415
|
+
}
|
|
416
|
+
async loadWorkflowFile(id) {
|
|
417
|
+
try {
|
|
418
|
+
const raw = await (0, import_promises.readFile)(this.workflowFilePath(id), "utf-8");
|
|
419
|
+
return JSON.parse(raw);
|
|
283
420
|
} catch {
|
|
284
|
-
|
|
421
|
+
return null;
|
|
285
422
|
}
|
|
286
423
|
}
|
|
424
|
+
async writeWorkflowFile(id, workflow) {
|
|
425
|
+
const wfPath = this.workflowFilePath(id);
|
|
426
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
427
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
|
|
428
|
+
await (0, import_promises.rename)(tmpPath, wfPath);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Build a lightweight StoredWorkflow shell from a meta entry for use in
|
|
432
|
+
* scoring / clustering. Only node.type is populated in each node — no other
|
|
433
|
+
* node fields are used by hybridScore or clusterWorkflows.
|
|
434
|
+
*/
|
|
435
|
+
makeSearchShell(m) {
|
|
436
|
+
return {
|
|
437
|
+
...m,
|
|
438
|
+
workflow: {
|
|
439
|
+
name: m.workflowName,
|
|
440
|
+
nodes: m.cachedNodeTypes.map((type) => ({
|
|
441
|
+
id: "",
|
|
442
|
+
name: "",
|
|
443
|
+
type,
|
|
444
|
+
typeVersion: 1,
|
|
445
|
+
position: [0, 0],
|
|
446
|
+
parameters: {}
|
|
447
|
+
})),
|
|
448
|
+
connections: {}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
287
452
|
async search(description, options) {
|
|
288
|
-
const
|
|
289
|
-
if (
|
|
453
|
+
const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
|
|
454
|
+
if (filteredMeta.length === 0) return [];
|
|
290
455
|
const limit = options?.limit ?? 3;
|
|
291
456
|
const queryTokens = tokenize(description);
|
|
292
457
|
if (queryTokens.length === 0) return [];
|
|
293
|
-
const
|
|
458
|
+
const shells = filteredMeta.map((m) => this.makeSearchShell(m));
|
|
459
|
+
const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
|
|
294
460
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
295
|
-
const docCount =
|
|
461
|
+
const docCount = shells.length;
|
|
296
462
|
const idf = /* @__PURE__ */ new Map();
|
|
463
|
+
const idfCeiling = Math.log(docCount + 1) + 1;
|
|
297
464
|
const allTokens = new Set(queryTokens);
|
|
298
465
|
for (const token of allTokens) {
|
|
299
466
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
300
|
-
|
|
467
|
+
const rawIdf = Math.log((docCount + 1) / (docsWithToken + 1)) + 1;
|
|
468
|
+
idf.set(token, rawIdf / idfCeiling);
|
|
301
469
|
}
|
|
302
|
-
const scored = hybridScore(queryTokens, description,
|
|
303
|
-
const clusters = clusterWorkflows(
|
|
470
|
+
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
471
|
+
const clusters = clusterWorkflows(shells);
|
|
304
472
|
const reranked = rerank(scored, clusters).slice(0, limit);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
473
|
+
if (reranked.length === 0) return [];
|
|
474
|
+
for (const r of reranked) {
|
|
475
|
+
const m = this.meta.find((m2) => m2.id === r.workflow.id);
|
|
476
|
+
if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
|
|
477
|
+
}
|
|
478
|
+
this.persist();
|
|
479
|
+
const results = await Promise.all(
|
|
480
|
+
reranked.map(async (r) => {
|
|
481
|
+
const m = this.meta.find((meta) => meta.id === r.workflow.id);
|
|
482
|
+
const workflow = await this.loadWorkflowFile(r.workflow.id);
|
|
483
|
+
if (!workflow) return null;
|
|
484
|
+
return {
|
|
485
|
+
workflow: { ...m, workflow },
|
|
486
|
+
score: r.score,
|
|
487
|
+
mode: scoreToMode(r.score)
|
|
488
|
+
};
|
|
489
|
+
})
|
|
490
|
+
);
|
|
491
|
+
return results.filter((r) => r !== null);
|
|
315
492
|
}
|
|
316
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
|
+
}
|
|
317
515
|
const id = generateUUID();
|
|
516
|
+
await this.writeWorkflowFile(id, workflow);
|
|
318
517
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
319
|
-
const
|
|
518
|
+
const meta = {
|
|
320
519
|
id,
|
|
321
|
-
workflow,
|
|
322
520
|
description: metadata.description,
|
|
323
521
|
tags: metadata.tags ?? [],
|
|
324
522
|
platform: metadata.platform ?? "n8n",
|
|
325
523
|
deployCount: 0,
|
|
326
524
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
525
|
+
workflowName: workflow.name,
|
|
526
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type),
|
|
327
527
|
...failurePatterns?.length ? { failurePatterns } : {},
|
|
328
528
|
...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
|
|
329
529
|
...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
|
|
@@ -333,33 +533,39 @@ var FileLibrary = class {
|
|
|
333
533
|
...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
|
|
334
534
|
...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
|
|
335
535
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
336
|
-
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
536
|
+
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {},
|
|
537
|
+
...metadata.n8nWorkflowId ? { n8nWorkflowId: metadata.n8nWorkflowId } : {}
|
|
337
538
|
};
|
|
338
|
-
this.
|
|
339
|
-
if (this.
|
|
340
|
-
this.
|
|
341
|
-
|
|
539
|
+
this.meta.push(meta);
|
|
540
|
+
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
541
|
+
this.meta.sort((a, b) => {
|
|
542
|
+
if (a.id === id) return -1;
|
|
543
|
+
if (b.id === id) return 1;
|
|
544
|
+
return evictionScore(b) - evictionScore(a);
|
|
545
|
+
});
|
|
546
|
+
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
342
547
|
}
|
|
343
548
|
await this.persist();
|
|
344
549
|
return id;
|
|
345
550
|
}
|
|
346
|
-
async recordDeployment(id) {
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
551
|
+
async recordDeployment(id, n8nWorkflowId) {
|
|
552
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
553
|
+
if (m) {
|
|
554
|
+
m.deployCount++;
|
|
555
|
+
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
556
|
+
if (n8nWorkflowId) m.n8nWorkflowId = n8nWorkflowId;
|
|
351
557
|
await this.persist();
|
|
352
558
|
}
|
|
353
559
|
}
|
|
354
560
|
async recordOutcome(id, outcome) {
|
|
355
|
-
const
|
|
356
|
-
if (!
|
|
561
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
562
|
+
if (!m) return;
|
|
357
563
|
if (outcome.mode === "direct") {
|
|
358
|
-
|
|
564
|
+
m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
|
|
359
565
|
} else {
|
|
360
|
-
|
|
566
|
+
m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
|
|
361
567
|
}
|
|
362
|
-
const stats =
|
|
568
|
+
const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
|
|
363
569
|
stats.totalUses++;
|
|
364
570
|
stats.totalAttempts += outcome.attempts;
|
|
365
571
|
if (outcome.firstTryPass) stats.firstTryPasses++;
|
|
@@ -367,24 +573,35 @@ var FileLibrary = class {
|
|
|
367
573
|
const key = String(rule);
|
|
368
574
|
stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
|
|
369
575
|
}
|
|
370
|
-
|
|
576
|
+
m.outcomeStats = stats;
|
|
371
577
|
await this.persist();
|
|
372
578
|
}
|
|
373
579
|
async drain() {
|
|
374
580
|
await this.writeQueue;
|
|
375
581
|
}
|
|
376
582
|
async get(id) {
|
|
377
|
-
|
|
583
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
584
|
+
if (!m) return null;
|
|
585
|
+
const workflow = await this.loadWorkflowFile(id);
|
|
586
|
+
if (!workflow) return null;
|
|
587
|
+
return { ...m, workflow };
|
|
378
588
|
}
|
|
379
589
|
async list(filters) {
|
|
380
|
-
let
|
|
590
|
+
let filtered = this.meta;
|
|
381
591
|
if (filters?.platform) {
|
|
382
|
-
|
|
592
|
+
filtered = filtered.filter((m) => m.platform === filters.platform);
|
|
383
593
|
}
|
|
384
594
|
if (filters?.tags && filters.tags.length > 0) {
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
595
|
+
filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
|
|
596
|
+
}
|
|
597
|
+
const results = await Promise.all(
|
|
598
|
+
filtered.map(async (m) => {
|
|
599
|
+
const workflow = await this.loadWorkflowFile(m.id);
|
|
600
|
+
if (!workflow) return null;
|
|
601
|
+
return { ...m, workflow };
|
|
602
|
+
})
|
|
603
|
+
);
|
|
604
|
+
return results.filter((r) => r !== null);
|
|
388
605
|
}
|
|
389
606
|
deduplicateFailurePatterns(patterns) {
|
|
390
607
|
if (!patterns?.length) return void 0;
|
|
@@ -399,12 +616,98 @@ var FileLibrary = class {
|
|
|
399
616
|
}
|
|
400
617
|
return [...map.values()];
|
|
401
618
|
}
|
|
402
|
-
|
|
403
|
-
|
|
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
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Direct write used only during migration (before writeQueue is needed).
|
|
672
|
+
*/
|
|
673
|
+
async persistNow() {
|
|
674
|
+
const releaseLock = await this.acquireLock();
|
|
675
|
+
try {
|
|
404
676
|
const indexPath = (0, import_node_path.join)(this.dir, "index.json");
|
|
405
677
|
const tmpPath = `${indexPath}.tmp`;
|
|
406
|
-
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(this.
|
|
678
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
407
679
|
await (0, import_promises.rename)(tmpPath, indexPath);
|
|
680
|
+
} finally {
|
|
681
|
+
await releaseLock();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
persist() {
|
|
685
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
686
|
+
const releaseLock = await this.acquireLock();
|
|
687
|
+
try {
|
|
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 {
|
|
697
|
+
}
|
|
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();
|
|
710
|
+
}
|
|
408
711
|
});
|
|
409
712
|
return this.writeQueue;
|
|
410
713
|
}
|
|
@@ -510,6 +813,14 @@ var NodeRegistry = class {
|
|
|
510
813
|
if (!def) return true;
|
|
511
814
|
return def.safeTypeVersions.includes(version);
|
|
512
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
|
+
}
|
|
513
824
|
getRequiredParams(type) {
|
|
514
825
|
return this.byType.get(type)?.requiredParams ?? [];
|
|
515
826
|
}
|
|
@@ -577,6 +888,17 @@ var N8nValidator = class {
|
|
|
577
888
|
this.checkRule21(workflow, issues);
|
|
578
889
|
this.checkRule22(workflow, issues);
|
|
579
890
|
this.checkRule23(workflow, issues);
|
|
891
|
+
this.checkRule24(workflow, issues);
|
|
892
|
+
this.checkRule25(workflow, issues);
|
|
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);
|
|
580
902
|
if (Array.isArray(workflow.nodes)) {
|
|
581
903
|
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
582
904
|
for (const issue of issues) {
|
|
@@ -709,10 +1031,14 @@ var N8nValidator = class {
|
|
|
709
1031
|
checkRule11(w, issues) {
|
|
710
1032
|
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
711
1033
|
const reachable = /* @__PURE__ */ new Set();
|
|
712
|
-
|
|
1034
|
+
const aiSubNodeSources = /* @__PURE__ */ new Set();
|
|
1035
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
713
1036
|
if (typeof outputs !== "object" || outputs === null) continue;
|
|
714
|
-
|
|
1037
|
+
let hasAiPort = false;
|
|
1038
|
+
for (const [portName, portGroup] of Object.entries(outputs)) {
|
|
715
1039
|
if (!Array.isArray(portGroup)) continue;
|
|
1040
|
+
const isAiPort = portName.startsWith("ai_");
|
|
1041
|
+
if (isAiPort) hasAiPort = true;
|
|
716
1042
|
for (const targets of portGroup) {
|
|
717
1043
|
if (!Array.isArray(targets)) continue;
|
|
718
1044
|
for (const target of targets) {
|
|
@@ -721,10 +1047,13 @@ var N8nValidator = class {
|
|
|
721
1047
|
}
|
|
722
1048
|
}
|
|
723
1049
|
}
|
|
1050
|
+
if (hasAiPort) aiSubNodeSources.add(sourceName);
|
|
724
1051
|
}
|
|
725
1052
|
for (const node of w.nodes) {
|
|
726
1053
|
if (node.type.includes("stickyNote")) continue;
|
|
727
|
-
if (
|
|
1054
|
+
if (this.isTriggerNode(node)) continue;
|
|
1055
|
+
if (aiSubNodeSources.has(node.name)) continue;
|
|
1056
|
+
if (!reachable.has(node.name)) {
|
|
728
1057
|
this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
|
|
729
1058
|
}
|
|
730
1059
|
}
|
|
@@ -821,19 +1150,22 @@ var N8nValidator = class {
|
|
|
821
1150
|
}
|
|
822
1151
|
}
|
|
823
1152
|
}
|
|
824
|
-
// 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.
|
|
825
1156
|
checkRule19(w, issues) {
|
|
826
1157
|
if (!Array.isArray(w.nodes)) return;
|
|
1158
|
+
const strict = process.env["KAIROS_REGISTRY_STRICT"] === "true";
|
|
827
1159
|
for (const node of w.nodes) {
|
|
828
1160
|
if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
|
|
829
|
-
if (
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
+
);
|
|
837
1169
|
}
|
|
838
1170
|
}
|
|
839
1171
|
// Rule 20 (WARN): cycle detection — no node should be reachable from itself
|
|
@@ -882,6 +1214,27 @@ var N8nValidator = class {
|
|
|
882
1214
|
}
|
|
883
1215
|
}
|
|
884
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
|
+
}
|
|
885
1238
|
// Rule 22 (WARN): check requiredParams from registry
|
|
886
1239
|
checkRule22(w, issues) {
|
|
887
1240
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -920,23 +1273,232 @@ var N8nValidator = class {
|
|
|
920
1273
|
}
|
|
921
1274
|
}
|
|
922
1275
|
}
|
|
923
|
-
// Rule
|
|
924
|
-
|
|
1276
|
+
// Rule 24 (WARN): deprecated accessor syntax in expressions
|
|
1277
|
+
checkRule24(w, issues) {
|
|
925
1278
|
if (!Array.isArray(w.nodes)) return;
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1279
|
+
const deprecated = /\$node\s*\[/;
|
|
1280
|
+
for (const node of w.nodes) {
|
|
1281
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1282
|
+
if (deprecated.test(expr)) {
|
|
1283
|
+
this.warn(
|
|
1284
|
+
issues,
|
|
1285
|
+
24,
|
|
1286
|
+
`Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
|
|
1287
|
+
node.id
|
|
1288
|
+
);
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
// Rule 25 (WARN): wrong item index assumptions in expressions
|
|
1295
|
+
checkRule25(w, issues) {
|
|
1296
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1297
|
+
const itemIndex = /\$json\s*\.\s*items\s*\[/;
|
|
1298
|
+
for (const node of w.nodes) {
|
|
1299
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1300
|
+
if (itemIndex.test(expr)) {
|
|
1301
|
+
this.warn(
|
|
1302
|
+
issues,
|
|
1303
|
+
25,
|
|
1304
|
+
`Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
|
|
1305
|
+
node.id
|
|
1306
|
+
);
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
// Rule 26 (WARN): missing .first() or .all() on node references
|
|
1313
|
+
checkRule26(w, issues) {
|
|
1314
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1315
|
+
const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
|
|
1316
|
+
for (const node of w.nodes) {
|
|
1317
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1318
|
+
if (bareRef.test(expr)) {
|
|
1319
|
+
this.warn(
|
|
1320
|
+
issues,
|
|
1321
|
+
26,
|
|
1322
|
+
`Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
|
|
1323
|
+
node.id
|
|
1324
|
+
);
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
extractExpressions(params) {
|
|
1331
|
+
const expressions = [];
|
|
1332
|
+
const walk = (val) => {
|
|
1333
|
+
if (typeof val === "string") {
|
|
1334
|
+
if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
|
|
1335
|
+
expressions.push(val);
|
|
1336
|
+
}
|
|
1337
|
+
} else if (Array.isArray(val)) {
|
|
1338
|
+
for (const item of val) walk(item);
|
|
1339
|
+
} else if (val !== null && typeof val === "object") {
|
|
1340
|
+
for (const v of Object.values(val)) walk(v);
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
walk(params);
|
|
1344
|
+
return expressions;
|
|
1345
|
+
}
|
|
1346
|
+
// Rule 27 (WARN): httpRequest URL is a placeholder
|
|
1347
|
+
checkRule27(w, issues) {
|
|
1348
|
+
if (!Array.isArray(w.nodes)) return;
|
|
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()))) {
|
|
935
1362
|
this.warn(
|
|
936
1363
|
issues,
|
|
937
|
-
|
|
938
|
-
`
|
|
939
|
-
|
|
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
|
|
940
1502
|
);
|
|
941
1503
|
}
|
|
942
1504
|
}
|
|
@@ -991,7 +1553,26 @@ var ProviderError = class extends KairosError {
|
|
|
991
1553
|
}
|
|
992
1554
|
};
|
|
993
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
|
+
|
|
994
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
|
+
}
|
|
995
1576
|
async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
|
|
996
1577
|
let lastError;
|
|
997
1578
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
@@ -1016,6 +1597,7 @@ function fetchWithTimeout(url, init, timeoutMs) {
|
|
|
1016
1597
|
|
|
1017
1598
|
// src/providers/n8n/api-client.ts
|
|
1018
1599
|
var EXECUTION_LIMIT_CAP = 100;
|
|
1600
|
+
var N8N_API_PAGE_SIZE = 250;
|
|
1019
1601
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
1020
1602
|
var RETRY_ATTEMPTS = 3;
|
|
1021
1603
|
var RETRY_DELAY_MS = 1e3;
|
|
@@ -1024,6 +1606,17 @@ var N8nApiClient = class {
|
|
|
1024
1606
|
this.baseUrl = baseUrl;
|
|
1025
1607
|
this.apiKey = apiKey;
|
|
1026
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
|
+
}
|
|
1027
1620
|
}
|
|
1028
1621
|
baseUrl;
|
|
1029
1622
|
apiKey;
|
|
@@ -1033,7 +1626,12 @@ var N8nApiClient = class {
|
|
|
1033
1626
|
this.logger.debug(`n8n ${method} ${path}`);
|
|
1034
1627
|
const isSafe = method === "GET";
|
|
1035
1628
|
if (!isSafe) {
|
|
1036
|
-
return
|
|
1629
|
+
return withRetry(
|
|
1630
|
+
() => this.singleRequest(url, method, path, body),
|
|
1631
|
+
2,
|
|
1632
|
+
RETRY_DELAY_MS,
|
|
1633
|
+
isTransientNetworkError
|
|
1634
|
+
);
|
|
1037
1635
|
}
|
|
1038
1636
|
return withRetry(
|
|
1039
1637
|
() => this.singleRequest(url, method, path, body),
|
|
@@ -1088,7 +1686,7 @@ var N8nApiClient = class {
|
|
|
1088
1686
|
}
|
|
1089
1687
|
async listWorkflows() {
|
|
1090
1688
|
const all = [];
|
|
1091
|
-
let path =
|
|
1689
|
+
let path = `/workflows?limit=${N8N_API_PAGE_SIZE}`;
|
|
1092
1690
|
for (; ; ) {
|
|
1093
1691
|
const response = await this.request("GET", path);
|
|
1094
1692
|
for (const w of response.data) {
|
|
@@ -1102,7 +1700,7 @@ var N8nApiClient = class {
|
|
|
1102
1700
|
});
|
|
1103
1701
|
}
|
|
1104
1702
|
if (!response.nextCursor) break;
|
|
1105
|
-
path = `/workflows?limit
|
|
1703
|
+
path = `/workflows?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
1106
1704
|
}
|
|
1107
1705
|
return all;
|
|
1108
1706
|
}
|
|
@@ -1132,14 +1730,14 @@ var N8nApiClient = class {
|
|
|
1132
1730
|
}
|
|
1133
1731
|
async listTags() {
|
|
1134
1732
|
const all = [];
|
|
1135
|
-
let path =
|
|
1733
|
+
let path = `/tags?limit=${N8N_API_PAGE_SIZE}`;
|
|
1136
1734
|
for (; ; ) {
|
|
1137
1735
|
const response = await this.request("GET", path);
|
|
1138
1736
|
for (const t of response.data) {
|
|
1139
1737
|
all.push({ id: t.id, name: t.name });
|
|
1140
1738
|
}
|
|
1141
1739
|
if (!response.nextCursor) break;
|
|
1142
|
-
path = `/tags?limit
|
|
1740
|
+
path = `/tags?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
1143
1741
|
}
|
|
1144
1742
|
return all;
|
|
1145
1743
|
}
|
|
@@ -1163,6 +1761,32 @@ var N8nApiClient = class {
|
|
|
1163
1761
|
return [];
|
|
1164
1762
|
}
|
|
1165
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
|
+
}
|
|
1166
1790
|
mapExecution(e) {
|
|
1167
1791
|
return {
|
|
1168
1792
|
id: e.id,
|
|
@@ -1209,9 +1833,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
|
|
|
1209
1833
|
- Never reuse IDs, never use sequential fake IDs like "node-1"
|
|
1210
1834
|
|
|
1211
1835
|
### Credentials:
|
|
1212
|
-
-
|
|
1213
|
-
|
|
1214
|
-
-
|
|
1836
|
+
- Each credential is keyed by its type string, with an object value containing id and name:
|
|
1837
|
+
"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack Credential" } }
|
|
1838
|
+
- Use "placeholder-id" as the id \u2014 users replace this with their real credential ID from n8n after deployment
|
|
1839
|
+
- The credentialsNeeded field in your response declares what credentials the user must configure
|
|
1840
|
+
- Never put API keys or tokens directly in node parameters when a credential type exists
|
|
1215
1841
|
|
|
1216
1842
|
### Node names:
|
|
1217
1843
|
- All node names must be unique within the workflow
|
|
@@ -1258,6 +1884,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
|
|
|
1258
1884
|
|
|
1259
1885
|
---
|
|
1260
1886
|
|
|
1887
|
+
## EXPRESSION SYNTAX \u2014 how to reference upstream node data
|
|
1888
|
+
|
|
1889
|
+
### Accessing a field from an upstream node:
|
|
1890
|
+
- CORRECT: $('NodeName').item.json.field
|
|
1891
|
+
- WRONG: $node["NodeName"].json.field \u2190 deprecated accessor, fails at runtime (Rule 24)
|
|
1892
|
+
|
|
1893
|
+
### Accessing array items from $json:
|
|
1894
|
+
- CORRECT: $json.field \u2190 n8n auto-flattens items; each item is already a flat object
|
|
1895
|
+
- WRONG: $json.items[0].field \u2190 do not index into items[] (Rule 25)
|
|
1896
|
+
|
|
1897
|
+
### Calling node data \u2014 always qualify with .first() or .all():
|
|
1898
|
+
- CORRECT: $('NodeName').first().json.field \u2190 single item
|
|
1899
|
+
- CORRECT: $('NodeName').all() \u2190 array of all items
|
|
1900
|
+
- WRONG: $('NodeName').json \u2190 throws at runtime without .first() or .all() (Rule 26)
|
|
1901
|
+
|
|
1902
|
+
---
|
|
1903
|
+
|
|
1261
1904
|
## NODE CATALOG \u2014 exact type strings and safe typeVersions
|
|
1262
1905
|
|
|
1263
1906
|
### Triggers (always at least one required):
|
|
@@ -1357,6 +2000,17 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
|
|
|
1357
2000
|
5. At least one trigger node present
|
|
1358
2001
|
6. Every AI Agent has an ai_languageModel sub-node
|
|
1359
2002
|
7. settings block is complete with executionOrder: "v1"
|
|
2003
|
+
8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
|
|
2004
|
+
9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
|
|
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://)
|
|
1360
2014
|
|
|
1361
2015
|
---
|
|
1362
2016
|
|
|
@@ -1364,7 +2018,7 @@ Respond ONLY with a generate_workflow tool call. No prose. No markdown outside t
|
|
|
1364
2018
|
If the request is impossible or unclear, set the error field instead of generating a workflow.`;
|
|
1365
2019
|
|
|
1366
2020
|
// src/validation/rule-metadata.ts
|
|
1367
|
-
var VALIDATOR_RULE_IDS = Array.from({ length:
|
|
2021
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 34 }, (_, i) => i + 1);
|
|
1368
2022
|
var RULE_PIPELINE_STAGES = {
|
|
1369
2023
|
1: "node_generation",
|
|
1370
2024
|
2: "node_generation",
|
|
@@ -1388,7 +2042,68 @@ var RULE_PIPELINE_STAGES = {
|
|
|
1388
2042
|
20: "connection_wiring",
|
|
1389
2043
|
21: "workflow_structure",
|
|
1390
2044
|
22: "workflow_structure",
|
|
1391
|
-
23: "node_generation"
|
|
2045
|
+
23: "node_generation",
|
|
2046
|
+
24: "expression_syntax",
|
|
2047
|
+
25: "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"
|
|
2057
|
+
};
|
|
2058
|
+
var RULE_EXAMPLES = {
|
|
2059
|
+
17: {
|
|
2060
|
+
bad: '"credentials": { "slackOAuth2Api": "my-token" }',
|
|
2061
|
+
good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
|
|
2062
|
+
},
|
|
2063
|
+
24: {
|
|
2064
|
+
bad: '$node["Fetch Data"].json.email',
|
|
2065
|
+
good: "$('Fetch Data').item.json.email"
|
|
2066
|
+
},
|
|
2067
|
+
25: {
|
|
2068
|
+
bad: "$json.items[0].email",
|
|
2069
|
+
good: "$json.email"
|
|
2070
|
+
},
|
|
2071
|
+
26: {
|
|
2072
|
+
bad: "$('Fetch Data').json.email",
|
|
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"'
|
|
2106
|
+
}
|
|
1392
2107
|
};
|
|
1393
2108
|
var RULE_MITIGATIONS = {
|
|
1394
2109
|
1: "Provide a non-empty workflow name string",
|
|
@@ -1407,36 +2122,86 @@ var RULE_MITIGATIONS = {
|
|
|
1407
2122
|
14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
|
|
1408
2123
|
15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
|
|
1409
2124
|
16: "All node names must be unique within the workflow",
|
|
1410
|
-
17: '
|
|
2125
|
+
17: 'Each credential entry must be keyed by credential type with an object value: { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Credential" } } \u2014 the key is the credential type, the value has id and name strings',
|
|
1411
2126
|
18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
|
|
1412
2127
|
19: "Use known safe typeVersion values for each node type",
|
|
1413
2128
|
20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
|
|
1414
2129
|
21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
|
|
1415
2130
|
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
|
|
1416
|
-
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync"
|
|
2131
|
+
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
2132
|
+
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
2133
|
+
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
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")'
|
|
1417
2143
|
};
|
|
1418
2144
|
|
|
1419
2145
|
// src/generation/prompt-builder.ts
|
|
1420
2146
|
var CRITICAL_SCORE_THRESHOLD = 0.15;
|
|
2147
|
+
function resolveProfile() {
|
|
2148
|
+
const env = process.env["KAIROS_PROMPT_PROFILE"];
|
|
2149
|
+
if (env === "minimal" || env === "standard" || env === "rich") return env;
|
|
2150
|
+
return "standard";
|
|
2151
|
+
}
|
|
2152
|
+
var PROACTIVE_EXPRESSION_GUIDANCE = `## Expression Syntax Quick Reference
|
|
2153
|
+
|
|
2154
|
+
Always use these patterns in expressions:
|
|
2155
|
+
- Access node data: $('NodeName').item.json.field (not $node["NodeName"].json)
|
|
2156
|
+
- Access JSON field: $json.field (not $json.items[0].field)
|
|
2157
|
+
- Single item: $('NodeName').first().json.field
|
|
2158
|
+
- All items: $('NodeName').all()`;
|
|
1421
2159
|
var PromptBuilder = class {
|
|
1422
2160
|
patternsPath;
|
|
1423
|
-
|
|
2161
|
+
profile;
|
|
2162
|
+
_lastActivePatterns = null;
|
|
2163
|
+
constructor(patternsPath, profile) {
|
|
1424
2164
|
this.patternsPath = patternsPath ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "patterns.json");
|
|
2165
|
+
this.profile = profile ?? resolveProfile();
|
|
2166
|
+
}
|
|
2167
|
+
resolveMaxPatterns() {
|
|
2168
|
+
if (this.profile === "minimal") return 3;
|
|
2169
|
+
if (this.profile === "rich") return 15;
|
|
2170
|
+
return 10;
|
|
1425
2171
|
}
|
|
1426
2172
|
build(request, matches, globalFailureRates = [], dynamicCatalog) {
|
|
1427
2173
|
const mode = this.resolveMode(matches);
|
|
1428
|
-
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
|
|
2174
|
+
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog, request.description);
|
|
1429
2175
|
const userMessage = this.buildUserMessage(request, matches, mode);
|
|
1430
2176
|
return { system, userMessage, mode, matches };
|
|
1431
2177
|
}
|
|
1432
|
-
buildCorrectionMessage(request, matches, allIssues, attempt) {
|
|
2178
|
+
buildCorrectionMessage(request, matches, allIssues, attempt, failingRuleIds) {
|
|
1433
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
|
+
}
|
|
1434
2199
|
return `${base}
|
|
1435
2200
|
|
|
1436
2201
|
IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
|
|
1437
2202
|
${allIssues.join("\n")}
|
|
1438
2203
|
|
|
1439
|
-
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}`;
|
|
1440
2205
|
}
|
|
1441
2206
|
resolveMode(matches) {
|
|
1442
2207
|
if (matches.length === 0) return "scratch";
|
|
@@ -1444,7 +2209,7 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1444
2209
|
if (!top) return "scratch";
|
|
1445
2210
|
return scoreToMode(top.score);
|
|
1446
2211
|
}
|
|
1447
|
-
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog) {
|
|
2212
|
+
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog, description) {
|
|
1448
2213
|
let basePrompt = SYSTEM_PROMPT_V1;
|
|
1449
2214
|
if (dynamicCatalog) {
|
|
1450
2215
|
basePrompt = basePrompt.replace(
|
|
@@ -1459,53 +2224,62 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1459
2224
|
cache_control: { type: "ephemeral" }
|
|
1460
2225
|
}
|
|
1461
2226
|
];
|
|
1462
|
-
if (
|
|
1463
|
-
|
|
1464
|
-
const
|
|
1465
|
-
|
|
2227
|
+
if (this.profile !== "minimal") {
|
|
2228
|
+
if (mode === "reference" && matches.length > 0) {
|
|
2229
|
+
const refText = matches.slice(0, 3).map((m) => {
|
|
2230
|
+
const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
2231
|
+
return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
|
|
1466
2232
|
Nodes:
|
|
1467
2233
|
${nodes}`;
|
|
1468
|
-
|
|
1469
|
-
blocks.push({
|
|
1470
|
-
type: "text",
|
|
1471
|
-
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1472
|
-
|
|
1473
|
-
${refText}`
|
|
1474
|
-
});
|
|
1475
|
-
}
|
|
1476
|
-
if (mode === "direct" && matches[0]) {
|
|
1477
|
-
const match = matches[0];
|
|
1478
|
-
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1479
|
-
if (json.length > 3e4) {
|
|
1480
|
-
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
2234
|
+
}).join("\n\n");
|
|
1481
2235
|
blocks.push({
|
|
1482
2236
|
type: "text",
|
|
1483
|
-
text: `##
|
|
2237
|
+
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
2238
|
+
|
|
2239
|
+
${refText}`
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
if (mode === "direct" && matches[0]) {
|
|
2243
|
+
const match = matches[0];
|
|
2244
|
+
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
2245
|
+
if (json.length > 3e4) {
|
|
2246
|
+
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
2247
|
+
blocks.push({
|
|
2248
|
+
type: "text",
|
|
2249
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
|
|
1484
2250
|
Nodes:
|
|
1485
2251
|
${nodes}`
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
2252
|
+
});
|
|
2253
|
+
} else {
|
|
2254
|
+
blocks.push({
|
|
2255
|
+
type: "text",
|
|
2256
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
|
|
1491
2257
|
|
|
1492
2258
|
${json}`
|
|
1493
|
-
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
1494
2261
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
text: `## Weak Structural Hint
|
|
2262
|
+
if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
|
|
2263
|
+
const hint = matches[0];
|
|
2264
|
+
const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
|
|
2265
|
+
blocks.push({
|
|
2266
|
+
type: "text",
|
|
2267
|
+
text: `## Weak Structural Hint
|
|
1502
2268
|
A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
|
|
1503
|
-
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
1504
2271
|
}
|
|
1505
|
-
const warnings = this.buildFailureWarnings(matches, globalFailureRates);
|
|
2272
|
+
const warnings = this.buildFailureWarnings(matches, globalFailureRates, description);
|
|
1506
2273
|
if (warnings) {
|
|
1507
2274
|
blocks.push({ type: "text", text: warnings });
|
|
1508
2275
|
}
|
|
2276
|
+
if (this.profile === "rich") {
|
|
2277
|
+
const expressionRules = /* @__PURE__ */ new Set([24, 25, 26]);
|
|
2278
|
+
const expressionAlreadyCovered = (this._lastActivePatterns ?? []).some((p) => expressionRules.has(p.rule));
|
|
2279
|
+
if (!expressionAlreadyCovered) {
|
|
2280
|
+
blocks.push({ type: "text", text: PROACTIVE_EXPRESSION_GUIDANCE });
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
1509
2283
|
return blocks;
|
|
1510
2284
|
}
|
|
1511
2285
|
loadPatterns() {
|
|
@@ -1519,18 +2293,38 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1519
2293
|
}
|
|
1520
2294
|
}
|
|
1521
2295
|
getWarnedRules() {
|
|
1522
|
-
|
|
2296
|
+
const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
|
|
2297
|
+
return patterns.map((p) => p.rule);
|
|
1523
2298
|
}
|
|
1524
|
-
getActivePatterns() {
|
|
1525
|
-
const MAX_WARNED = 10;
|
|
2299
|
+
getActivePatterns(maxCount = 10, description) {
|
|
1526
2300
|
const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
|
|
1527
2301
|
const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1528
2302
|
const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1529
2303
|
const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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);
|
|
2327
|
+
this._lastActivePatterns = richPatterns;
|
|
1534
2328
|
if (richPatterns.length > 0) {
|
|
1535
2329
|
return this.buildStageGroupedWarnings(richPatterns, matches);
|
|
1536
2330
|
}
|
|
@@ -1541,7 +2335,8 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1541
2335
|
credential_injection: "CREDENTIAL FORMATTING",
|
|
1542
2336
|
connection_wiring: "CONNECTION WIRING",
|
|
1543
2337
|
node_generation: "NODE GENERATION",
|
|
1544
|
-
workflow_structure: "WORKFLOW STRUCTURE"
|
|
2338
|
+
workflow_structure: "WORKFLOW STRUCTURE",
|
|
2339
|
+
expression_syntax: "EXPRESSION SYNTAX"
|
|
1545
2340
|
};
|
|
1546
2341
|
const byStage = /* @__PURE__ */ new Map();
|
|
1547
2342
|
for (const p of patterns) {
|
|
@@ -1569,7 +2364,11 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1569
2364
|
const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
|
|
1570
2365
|
const remedyStr = remedy ? `
|
|
1571
2366
|
Fix: ${remedy}` : "";
|
|
1572
|
-
|
|
2367
|
+
const ex = RULE_EXAMPLES[p.rule];
|
|
2368
|
+
const exampleStr = ex ? `
|
|
2369
|
+
Bad: ${ex.bad}
|
|
2370
|
+
Good: ${ex.good}` : "";
|
|
2371
|
+
lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}${exampleStr}`);
|
|
1573
2372
|
} else {
|
|
1574
2373
|
const ruleNums = group.map((p) => p.rule).join(", ");
|
|
1575
2374
|
const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
|
|
@@ -1691,19 +2490,20 @@ var TelemetryReader = class {
|
|
|
1691
2490
|
}
|
|
1692
2491
|
const events = await this.readRecentEvents(days);
|
|
1693
2492
|
const buildSessions = new Set(
|
|
1694
|
-
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)
|
|
1695
2494
|
);
|
|
1696
2495
|
const MIN_BUILDS_FOR_RATES = 3;
|
|
1697
2496
|
if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
|
|
1698
2497
|
const ruleSessions = /* @__PURE__ */ new Map();
|
|
1699
2498
|
for (const event of events) {
|
|
1700
2499
|
if (event.eventType !== "generation_attempt") continue;
|
|
1701
|
-
|
|
2500
|
+
const eventKey = event.runId ?? event.sessionId;
|
|
2501
|
+
if (!buildSessions.has(eventKey)) continue;
|
|
1702
2502
|
const data = event.data;
|
|
1703
2503
|
if (data.validationPassed || !data.issues) continue;
|
|
1704
2504
|
for (const issue of data.issues) {
|
|
1705
2505
|
const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
|
|
1706
|
-
entry.sessions.add(
|
|
2506
|
+
entry.sessions.add(eventKey);
|
|
1707
2507
|
entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
|
|
1708
2508
|
ruleSessions.set(issue.rule, entry);
|
|
1709
2509
|
}
|
|
@@ -1744,22 +2544,25 @@ var PATTERN_SCHEMA_VERSION = 2;
|
|
|
1744
2544
|
var PatternAnalyzer = class _PatternAnalyzer {
|
|
1745
2545
|
telemetryDir;
|
|
1746
2546
|
outputDir;
|
|
2547
|
+
_cachedEvents = null;
|
|
2548
|
+
_cachedPreviousPatterns = null;
|
|
1747
2549
|
constructor(telemetryDir) {
|
|
1748
2550
|
const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
|
|
1749
2551
|
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
1750
2552
|
this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
|
|
1751
2553
|
}
|
|
1752
2554
|
async loadPreviousPatterns() {
|
|
2555
|
+
if (this._cachedPreviousPatterns !== null) return this._cachedPreviousPatterns;
|
|
1753
2556
|
try {
|
|
1754
2557
|
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
|
|
1755
2558
|
const prev = JSON.parse(raw);
|
|
1756
2559
|
const version = prev.schemaVersion ?? 0;
|
|
1757
2560
|
const patterns = prev.topFailureRules ?? [];
|
|
1758
|
-
|
|
1759
|
-
return this.migratePatterns(patterns, version);
|
|
2561
|
+
this._cachedPreviousPatterns = version === PATTERN_SCHEMA_VERSION ? patterns : this.migratePatterns(patterns, version);
|
|
1760
2562
|
} catch {
|
|
1761
|
-
|
|
2563
|
+
this._cachedPreviousPatterns = [];
|
|
1762
2564
|
}
|
|
2565
|
+
return this._cachedPreviousPatterns;
|
|
1763
2566
|
}
|
|
1764
2567
|
migratePatterns(patterns, fromVersion) {
|
|
1765
2568
|
let migrated = patterns;
|
|
@@ -1772,19 +2575,23 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1772
2575
|
}));
|
|
1773
2576
|
}
|
|
1774
2577
|
if (fromVersion < 2) {
|
|
1775
|
-
migrated = migrated.map((p) =>
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
...p
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
2578
|
+
migrated = migrated.map((p) => {
|
|
2579
|
+
const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
|
|
2580
|
+
return {
|
|
2581
|
+
...p,
|
|
2582
|
+
scoringFactors: {
|
|
2583
|
+
...sf,
|
|
2584
|
+
stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
});
|
|
1782
2588
|
}
|
|
1783
2589
|
return migrated;
|
|
1784
2590
|
}
|
|
1785
2591
|
async analyze(days = 30) {
|
|
1786
2592
|
const previousPatterns = await this.loadPreviousPatterns();
|
|
1787
2593
|
const events = await this.readAllEvents(days);
|
|
2594
|
+
this._cachedEvents = events;
|
|
1788
2595
|
const starts = events.filter((e) => e.eventType === "build_start");
|
|
1789
2596
|
const attempts = events.filter((e) => e.eventType === "generation_attempt");
|
|
1790
2597
|
const passed = attempts.filter(
|
|
@@ -1797,13 +2604,18 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1797
2604
|
const credentialFailures = /* @__PURE__ */ new Map();
|
|
1798
2605
|
for (const a of failed) {
|
|
1799
2606
|
const weight = this.recencyWeight(a.fileDate);
|
|
2607
|
+
const buildId = a.runId ?? a.sessionId;
|
|
1800
2608
|
const data = a.data;
|
|
1801
2609
|
for (const issue of data.issues ?? []) {
|
|
1802
|
-
|
|
2610
|
+
if (issue.severity === "warn") continue;
|
|
2611
|
+
const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
|
|
1803
2612
|
entry.count++;
|
|
1804
|
-
entry.sessions.add(
|
|
2613
|
+
entry.sessions.add(buildId);
|
|
1805
2614
|
entry.recencyWeights.push(weight);
|
|
1806
2615
|
entry.allMessages.push(issue.message);
|
|
2616
|
+
if (data.workflowType) {
|
|
2617
|
+
entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
|
|
2618
|
+
}
|
|
1807
2619
|
ruleFailures.set(issue.rule, entry);
|
|
1808
2620
|
if (issue.rule === 17) {
|
|
1809
2621
|
const credPatterns = [
|
|
@@ -1856,9 +2668,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1856
2668
|
}
|
|
1857
2669
|
const sessions = /* @__PURE__ */ new Map();
|
|
1858
2670
|
for (const a of attempts) {
|
|
1859
|
-
const
|
|
2671
|
+
const buildId = a.runId ?? a.sessionId;
|
|
2672
|
+
const list = sessions.get(buildId) ?? [];
|
|
1860
2673
|
list.push(a);
|
|
1861
|
-
sessions.set(
|
|
2674
|
+
sessions.set(buildId, list);
|
|
1862
2675
|
}
|
|
1863
2676
|
let firstTryPass = 0;
|
|
1864
2677
|
let correctionNeeded = 0;
|
|
@@ -1905,7 +2718,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1905
2718
|
const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
|
|
1906
2719
|
const stickiness = stickinessCount.get(rule) ?? 0;
|
|
1907
2720
|
const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
|
|
1908
|
-
|
|
2721
|
+
const pattern = {
|
|
1909
2722
|
rule,
|
|
1910
2723
|
failureCount: entry.count,
|
|
1911
2724
|
confidence: Math.round(rawConfidence * 1e3) / 1e3,
|
|
@@ -1917,6 +2730,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1917
2730
|
exampleMessages: this.deduplicateMessages(entry.allMessages),
|
|
1918
2731
|
mitigation: RULE_MITIGATIONS[rule] ?? null
|
|
1919
2732
|
};
|
|
2733
|
+
if (entry.workflowTypes.size > 0) {
|
|
2734
|
+
pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
|
|
2735
|
+
}
|
|
2736
|
+
return pattern;
|
|
1920
2737
|
}).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1921
2738
|
const activeRules = new Set(activePatterns.map((p) => p.rule));
|
|
1922
2739
|
for (const p of activePatterns) {
|
|
@@ -1973,7 +2790,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1973
2790
|
const warned = bcData.warnedRules ?? [];
|
|
1974
2791
|
if (warned.length === 0) continue;
|
|
1975
2792
|
const sessionFailedRules = /* @__PURE__ */ new Set();
|
|
1976
|
-
const sessionAttempts = sessions.get(bc.sessionId) ?? [];
|
|
2793
|
+
const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
|
|
1977
2794
|
for (const a of sessionAttempts) {
|
|
1978
2795
|
const ad = a.data;
|
|
1979
2796
|
if (ad.validationPassed === false) {
|
|
@@ -2045,6 +2862,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2045
2862
|
const tmpPath = `${outputPath}.tmp`;
|
|
2046
2863
|
await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
2047
2864
|
await (0, import_promises3.rename)(tmpPath, outputPath);
|
|
2865
|
+
this._cachedPreviousPatterns = null;
|
|
2048
2866
|
const historySummary = {
|
|
2049
2867
|
timestamp: analysis.generatedAt,
|
|
2050
2868
|
totalBuilds: analysis.summary.totalBuilds,
|
|
@@ -2056,8 +2874,55 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2056
2874
|
};
|
|
2057
2875
|
const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
|
|
2058
2876
|
await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
|
|
2877
|
+
const sessions = await this.buildSessionSummaries(days);
|
|
2878
|
+
const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
|
|
2879
|
+
const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
|
|
2880
|
+
await (0, import_promises3.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
|
|
2881
|
+
await (0, import_promises3.rename)(sessionHistoryTmp, sessionHistoryPath);
|
|
2059
2882
|
return analysis;
|
|
2060
2883
|
}
|
|
2884
|
+
async getSessions(limit = 20) {
|
|
2885
|
+
try {
|
|
2886
|
+
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
|
|
2887
|
+
const all = JSON.parse(raw);
|
|
2888
|
+
return all.slice(-limit);
|
|
2889
|
+
} catch {
|
|
2890
|
+
return [];
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
async buildSessionSummaries(days = 30) {
|
|
2894
|
+
const events = this._cachedEvents ?? await this.readAllEvents(days);
|
|
2895
|
+
const buildCompletes = events.filter((e) => e.eventType === "build_complete");
|
|
2896
|
+
const attemptsByBuild = /* @__PURE__ */ new Map();
|
|
2897
|
+
for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
|
|
2898
|
+
const buildId = e.runId ?? e.sessionId;
|
|
2899
|
+
const list = attemptsByBuild.get(buildId) ?? [];
|
|
2900
|
+
list.push(e);
|
|
2901
|
+
attemptsByBuild.set(buildId, list);
|
|
2902
|
+
}
|
|
2903
|
+
const summaries = buildCompletes.map((bc) => {
|
|
2904
|
+
const data = bc.data;
|
|
2905
|
+
const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
|
|
2906
|
+
const failedRules = Array.from(new Set(
|
|
2907
|
+
sessionAttempts.flatMap((a) => {
|
|
2908
|
+
const ad = a.data;
|
|
2909
|
+
if (ad.validationPassed !== false) return [];
|
|
2910
|
+
return (ad.issues ?? []).map((i) => i.rule);
|
|
2911
|
+
})
|
|
2912
|
+
));
|
|
2913
|
+
return {
|
|
2914
|
+
sessionId: bc.runId ?? bc.sessionId,
|
|
2915
|
+
date: bc.fileDate,
|
|
2916
|
+
description: data.description ?? "",
|
|
2917
|
+
workflowType: data.workflowType ?? null,
|
|
2918
|
+
attempts: data.totalAttempts ?? 1,
|
|
2919
|
+
success: data.success ?? false,
|
|
2920
|
+
failedRules,
|
|
2921
|
+
workflowName: data.workflowName ?? null
|
|
2922
|
+
};
|
|
2923
|
+
});
|
|
2924
|
+
return summaries.sort((a, b) => a.date.localeCompare(b.date));
|
|
2925
|
+
}
|
|
2061
2926
|
async getHistory(limit = 20) {
|
|
2062
2927
|
try {
|
|
2063
2928
|
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
|
|
@@ -2079,7 +2944,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2079
2944
|
alerts.push({
|
|
2080
2945
|
type: "stale_pattern",
|
|
2081
2946
|
rule: p.rule,
|
|
2082
|
-
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)`
|
|
2083
2948
|
});
|
|
2084
2949
|
}
|
|
2085
2950
|
}
|
|
@@ -2211,6 +3076,43 @@ ${regularLines}`;
|
|
|
2211
3076
|
}
|
|
2212
3077
|
};
|
|
2213
3078
|
|
|
3079
|
+
// src/telemetry/collector.ts
|
|
3080
|
+
var import_promises4 = require("fs/promises");
|
|
3081
|
+
var import_node_path6 = require("path");
|
|
3082
|
+
var import_node_os5 = require("os");
|
|
3083
|
+
|
|
3084
|
+
// src/telemetry/types.ts
|
|
3085
|
+
var TELEMETRY_SCHEMA_VERSION = 2;
|
|
3086
|
+
|
|
3087
|
+
// src/telemetry/collector.ts
|
|
3088
|
+
var TelemetryCollector = class {
|
|
3089
|
+
dir;
|
|
3090
|
+
sessionId;
|
|
3091
|
+
dirReady = null;
|
|
3092
|
+
constructor(dir) {
|
|
3093
|
+
this.dir = dir ?? (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".kairos", "telemetry");
|
|
3094
|
+
this.sessionId = generateUUID();
|
|
3095
|
+
}
|
|
3096
|
+
async emit(eventType, data, runId) {
|
|
3097
|
+
const event = {
|
|
3098
|
+
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
3099
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3100
|
+
sessionId: this.sessionId,
|
|
3101
|
+
...runId ? { runId } : {},
|
|
3102
|
+
eventType,
|
|
3103
|
+
data
|
|
3104
|
+
};
|
|
3105
|
+
if (!this.dirReady) {
|
|
3106
|
+
this.dirReady = (0, import_promises4.mkdir)(this.dir, { recursive: true }).then(() => {
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
await this.dirReady;
|
|
3110
|
+
const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
|
|
3111
|
+
const filepath = (0, import_node_path6.join)(this.dir, filename);
|
|
3112
|
+
await (0, import_promises4.appendFile)(filepath, JSON.stringify(event) + "\n", "utf-8");
|
|
3113
|
+
}
|
|
3114
|
+
};
|
|
3115
|
+
|
|
2214
3116
|
// src/utils/logger.ts
|
|
2215
3117
|
var nullLogger = {
|
|
2216
3118
|
debug() {
|
|
@@ -2223,19 +3125,95 @@ var nullLogger = {
|
|
|
2223
3125
|
}
|
|
2224
3126
|
};
|
|
2225
3127
|
|
|
3128
|
+
// src/utils/workflow-type.ts
|
|
3129
|
+
var TYPE_KEYWORDS = [
|
|
3130
|
+
["gmail", "email"],
|
|
3131
|
+
["imap", "email"],
|
|
3132
|
+
["smtp", "email"],
|
|
3133
|
+
[" email", "email"],
|
|
3134
|
+
["slack", "slack"],
|
|
3135
|
+
["telegram", "messaging"],
|
|
3136
|
+
["discord", "messaging"],
|
|
3137
|
+
[" sms", "messaging"],
|
|
3138
|
+
["twilio", "messaging"],
|
|
3139
|
+
["webhook", "webhook"],
|
|
3140
|
+
["google sheets", "data"],
|
|
3141
|
+
["spreadsheet", "data"],
|
|
3142
|
+
["airtable", "data"],
|
|
3143
|
+
["notion", "data"],
|
|
3144
|
+
["github", "devops"],
|
|
3145
|
+
["gitlab", "devops"],
|
|
3146
|
+
["schedule", "schedule"],
|
|
3147
|
+
[" cron", "schedule"],
|
|
3148
|
+
["daily", "schedule"],
|
|
3149
|
+
["weekly", "schedule"],
|
|
3150
|
+
["hourly", "schedule"],
|
|
3151
|
+
["every day", "schedule"],
|
|
3152
|
+
["every hour", "schedule"],
|
|
3153
|
+
["every morning", "schedule"],
|
|
3154
|
+
["postgres", "database"],
|
|
3155
|
+
["mysql", "database"],
|
|
3156
|
+
["supabase", "database"],
|
|
3157
|
+
["redis", "database"],
|
|
3158
|
+
[" database", "database"],
|
|
3159
|
+
[" llm", "ai"],
|
|
3160
|
+
[" gpt", "ai"],
|
|
3161
|
+
["claude", "ai"],
|
|
3162
|
+
[" agent", "ai"],
|
|
3163
|
+
["langchain", "ai"],
|
|
3164
|
+
[" ai ", "ai"],
|
|
3165
|
+
[" ai", "ai"],
|
|
3166
|
+
["http request", "api"],
|
|
3167
|
+
["rest api", "api"],
|
|
3168
|
+
[" api", "api"]
|
|
3169
|
+
];
|
|
3170
|
+
function inferWorkflowType(description) {
|
|
3171
|
+
const lower = " " + description.toLowerCase();
|
|
3172
|
+
for (const [keyword, type] of TYPE_KEYWORDS) {
|
|
3173
|
+
if (lower.includes(keyword)) return type;
|
|
3174
|
+
}
|
|
3175
|
+
return null;
|
|
3176
|
+
}
|
|
3177
|
+
|
|
2226
3178
|
// src/mcp-server.ts
|
|
2227
3179
|
var import_node_fs3 = require("fs");
|
|
2228
|
-
var
|
|
3180
|
+
var import_node_path7 = require("path");
|
|
3181
|
+
var import_node_os6 = require("os");
|
|
2229
3182
|
var import_node_url = require("url");
|
|
2230
3183
|
var import_meta = {};
|
|
2231
|
-
var __dirname = (0,
|
|
2232
|
-
var pkg = JSON.parse((0, import_node_fs3.readFileSync)((0,
|
|
3184
|
+
var __dirname = (0, import_node_path7.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
3185
|
+
var pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path7.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
2233
3186
|
var library = new FileLibrary();
|
|
2234
|
-
var
|
|
3187
|
+
var _validator = new N8nValidator();
|
|
3188
|
+
function getValidator() {
|
|
3189
|
+
return _validator;
|
|
3190
|
+
}
|
|
2235
3191
|
var nodeSyncer = new NodeSyncer();
|
|
2236
3192
|
var lastSync = null;
|
|
3193
|
+
var AUTO_SYNC_TIMEOUT_MS = 5e3;
|
|
2237
3194
|
var stripper = new N8nFieldStripper();
|
|
2238
|
-
var promptBuilder = new PromptBuilder();
|
|
3195
|
+
var promptBuilder = new PromptBuilder(getMcpPatternsPath());
|
|
3196
|
+
function getMcpTelemetry() {
|
|
3197
|
+
const val = process.env["KAIROS_TELEMETRY"];
|
|
3198
|
+
if (!val || val === "false") return null;
|
|
3199
|
+
return val === "true" ? new TelemetryCollector() : new TelemetryCollector(val);
|
|
3200
|
+
}
|
|
3201
|
+
function getMcpPatternsPath() {
|
|
3202
|
+
const val = process.env["KAIROS_TELEMETRY"];
|
|
3203
|
+
if (val && val !== "false" && val !== "true") {
|
|
3204
|
+
return (0, import_node_path7.join)(val, "..", "patterns.json");
|
|
3205
|
+
}
|
|
3206
|
+
return (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".kairos", "patterns.json");
|
|
3207
|
+
}
|
|
3208
|
+
var mcpTelemetry = getMcpTelemetry();
|
|
3209
|
+
var mcpSessions = /* @__PURE__ */ new Map();
|
|
3210
|
+
var SESSION_TTL_MS = 60 * 60 * 1e3;
|
|
3211
|
+
function evictStaleSessions() {
|
|
3212
|
+
const cutoff = Date.now() - SESSION_TTL_MS;
|
|
3213
|
+
for (const [id, session] of mcpSessions) {
|
|
3214
|
+
if (session.startTime < cutoff) mcpSessions.delete(id);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
2239
3217
|
function getTelemetryReader() {
|
|
2240
3218
|
try {
|
|
2241
3219
|
return new TelemetryReader();
|
|
@@ -2247,11 +3225,23 @@ function isAllowed(action) {
|
|
|
2247
3225
|
const key = `KAIROS_MCP_ALLOW_${action.toUpperCase()}`;
|
|
2248
3226
|
return process.env[key] === "true";
|
|
2249
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
|
+
}
|
|
2250
3240
|
function getApiClient() {
|
|
2251
3241
|
const baseUrl = process.env["N8N_BASE_URL"];
|
|
2252
3242
|
const apiKey = process.env["N8N_API_KEY"];
|
|
2253
3243
|
if (!baseUrl || !apiKey) {
|
|
2254
|
-
throw new
|
|
3244
|
+
throw new GuardError("N8N_BASE_URL and N8N_API_KEY environment variables are required for n8n operations");
|
|
2255
3245
|
}
|
|
2256
3246
|
return new N8nApiClient(baseUrl, apiKey, nullLogger);
|
|
2257
3247
|
}
|
|
@@ -2265,7 +3255,7 @@ async function autoSync() {
|
|
|
2265
3255
|
const nodeTypes = await client.getNodeTypes();
|
|
2266
3256
|
if (nodeTypes.length === 0) return null;
|
|
2267
3257
|
lastSync = nodeSyncer.sync(nodeTypes);
|
|
2268
|
-
|
|
3258
|
+
_validator = new N8nValidator(lastSync.registry);
|
|
2269
3259
|
return lastSync;
|
|
2270
3260
|
} catch {
|
|
2271
3261
|
return null;
|
|
@@ -2283,103 +3273,110 @@ server.tool(
|
|
|
2283
3273
|
name: import_zod.z.string().optional().describe("Optional workflow name override")
|
|
2284
3274
|
},
|
|
2285
3275
|
async ({ description, name }) => {
|
|
3276
|
+
evictStaleSessions();
|
|
2286
3277
|
const baseUrl = process.env["N8N_BASE_URL"];
|
|
2287
3278
|
const apiKey = process.env["N8N_API_KEY"];
|
|
2288
3279
|
if (!baseUrl || !apiKey) {
|
|
2289
|
-
return {
|
|
2290
|
-
content: [{
|
|
2291
|
-
type: "text",
|
|
2292
|
-
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." })
|
|
2293
|
-
}],
|
|
2294
|
-
isError: true
|
|
2295
|
-
};
|
|
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." }));
|
|
2296
3281
|
}
|
|
3282
|
+
const runId = generateUUID();
|
|
3283
|
+
const workflowType = inferWorkflowType(description);
|
|
3284
|
+
const syncPromise = autoSync();
|
|
3285
|
+
const syncTimeout = new Promise((resolve) => setTimeout(() => resolve(null), AUTO_SYNC_TIMEOUT_MS));
|
|
2297
3286
|
await library.initialize();
|
|
2298
|
-
const syncResult = await
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
+
]);
|
|
2302
3295
|
const request = { description, ...name ? { name } : {} };
|
|
2303
3296
|
const built = promptBuilder.build(request, matches, failureRates, syncResult?.catalogText);
|
|
3297
|
+
if (mcpTelemetry) {
|
|
3298
|
+
mcpSessions.set(runId, {
|
|
3299
|
+
description,
|
|
3300
|
+
startTime: Date.now(),
|
|
3301
|
+
validateAttempts: 0,
|
|
3302
|
+
warnedRules: promptBuilder.getWarnedRules(),
|
|
3303
|
+
workflowType
|
|
3304
|
+
});
|
|
3305
|
+
await mcpTelemetry.emit("build_start", { description, model: "mcp-decomposed", dryRun: false }, runId);
|
|
3306
|
+
}
|
|
2304
3307
|
const systemText = built.system.map((block) => block.text).join("\n\n---\n\n");
|
|
2305
|
-
return {
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
}
|
|
2333
|
-
}, null, 2)
|
|
2334
|
-
}]
|
|
2335
|
-
};
|
|
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));
|
|
2336
3335
|
}
|
|
2337
3336
|
);
|
|
2338
3337
|
server.tool(
|
|
2339
3338
|
"kairos_validate",
|
|
2340
|
-
"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.",
|
|
2341
3340
|
{
|
|
2342
|
-
workflow: import_zod.z.string().describe("The workflow JSON string to validate")
|
|
3341
|
+
workflow: import_zod.z.string().describe("The workflow JSON string to validate"),
|
|
3342
|
+
kairos_run_id: import_zod.z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation")
|
|
2343
3343
|
},
|
|
2344
|
-
async ({ workflow: workflowStr }) => {
|
|
3344
|
+
async ({ workflow: workflowStr, kairos_run_id }) => {
|
|
2345
3345
|
let parsed;
|
|
2346
3346
|
try {
|
|
2347
3347
|
parsed = JSON.parse(workflowStr);
|
|
2348
3348
|
} catch (e) {
|
|
2349
|
-
return {
|
|
2350
|
-
content: [{
|
|
2351
|
-
type: "text",
|
|
2352
|
-
text: JSON.stringify({
|
|
2353
|
-
valid: false,
|
|
2354
|
-
error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`
|
|
2355
|
-
}, null, 2)
|
|
2356
|
-
}]
|
|
2357
|
-
};
|
|
3349
|
+
return mcpText(JSON.stringify({ valid: false, error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }, null, 2));
|
|
2358
3350
|
}
|
|
2359
|
-
const result =
|
|
3351
|
+
const result = getValidator().validate(parsed);
|
|
2360
3352
|
const errors = result.issues.filter((i) => i.severity === "error");
|
|
2361
3353
|
const warnings = result.issues.filter((i) => i.severity === "warn");
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
3354
|
+
if (mcpTelemetry && kairos_run_id) {
|
|
3355
|
+
const session = mcpSessions.get(kairos_run_id);
|
|
3356
|
+
if (session) {
|
|
3357
|
+
session.validateAttempts++;
|
|
3358
|
+
await mcpTelemetry.emit("generation_attempt", {
|
|
3359
|
+
description: session.description,
|
|
3360
|
+
attempt: session.validateAttempts,
|
|
3361
|
+
temperature: 0,
|
|
3362
|
+
durationMs: 0,
|
|
3363
|
+
tokensInput: 0,
|
|
3364
|
+
tokensOutput: 0,
|
|
3365
|
+
validationPassed: result.valid,
|
|
3366
|
+
issueCount: result.issues.length,
|
|
3367
|
+
issues: result.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null })),
|
|
3368
|
+
workflowType: session.workflowType
|
|
3369
|
+
}, kairos_run_id);
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
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));
|
|
2383
3380
|
}
|
|
2384
3381
|
);
|
|
2385
3382
|
server.tool(
|
|
@@ -2387,79 +3384,147 @@ server.tool(
|
|
|
2387
3384
|
"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.",
|
|
2388
3385
|
{
|
|
2389
3386
|
workflow: import_zod.z.string().describe("The validated workflow JSON string to deploy"),
|
|
2390
|
-
activate: import_zod.z.boolean().default(false).describe("Activate the workflow immediately after deployment")
|
|
3387
|
+
activate: import_zod.z.boolean().default(false).describe("Activate the workflow immediately after deployment"),
|
|
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")
|
|
2391
3390
|
},
|
|
2392
|
-
async ({ workflow: workflowStr, activate }) => {
|
|
3391
|
+
async ({ workflow: workflowStr, activate, kairos_run_id, kairos_secret }) => {
|
|
3392
|
+
const authError = checkMcpAuth(kairos_secret);
|
|
3393
|
+
if (authError) return authError;
|
|
2393
3394
|
if (!isAllowed("deploy")) {
|
|
2394
|
-
return {
|
|
2395
|
-
content: [{
|
|
2396
|
-
type: "text",
|
|
2397
|
-
text: JSON.stringify({ error: "Deploy is disabled. Set KAIROS_MCP_ALLOW_DEPLOY=true to enable." })
|
|
2398
|
-
}],
|
|
2399
|
-
isError: true
|
|
2400
|
-
};
|
|
3395
|
+
return mcpError(JSON.stringify({ error: "Deploy is disabled. Set KAIROS_MCP_ALLOW_DEPLOY=true to enable." }));
|
|
2401
3396
|
}
|
|
2402
3397
|
let parsed;
|
|
2403
3398
|
try {
|
|
2404
3399
|
parsed = JSON.parse(workflowStr);
|
|
2405
3400
|
} catch (e) {
|
|
2406
|
-
return {
|
|
2407
|
-
content: [{
|
|
2408
|
-
type: "text",
|
|
2409
|
-
text: JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` })
|
|
2410
|
-
}]
|
|
2411
|
-
};
|
|
3401
|
+
return mcpError(JSON.stringify({ error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }));
|
|
2412
3402
|
}
|
|
2413
|
-
const validation =
|
|
3403
|
+
const validation = getValidator().validate(parsed);
|
|
2414
3404
|
const errors = validation.issues.filter((i) => i.severity === "error");
|
|
2415
3405
|
if (errors.length > 0) {
|
|
2416
|
-
return {
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
error: "Workflow has validation errors \u2014 fix them before deploying",
|
|
2421
|
-
errors: errors.map((i) => ({ rule: i.rule, message: i.message }))
|
|
2422
|
-
}, null, 2)
|
|
2423
|
-
}]
|
|
2424
|
-
};
|
|
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));
|
|
2425
3410
|
}
|
|
2426
3411
|
const client = getApiClient();
|
|
2427
3412
|
const stripped = stripper.stripForCreate(parsed);
|
|
2428
3413
|
const response = await client.createWorkflow(stripped);
|
|
2429
3414
|
if (activate) {
|
|
2430
3415
|
if (!isAllowed("activate")) {
|
|
2431
|
-
return {
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
warning: "Workflow deployed but activation is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable.",
|
|
2439
|
-
url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
|
|
2440
|
-
}, null, 2)
|
|
2441
|
-
}]
|
|
2442
|
-
};
|
|
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));
|
|
2443
3423
|
}
|
|
2444
3424
|
await client.activateWorkflow(response.id);
|
|
2445
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.` : "";
|
|
2446
3430
|
await library.initialize();
|
|
2447
3431
|
await library.save(parsed, {
|
|
2448
|
-
description: parsed.name,
|
|
3432
|
+
description: session?.description ?? parsed.name,
|
|
2449
3433
|
generationMode: "scratch",
|
|
2450
|
-
generationAttempts: 1
|
|
3434
|
+
generationAttempts: session?.validateAttempts ?? 1,
|
|
3435
|
+
n8nWorkflowId: response.id
|
|
2451
3436
|
});
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
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
|
+
});
|
|
3455
|
+
}
|
|
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);
|
|
2463
3528
|
}
|
|
2464
3529
|
);
|
|
2465
3530
|
server.tool(
|
|
@@ -2472,23 +3537,20 @@ server.tool(
|
|
|
2472
3537
|
async ({ query, limit }) => {
|
|
2473
3538
|
await library.initialize();
|
|
2474
3539
|
const matches = await library.search(query);
|
|
2475
|
-
return
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
)
|
|
2490
|
-
}]
|
|
2491
|
-
};
|
|
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
|
+
));
|
|
2492
3554
|
}
|
|
2493
3555
|
);
|
|
2494
3556
|
server.tool(
|
|
@@ -2499,36 +3561,19 @@ server.tool(
|
|
|
2499
3561
|
const baseUrl = process.env["N8N_BASE_URL"];
|
|
2500
3562
|
const apiKey = process.env["N8N_API_KEY"];
|
|
2501
3563
|
if (!baseUrl || !apiKey) {
|
|
2502
|
-
return {
|
|
2503
|
-
content: [{
|
|
2504
|
-
type: "text",
|
|
2505
|
-
text: JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required for sync." })
|
|
2506
|
-
}],
|
|
2507
|
-
isError: true
|
|
2508
|
-
};
|
|
3564
|
+
return mcpError(JSON.stringify({ error: "N8N_BASE_URL and N8N_API_KEY are required for sync." }));
|
|
2509
3565
|
}
|
|
2510
3566
|
lastSync = null;
|
|
2511
3567
|
const result = await autoSync();
|
|
2512
3568
|
if (!result) {
|
|
2513
|
-
return {
|
|
2514
|
-
content: [{
|
|
2515
|
-
type: "text",
|
|
2516
|
-
text: JSON.stringify({ error: "Failed to fetch node types from n8n. Check your credentials and that your instance is running." })
|
|
2517
|
-
}],
|
|
2518
|
-
isError: true
|
|
2519
|
-
};
|
|
3569
|
+
return mcpError(JSON.stringify({ error: "Failed to fetch node types from n8n. Check your credentials and that your instance is running." }));
|
|
2520
3570
|
}
|
|
2521
|
-
return {
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
newNodes: result.newNodes,
|
|
2528
|
-
message: `Synced ${result.nodeCount} node types from your n8n instance (${result.newNodes} not in default catalog).`
|
|
2529
|
-
}, null, 2)
|
|
2530
|
-
}]
|
|
2531
|
-
};
|
|
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));
|
|
2532
3577
|
}
|
|
2533
3578
|
);
|
|
2534
3579
|
server.tool(
|
|
@@ -2544,12 +3589,72 @@ server.tool(
|
|
|
2544
3589
|
if (limit !== void 0 && limit > 0) {
|
|
2545
3590
|
analysis.topFailureRules = analysis.topFailureRules.slice(0, limit);
|
|
2546
3591
|
}
|
|
2547
|
-
return
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
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 }));
|
|
2553
3658
|
}
|
|
2554
3659
|
);
|
|
2555
3660
|
server.tool(
|
|
@@ -2559,12 +3664,7 @@ server.tool(
|
|
|
2559
3664
|
async () => {
|
|
2560
3665
|
const client = getApiClient();
|
|
2561
3666
|
const workflows = await client.listWorkflows();
|
|
2562
|
-
return
|
|
2563
|
-
content: [{
|
|
2564
|
-
type: "text",
|
|
2565
|
-
text: JSON.stringify(workflows, null, 2)
|
|
2566
|
-
}]
|
|
2567
|
-
};
|
|
3667
|
+
return mcpText(JSON.stringify(workflows, null, 2));
|
|
2568
3668
|
}
|
|
2569
3669
|
);
|
|
2570
3670
|
server.tool(
|
|
@@ -2576,12 +3676,7 @@ server.tool(
|
|
|
2576
3676
|
async ({ workflow_id }) => {
|
|
2577
3677
|
const client = getApiClient();
|
|
2578
3678
|
const workflow = await client.getWorkflow(workflow_id);
|
|
2579
|
-
return
|
|
2580
|
-
content: [{
|
|
2581
|
-
type: "text",
|
|
2582
|
-
text: JSON.stringify(workflow, null, 2)
|
|
2583
|
-
}]
|
|
2584
|
-
};
|
|
3679
|
+
return mcpText(JSON.stringify(workflow, null, 2));
|
|
2585
3680
|
}
|
|
2586
3681
|
);
|
|
2587
3682
|
server.tool(
|
|
@@ -2592,22 +3687,11 @@ server.tool(
|
|
|
2592
3687
|
},
|
|
2593
3688
|
async ({ workflow_id }) => {
|
|
2594
3689
|
if (!isAllowed("activate")) {
|
|
2595
|
-
return {
|
|
2596
|
-
content: [{
|
|
2597
|
-
type: "text",
|
|
2598
|
-
text: JSON.stringify({ error: "Activate is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable." })
|
|
2599
|
-
}],
|
|
2600
|
-
isError: true
|
|
2601
|
-
};
|
|
3690
|
+
return mcpError(JSON.stringify({ error: "Activate is disabled. Set KAIROS_MCP_ALLOW_ACTIVATE=true to enable." }));
|
|
2602
3691
|
}
|
|
2603
3692
|
const client = getApiClient();
|
|
2604
3693
|
await client.activateWorkflow(workflow_id);
|
|
2605
|
-
return {
|
|
2606
|
-
content: [{
|
|
2607
|
-
type: "text",
|
|
2608
|
-
text: `Activated workflow ${workflow_id}`
|
|
2609
|
-
}]
|
|
2610
|
-
};
|
|
3694
|
+
return mcpText(`Activated workflow ${workflow_id}`);
|
|
2611
3695
|
}
|
|
2612
3696
|
);
|
|
2613
3697
|
server.tool(
|
|
@@ -2619,38 +3703,25 @@ server.tool(
|
|
|
2619
3703
|
async ({ workflow_id }) => {
|
|
2620
3704
|
const client = getApiClient();
|
|
2621
3705
|
await client.deactivateWorkflow(workflow_id);
|
|
2622
|
-
return {
|
|
2623
|
-
content: [{
|
|
2624
|
-
type: "text",
|
|
2625
|
-
text: `Deactivated workflow ${workflow_id}`
|
|
2626
|
-
}]
|
|
2627
|
-
};
|
|
3706
|
+
return mcpText(`Deactivated workflow ${workflow_id}`);
|
|
2628
3707
|
}
|
|
2629
3708
|
);
|
|
2630
3709
|
server.tool(
|
|
2631
3710
|
"kairos_delete",
|
|
2632
3711
|
"Delete a workflow from n8n. This is irreversible.",
|
|
2633
3712
|
{
|
|
2634
|
-
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")
|
|
2635
3715
|
},
|
|
2636
|
-
async ({ workflow_id }) => {
|
|
3716
|
+
async ({ workflow_id, kairos_secret }) => {
|
|
3717
|
+
const authError = checkMcpAuth(kairos_secret);
|
|
3718
|
+
if (authError) return authError;
|
|
2637
3719
|
if (!isAllowed("delete")) {
|
|
2638
|
-
return {
|
|
2639
|
-
content: [{
|
|
2640
|
-
type: "text",
|
|
2641
|
-
text: JSON.stringify({ error: "Delete is disabled. Set KAIROS_MCP_ALLOW_DELETE=true to enable." })
|
|
2642
|
-
}],
|
|
2643
|
-
isError: true
|
|
2644
|
-
};
|
|
3720
|
+
return mcpError(JSON.stringify({ error: "Delete is disabled. Set KAIROS_MCP_ALLOW_DELETE=true to enable." }));
|
|
2645
3721
|
}
|
|
2646
3722
|
const client = getApiClient();
|
|
2647
3723
|
await client.deleteWorkflow(workflow_id);
|
|
2648
|
-
return {
|
|
2649
|
-
content: [{
|
|
2650
|
-
type: "text",
|
|
2651
|
-
text: `Deleted workflow ${workflow_id}`
|
|
2652
|
-
}]
|
|
2653
|
-
};
|
|
3724
|
+
return mcpText(`Deleted workflow ${workflow_id}`);
|
|
2654
3725
|
}
|
|
2655
3726
|
);
|
|
2656
3727
|
server.tool(
|
|
@@ -2663,15 +3734,15 @@ server.tool(
|
|
|
2663
3734
|
async ({ workflow_id, limit }) => {
|
|
2664
3735
|
const client = getApiClient();
|
|
2665
3736
|
const executions = await client.getExecutions(workflow_id, { limit });
|
|
2666
|
-
return
|
|
2667
|
-
content: [{
|
|
2668
|
-
type: "text",
|
|
2669
|
-
text: JSON.stringify(executions, null, 2)
|
|
2670
|
-
}]
|
|
2671
|
-
};
|
|
3737
|
+
return mcpText(JSON.stringify(executions, null, 2));
|
|
2672
3738
|
}
|
|
2673
3739
|
);
|
|
2674
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
|
+
}
|
|
2675
3746
|
const transport = new import_stdio.StdioServerTransport();
|
|
2676
3747
|
await server.connect(transport);
|
|
2677
3748
|
}
|