@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.
@@ -13,7 +13,13 @@ var import_node_os = require("os");
13
13
 
14
14
  // src/utils/uuid.ts
15
15
  function generateUUID() {
16
- return crypto.randomUUID();
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
- var WEIGHTS = {
30
- tfidf: 0.35,
31
- nodeFingerprint: 0.3,
32
- outcome: 0.2,
33
- deploy: 0.15
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
- return candidates.map((c) => {
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
- ...cluster ? { clusterPattern: cluster.pattern } : {}
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 MAX_LIBRARY_SIZE = 500;
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
- workflows = [];
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
- const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
275
- const parsed = JSON.parse(raw);
276
- if (!Array.isArray(parsed)) {
277
- this.workflows = [];
278
- } else {
279
- this.workflows = parsed.filter(
280
- (item) => typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(item.workflow.nodes)
281
- );
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
- this.workflows = [];
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 searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
289
- if (searchable.length === 0) return [];
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 docTokenArrays = searchable.map((w) => tokenize(buildSearchCorpus(w)));
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 = searchable.length;
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
- idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
467
+ const rawIdf = Math.log((docCount + 1) / (docsWithToken + 1)) + 1;
468
+ idf.set(token, rawIdf / idfCeiling);
301
469
  }
302
- const scored = hybridScore(queryTokens, description, searchable, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
303
- const clusters = clusterWorkflows(searchable);
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
- const results = reranked.map((m) => {
306
- return { workflow: m.workflow, score: m.score, mode: scoreToMode(m.score) };
307
- });
308
- if (results.length > 0) {
309
- for (const r of results) {
310
- r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
311
- }
312
- this.persist();
313
- }
314
- return results;
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 stored = {
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.workflows.push(stored);
339
- if (this.workflows.length > MAX_LIBRARY_SIZE) {
340
- this.workflows.sort((a, b) => (b.deployCount ?? 1) - (a.deployCount ?? 1));
341
- this.workflows = this.workflows.slice(0, MAX_LIBRARY_SIZE);
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 w = this.workflows.find((w2) => w2.id === id);
348
- if (w) {
349
- w.deployCount++;
350
- w.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
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 w = this.workflows.find((w2) => w2.id === id);
356
- if (!w) return;
561
+ const m = this.meta.find((m2) => m2.id === id);
562
+ if (!m) return;
357
563
  if (outcome.mode === "direct") {
358
- w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
564
+ m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
359
565
  } else {
360
- w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
566
+ m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
361
567
  }
362
- const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
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
- w.outcomeStats = stats;
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
- return this.workflows.find((w) => w.id === id) ?? null;
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 result = this.workflows;
590
+ let filtered = this.meta;
381
591
  if (filters?.platform) {
382
- result = result.filter((w) => w.platform === filters.platform);
592
+ filtered = filtered.filter((m) => m.platform === filters.platform);
383
593
  }
384
594
  if (filters?.tags && filters.tags.length > 0) {
385
- result = result.filter((w) => filters.tags.some((t) => w.tags.includes(t)));
386
- }
387
- return result;
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
- persist() {
403
- this.writeQueue = this.writeQueue.then(async () => {
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.workflows, null, 2), "utf-8");
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
- for (const [, outputs] of Object.entries(w.connections)) {
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
- for (const portGroup of Object.values(outputs)) {
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 (!this.isTriggerNode(node) && !reachable.has(node.name)) {
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 (!this.registry.isVersionSafe(node.type, node.typeVersion)) {
830
- this.warn(
831
- issues,
832
- 19,
833
- `Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
834
- node.id
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 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
924
- checkRule21(w, issues) {
1276
+ // Rule 24 (WARN): deprecated accessor syntax in expressions
1277
+ checkRule24(w, issues) {
925
1278
  if (!Array.isArray(w.nodes)) return;
926
- const webhooksNeedingResponse = w.nodes.filter((n) => {
927
- if (!n.type.includes("webhook")) return false;
928
- const params = n.parameters;
929
- return params?.responseMode === "responseNode";
930
- });
931
- if (webhooksNeedingResponse.length === 0) return;
932
- const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
933
- if (!hasRespondNode) {
934
- for (const wh of webhooksNeedingResponse) {
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
- 21,
938
- `Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
939
- wh.id
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 this.singleRequest(url, method, path, body);
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 = "/workflows?limit=250";
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=250&cursor=${response.nextCursor}`;
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 = "/tags?limit=250";
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=250&cursor=${response.nextCursor}`;
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
- - Only reference credentials with exact type names (see catalog below)
1213
- - If credential ID is unknown, OMIT the credentials block entirely \u2014 never invent credential IDs
1214
- - Never put API keys or tokens in parameters when a credential type exists
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: 23 }, (_, i) => i + 1);
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: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
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
- constructor(patternsPath) {
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 (mode === "reference" && matches.length > 0) {
1463
- const refText = matches.slice(0, 3).map((m) => {
1464
- const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1465
- return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
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
- }).join("\n\n");
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: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
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
- } else {
1488
- blocks.push({
1489
- type: "text",
1490
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
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
- if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1497
- const hint = matches[0];
1498
- const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1499
- blocks.push({
1500
- type: "text",
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
- return this.getActivePatterns().map((p) => p.rule);
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
- return [...regressed, ...confirmed, ...drafts].slice(0, MAX_WARNED);
1531
- }
1532
- buildFailureWarnings(matches, globalFailureRates) {
1533
- const richPatterns = this.getActivePatterns();
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
- lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}`);
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
- if (!buildSessions.has(event.sessionId)) continue;
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(event.sessionId);
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
- if (version === PATTERN_SCHEMA_VERSION) return patterns;
1759
- return this.migratePatterns(patterns, version);
2561
+ this._cachedPreviousPatterns = version === PATTERN_SCHEMA_VERSION ? patterns : this.migratePatterns(patterns, version);
1760
2562
  } catch {
1761
- return [];
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
- ...p,
1777
- scoringFactors: {
1778
- ...p.scoringFactors,
1779
- stickinessBoost: p.scoringFactors.stickinessBoost ?? p.scoringFactors["validationBoost"] ?? 0
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
- const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [] };
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(a.sessionId);
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 list = sessions.get(a.sessionId) ?? [];
2671
+ const buildId = a.runId ?? a.sessionId;
2672
+ const list = sessions.get(buildId) ?? [];
1860
2673
  list.push(a);
1861
- sessions.set(a.sessionId, list);
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
- return {
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-23)`
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 import_node_path6 = require("path");
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, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
2232
- var pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path6.join)(__dirname, "..", "package.json"), "utf-8"));
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 validator = new N8nValidator();
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 Error("N8N_BASE_URL and N8N_API_KEY environment variables are required for n8n operations");
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
- validator = new N8nValidator(lastSync.registry);
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 autoSync();
2299
- const matches = await library.search(description);
2300
- const telemetryReader = getTelemetryReader();
2301
- const failureRates = await telemetryReader?.getFailureRates() ?? [];
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
- content: [{
2307
- type: "text",
2308
- text: JSON.stringify({
2309
- mode: built.mode,
2310
- matchCount: matches.length,
2311
- topMatchScore: matches[0]?.score ?? null,
2312
- nodeCatalog: syncResult ? "synced" : "static",
2313
- nodeCount: syncResult?.nodeCount ?? null,
2314
- ...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." },
2315
- systemPrompt: systemText,
2316
- userMessage: built.userMessage,
2317
- outputFormat: {
2318
- description: "Generate a JSON object with this exact structure. The workflow field contains the n8n workflow. credentialsNeeded lists services requiring credentials.",
2319
- schema: {
2320
- workflow: {
2321
- name: "string \u2014 descriptive workflow name",
2322
- nodes: "array \u2014 n8n node objects with id (UUID v4), type, typeVersion, name, position, parameters",
2323
- connections: "object \u2014 keyed by source node NAME, maps to target nodes",
2324
- settings: 'object \u2014 include executionOrder: "v1"'
2325
- },
2326
- credentialsNeeded: [{
2327
- service: 'string \u2014 e.g. "Slack"',
2328
- credentialType: 'string \u2014 e.g. "slackOAuth2Api"',
2329
- description: "string \u2014 what the user needs to set up"
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 23 structural rules. Returns pass/fail with specific issues. If validation fails, fix the issues and call this again. Errors block deployment; warnings are advisory.",
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 = validator.validate(parsed);
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
- return {
2363
- content: [{
2364
- type: "text",
2365
- text: JSON.stringify({
2366
- valid: result.valid,
2367
- errorCount: errors.length,
2368
- warningCount: warnings.length,
2369
- errors: errors.map((i) => ({
2370
- rule: i.rule,
2371
- message: i.message,
2372
- nodeId: i.nodeId ?? null
2373
- })),
2374
- warnings: warnings.map((i) => ({
2375
- rule: i.rule,
2376
- message: i.message,
2377
- nodeId: i.nodeId ?? null
2378
- })),
2379
- deployable: errors.length === 0
2380
- }, null, 2)
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 = validator.validate(parsed);
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
- content: [{
2418
- type: "text",
2419
- text: JSON.stringify({
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
- content: [{
2433
- type: "text",
2434
- text: JSON.stringify({
2435
- workflowId: response.id,
2436
- name: response.name,
2437
- activated: false,
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
- return {
2453
- content: [{
2454
- type: "text",
2455
- text: JSON.stringify({
2456
- workflowId: response.id,
2457
- name: response.name,
2458
- activated: activate,
2459
- url: `${process.env["N8N_BASE_URL"]}/workflow/${response.id}`
2460
- }, null, 2)
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
- content: [{
2477
- type: "text",
2478
- text: JSON.stringify(
2479
- matches.slice(0, limit).map((m) => ({
2480
- score: Number(m.score.toFixed(3)),
2481
- mode: m.mode,
2482
- description: m.workflow.description,
2483
- nodeCount: m.workflow.workflow.nodes.length,
2484
- nodes: m.workflow.workflow.nodes.map((n) => n.name),
2485
- failurePatterns: m.workflow.failurePatterns ?? []
2486
- })),
2487
- null,
2488
- 2
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
- content: [{
2523
- type: "text",
2524
- text: JSON.stringify({
2525
- synced: true,
2526
- nodeCount: result.nodeCount,
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
- content: [{
2549
- type: "text",
2550
- text: JSON.stringify(analysis, null, 2)
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
  }