@kairos-sdk/core 0.3.2 → 0.4.5

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.
@@ -253,14 +253,28 @@ function buildSearchCorpus(w) {
253
253
  return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
254
254
  }
255
255
  var MAX_LIBRARY_SIZE = 500;
256
+ function isValidMeta(item) {
257
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
258
+ }
259
+ function isValidOldEntry(item) {
260
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
261
+ item.workflow.nodes
262
+ );
263
+ }
256
264
  var FileLibrary = class {
257
265
  dir;
258
- workflows = [];
266
+ meta = [];
259
267
  initPromise = null;
260
268
  writeQueue = Promise.resolve();
261
269
  constructor(dir) {
262
270
  this.dir = dir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "library");
263
271
  }
272
+ get workflowsDir() {
273
+ return (0, import_node_path.join)(this.dir, "workflows");
274
+ }
275
+ workflowFilePath(id) {
276
+ return (0, import_node_path.join)(this.workflowsDir, `${id}.json`);
277
+ }
264
278
  async initialize() {
265
279
  if (!this.initPromise) {
266
280
  this.initPromise = this.doInitialize();
@@ -270,60 +284,147 @@ var FileLibrary = class {
270
284
  async doInitialize() {
271
285
  await (0, import_promises.mkdir)(this.dir, { recursive: true });
272
286
  const indexPath = (0, import_node_path.join)(this.dir, "index.json");
287
+ let workflowsDirExists = false;
273
288
  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
- );
289
+ await (0, import_promises.stat)(this.workflowsDir);
290
+ workflowsDirExists = true;
291
+ } catch {
292
+ }
293
+ if (workflowsDirExists) {
294
+ try {
295
+ const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
296
+ const parsed = JSON.parse(raw);
297
+ if (Array.isArray(parsed)) {
298
+ this.meta = parsed.filter(isValidMeta);
299
+ }
300
+ } catch {
301
+ this.meta = [];
282
302
  }
303
+ } else {
304
+ try {
305
+ const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
306
+ const parsed = JSON.parse(raw);
307
+ if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
308
+ await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
309
+ return;
310
+ }
311
+ } catch {
312
+ }
313
+ this.meta = [];
314
+ await (0, import_promises.mkdir)(this.workflowsDir, { recursive: true });
315
+ }
316
+ }
317
+ /**
318
+ * One-time transparent migration from v0.4.x monolithic index.json.
319
+ * Splits each stored workflow into a per-file workflow JSON and a lightweight
320
+ * meta entry. Rewrites index.json in the new format.
321
+ */
322
+ async migrateFromMonolithic(oldEntries) {
323
+ await (0, import_promises.mkdir)(this.workflowsDir, { recursive: true });
324
+ const newMeta = [];
325
+ for (const entry of oldEntries) {
326
+ const wfPath = this.workflowFilePath(entry.id);
327
+ const tmpPath = `${wfPath}.tmp`;
328
+ await (0, import_promises.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
329
+ await (0, import_promises.rename)(tmpPath, wfPath);
330
+ const { workflow, ...metaFields } = entry;
331
+ newMeta.push({
332
+ ...metaFields,
333
+ workflowName: workflow.name,
334
+ cachedNodeTypes: workflow.nodes.map((n) => n.type)
335
+ });
336
+ }
337
+ this.meta = newMeta;
338
+ await this.persistNow();
339
+ }
340
+ async loadWorkflowFile(id) {
341
+ try {
342
+ const raw = await (0, import_promises.readFile)(this.workflowFilePath(id), "utf-8");
343
+ return JSON.parse(raw);
283
344
  } catch {
284
- this.workflows = [];
345
+ return null;
285
346
  }
286
347
  }
348
+ async writeWorkflowFile(id, workflow) {
349
+ const wfPath = this.workflowFilePath(id);
350
+ const tmpPath = `${wfPath}.tmp`;
351
+ await (0, import_promises.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
352
+ await (0, import_promises.rename)(tmpPath, wfPath);
353
+ }
354
+ /**
355
+ * Build a lightweight StoredWorkflow shell from a meta entry for use in
356
+ * scoring / clustering. Only node.type is populated in each node — no other
357
+ * node fields are used by hybridScore or clusterWorkflows.
358
+ */
359
+ makeSearchShell(m) {
360
+ return {
361
+ ...m,
362
+ workflow: {
363
+ name: m.workflowName,
364
+ nodes: m.cachedNodeTypes.map((type) => ({
365
+ id: "",
366
+ name: "",
367
+ type,
368
+ typeVersion: 1,
369
+ position: [0, 0],
370
+ parameters: {}
371
+ })),
372
+ connections: {}
373
+ }
374
+ };
375
+ }
287
376
  async search(description, options) {
288
- const searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
289
- if (searchable.length === 0) return [];
377
+ const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
378
+ if (filteredMeta.length === 0) return [];
290
379
  const limit = options?.limit ?? 3;
291
380
  const queryTokens = tokenize(description);
292
381
  if (queryTokens.length === 0) return [];
293
- const docTokenArrays = searchable.map((w) => tokenize(buildSearchCorpus(w)));
382
+ const shells = filteredMeta.map((m) => this.makeSearchShell(m));
383
+ const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
294
384
  const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
295
- const docCount = searchable.length;
385
+ const docCount = shells.length;
296
386
  const idf = /* @__PURE__ */ new Map();
297
387
  const allTokens = new Set(queryTokens);
298
388
  for (const token of allTokens) {
299
389
  const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
300
390
  idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
301
391
  }
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);
392
+ const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
393
+ const clusters = clusterWorkflows(shells);
304
394
  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;
395
+ if (reranked.length === 0) return [];
396
+ for (const r of reranked) {
397
+ const m = this.meta.find((m2) => m2.id === r.workflow.id);
398
+ if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
399
+ }
400
+ this.persist();
401
+ const results = await Promise.all(
402
+ reranked.map(async (r) => {
403
+ const m = this.meta.find((meta) => meta.id === r.workflow.id);
404
+ const workflow = await this.loadWorkflowFile(r.workflow.id);
405
+ if (!workflow) return null;
406
+ return {
407
+ workflow: { ...m, workflow },
408
+ score: r.score,
409
+ mode: scoreToMode(r.score)
410
+ };
411
+ })
412
+ );
413
+ return results.filter((r) => r !== null);
315
414
  }
316
415
  async save(workflow, metadata) {
317
416
  const id = generateUUID();
417
+ await this.writeWorkflowFile(id, workflow);
318
418
  const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
319
- const stored = {
419
+ const meta = {
320
420
  id,
321
- workflow,
322
421
  description: metadata.description,
323
422
  tags: metadata.tags ?? [],
324
423
  platform: metadata.platform ?? "n8n",
325
424
  deployCount: 0,
326
425
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
426
+ workflowName: workflow.name,
427
+ cachedNodeTypes: workflow.nodes.map((n) => n.type),
327
428
  ...failurePatterns?.length ? { failurePatterns } : {},
328
429
  ...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
329
430
  ...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
@@ -335,31 +436,35 @@ var FileLibrary = class {
335
436
  ...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
336
437
  ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
337
438
  };
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);
439
+ this.meta.push(meta);
440
+ if (this.meta.length > MAX_LIBRARY_SIZE) {
441
+ this.meta.sort((a, b) => {
442
+ if (a.id === id) return -1;
443
+ if (b.id === id) return 1;
444
+ return (b.deployCount ?? 0) - (a.deployCount ?? 0);
445
+ });
446
+ this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
342
447
  }
343
448
  await this.persist();
344
449
  return id;
345
450
  }
346
451
  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();
452
+ const m = this.meta.find((m2) => m2.id === id);
453
+ if (m) {
454
+ m.deployCount++;
455
+ m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
351
456
  await this.persist();
352
457
  }
353
458
  }
354
459
  async recordOutcome(id, outcome) {
355
- const w = this.workflows.find((w2) => w2.id === id);
356
- if (!w) return;
460
+ const m = this.meta.find((m2) => m2.id === id);
461
+ if (!m) return;
357
462
  if (outcome.mode === "direct") {
358
- w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
463
+ m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
359
464
  } else {
360
- w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
465
+ m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
361
466
  }
362
- const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
467
+ const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
363
468
  stats.totalUses++;
364
469
  stats.totalAttempts += outcome.attempts;
365
470
  if (outcome.firstTryPass) stats.firstTryPasses++;
@@ -367,24 +472,35 @@ var FileLibrary = class {
367
472
  const key = String(rule);
368
473
  stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
369
474
  }
370
- w.outcomeStats = stats;
475
+ m.outcomeStats = stats;
371
476
  await this.persist();
372
477
  }
373
478
  async drain() {
374
479
  await this.writeQueue;
375
480
  }
376
481
  async get(id) {
377
- return this.workflows.find((w) => w.id === id) ?? null;
482
+ const m = this.meta.find((m2) => m2.id === id);
483
+ if (!m) return null;
484
+ const workflow = await this.loadWorkflowFile(id);
485
+ if (!workflow) return null;
486
+ return { ...m, workflow };
378
487
  }
379
488
  async list(filters) {
380
- let result = this.workflows;
489
+ let filtered = this.meta;
381
490
  if (filters?.platform) {
382
- result = result.filter((w) => w.platform === filters.platform);
491
+ filtered = filtered.filter((m) => m.platform === filters.platform);
383
492
  }
384
493
  if (filters?.tags && filters.tags.length > 0) {
385
- result = result.filter((w) => filters.tags.some((t) => w.tags.includes(t)));
386
- }
387
- return result;
494
+ filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
495
+ }
496
+ const results = await Promise.all(
497
+ filtered.map(async (m) => {
498
+ const workflow = await this.loadWorkflowFile(m.id);
499
+ if (!workflow) return null;
500
+ return { ...m, workflow };
501
+ })
502
+ );
503
+ return results.filter((r) => r !== null);
388
504
  }
389
505
  deduplicateFailurePatterns(patterns) {
390
506
  if (!patterns?.length) return void 0;
@@ -399,11 +515,36 @@ var FileLibrary = class {
399
515
  }
400
516
  return [...map.values()];
401
517
  }
518
+ /**
519
+ * Direct write used only during migration (before writeQueue is needed).
520
+ */
521
+ async persistNow() {
522
+ const indexPath = (0, import_node_path.join)(this.dir, "index.json");
523
+ const tmpPath = `${indexPath}.tmp`;
524
+ await (0, import_promises.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
525
+ await (0, import_promises.rename)(tmpPath, indexPath);
526
+ }
402
527
  persist() {
403
528
  this.writeQueue = this.writeQueue.then(async () => {
404
529
  const indexPath = (0, import_node_path.join)(this.dir, "index.json");
530
+ let onDisk = [];
531
+ try {
532
+ const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
533
+ const parsed = JSON.parse(raw);
534
+ if (Array.isArray(parsed)) {
535
+ onDisk = parsed.filter(isValidMeta);
536
+ }
537
+ } catch {
538
+ }
539
+ const ourIds = new Set(this.meta.map((m) => m.id));
540
+ const external = onDisk.filter((m) => !ourIds.has(m.id));
541
+ let merged = [...this.meta, ...external];
542
+ if (merged.length > MAX_LIBRARY_SIZE) {
543
+ merged.sort((a, b) => (b.deployCount ?? 0) - (a.deployCount ?? 0));
544
+ merged = merged.slice(0, MAX_LIBRARY_SIZE);
545
+ }
405
546
  const tmpPath = `${indexPath}.tmp`;
406
- await (0, import_promises.writeFile)(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
547
+ await (0, import_promises.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
407
548
  await (0, import_promises.rename)(tmpPath, indexPath);
408
549
  });
409
550
  return this.writeQueue;
@@ -577,17 +718,31 @@ var N8nValidator = class {
577
718
  this.checkRule21(workflow, issues);
578
719
  this.checkRule22(workflow, issues);
579
720
  this.checkRule23(workflow, issues);
721
+ this.checkRule24(workflow, issues);
722
+ this.checkRule25(workflow, issues);
723
+ this.checkRule26(workflow, issues);
724
+ if (Array.isArray(workflow.nodes)) {
725
+ const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
726
+ for (const issue of issues) {
727
+ if (issue.nodeId && !issue.nodeType) {
728
+ const nt = nodeById.get(issue.nodeId);
729
+ if (nt) issue.nodeType = nt;
730
+ }
731
+ }
732
+ }
580
733
  const errors = issues.filter((i) => i.severity === "error");
581
734
  return { valid: errors.length === 0, issues };
582
735
  }
583
- err(issues, rule, message, nodeId) {
736
+ err(issues, rule, message, nodeId, nodeType) {
584
737
  const issue = { rule, severity: "error", message };
585
738
  if (nodeId !== void 0) issue.nodeId = nodeId;
739
+ if (nodeType !== void 0) issue.nodeType = nodeType;
586
740
  issues.push(issue);
587
741
  }
588
- warn(issues, rule, message, nodeId) {
742
+ warn(issues, rule, message, nodeId, nodeType) {
589
743
  const issue = { rule, severity: "warn", message };
590
744
  if (nodeId !== void 0) issue.nodeId = nodeId;
745
+ if (nodeType !== void 0) issue.nodeType = nodeType;
591
746
  issues.push(issue);
592
747
  }
593
748
  isTriggerNode(node) {
@@ -698,10 +853,14 @@ var N8nValidator = class {
698
853
  checkRule11(w, issues) {
699
854
  if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
700
855
  const reachable = /* @__PURE__ */ new Set();
701
- for (const [, outputs] of Object.entries(w.connections)) {
856
+ const aiSubNodeSources = /* @__PURE__ */ new Set();
857
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
702
858
  if (typeof outputs !== "object" || outputs === null) continue;
703
- for (const portGroup of Object.values(outputs)) {
859
+ let hasAiPort = false;
860
+ for (const [portName, portGroup] of Object.entries(outputs)) {
704
861
  if (!Array.isArray(portGroup)) continue;
862
+ const isAiPort = portName.startsWith("ai_");
863
+ if (isAiPort) hasAiPort = true;
705
864
  for (const targets of portGroup) {
706
865
  if (!Array.isArray(targets)) continue;
707
866
  for (const target of targets) {
@@ -710,10 +869,13 @@ var N8nValidator = class {
710
869
  }
711
870
  }
712
871
  }
872
+ if (hasAiPort) aiSubNodeSources.add(sourceName);
713
873
  }
714
874
  for (const node of w.nodes) {
715
875
  if (node.type.includes("stickyNote")) continue;
716
- if (!this.isTriggerNode(node) && !reachable.has(node.name)) {
876
+ if (this.isTriggerNode(node)) continue;
877
+ if (aiSubNodeSources.has(node.name)) continue;
878
+ if (!reachable.has(node.name)) {
717
879
  this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
718
880
  }
719
881
  }
@@ -909,6 +1071,76 @@ var N8nValidator = class {
909
1071
  }
910
1072
  }
911
1073
  }
1074
+ // Rule 24 (WARN): deprecated accessor syntax in expressions
1075
+ checkRule24(w, issues) {
1076
+ if (!Array.isArray(w.nodes)) return;
1077
+ const deprecated = /\$node\s*\[/;
1078
+ for (const node of w.nodes) {
1079
+ for (const expr of this.extractExpressions(node.parameters)) {
1080
+ if (deprecated.test(expr)) {
1081
+ this.warn(
1082
+ issues,
1083
+ 24,
1084
+ `Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
1085
+ node.id
1086
+ );
1087
+ break;
1088
+ }
1089
+ }
1090
+ }
1091
+ }
1092
+ // Rule 25 (WARN): wrong item index assumptions in expressions
1093
+ checkRule25(w, issues) {
1094
+ if (!Array.isArray(w.nodes)) return;
1095
+ const itemIndex = /\$json\s*\.\s*items\s*\[/;
1096
+ for (const node of w.nodes) {
1097
+ for (const expr of this.extractExpressions(node.parameters)) {
1098
+ if (itemIndex.test(expr)) {
1099
+ this.warn(
1100
+ issues,
1101
+ 25,
1102
+ `Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
1103
+ node.id
1104
+ );
1105
+ break;
1106
+ }
1107
+ }
1108
+ }
1109
+ }
1110
+ // Rule 26 (WARN): missing .first() or .all() on node references
1111
+ checkRule26(w, issues) {
1112
+ if (!Array.isArray(w.nodes)) return;
1113
+ const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
1114
+ for (const node of w.nodes) {
1115
+ for (const expr of this.extractExpressions(node.parameters)) {
1116
+ if (bareRef.test(expr)) {
1117
+ this.warn(
1118
+ issues,
1119
+ 26,
1120
+ `Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
1121
+ node.id
1122
+ );
1123
+ break;
1124
+ }
1125
+ }
1126
+ }
1127
+ }
1128
+ extractExpressions(params) {
1129
+ const expressions = [];
1130
+ const walk = (val) => {
1131
+ if (typeof val === "string") {
1132
+ if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
1133
+ expressions.push(val);
1134
+ }
1135
+ } else if (Array.isArray(val)) {
1136
+ for (const item of val) walk(item);
1137
+ } else if (val !== null && typeof val === "object") {
1138
+ for (const v of Object.values(val)) walk(v);
1139
+ }
1140
+ };
1141
+ walk(params);
1142
+ return expressions;
1143
+ }
912
1144
  // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
913
1145
  checkRule21(w, issues) {
914
1146
  if (!Array.isArray(w.nodes)) return;
@@ -1164,6 +1396,11 @@ var N8nApiClient = class {
1164
1396
  }
1165
1397
  };
1166
1398
 
1399
+ // src/generation/prompt-builder.ts
1400
+ var import_node_fs = require("fs");
1401
+ var import_node_path2 = require("path");
1402
+ var import_node_os2 = require("os");
1403
+
1167
1404
  // src/generation/prompts/v1.ts
1168
1405
  var SYSTEM_PROMPT_V1 = `You are a workflow generation engine for n8n. Your only output is a generate_workflow tool call containing valid n8n workflow JSON. You never respond with prose, explanations, or markdown. If you cannot fulfill the request, set the error field in the tool call.
1169
1406
 
@@ -1193,9 +1430,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
1193
1430
  - Never reuse IDs, never use sequential fake IDs like "node-1"
1194
1431
 
1195
1432
  ### Credentials:
1196
- - Only reference credentials with exact type names (see catalog below)
1197
- - If credential ID is unknown, OMIT the credentials block entirely \u2014 never invent credential IDs
1198
- - Never put API keys or tokens in parameters when a credential type exists
1433
+ - Each credential is keyed by its type string, with an object value containing id and name:
1434
+ "credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack Credential" } }
1435
+ - Use "placeholder-id" as the id \u2014 users replace this with their real credential ID from n8n after deployment
1436
+ - The credentialsNeeded field in your response declares what credentials the user must configure
1437
+ - Never put API keys or tokens directly in node parameters when a credential type exists
1199
1438
 
1200
1439
  ### Node names:
1201
1440
  - All node names must be unique within the workflow
@@ -1242,6 +1481,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
1242
1481
 
1243
1482
  ---
1244
1483
 
1484
+ ## EXPRESSION SYNTAX \u2014 how to reference upstream node data
1485
+
1486
+ ### Accessing a field from an upstream node:
1487
+ - CORRECT: $('NodeName').item.json.field
1488
+ - WRONG: $node["NodeName"].json.field \u2190 deprecated accessor, fails at runtime (Rule 24)
1489
+
1490
+ ### Accessing array items from $json:
1491
+ - CORRECT: $json.field \u2190 n8n auto-flattens items; each item is already a flat object
1492
+ - WRONG: $json.items[0].field \u2190 do not index into items[] (Rule 25)
1493
+
1494
+ ### Calling node data \u2014 always qualify with .first() or .all():
1495
+ - CORRECT: $('NodeName').first().json.field \u2190 single item
1496
+ - CORRECT: $('NodeName').all() \u2190 array of all items
1497
+ - WRONG: $('NodeName').json \u2190 throws at runtime without .first() or .all() (Rule 26)
1498
+
1499
+ ---
1500
+
1245
1501
  ## NODE CATALOG \u2014 exact type strings and safe typeVersions
1246
1502
 
1247
1503
  ### Triggers (always at least one required):
@@ -1341,14 +1597,64 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
1341
1597
  5. At least one trigger node present
1342
1598
  6. Every AI Agent has an ai_languageModel sub-node
1343
1599
  7. settings block is complete with executionOrder: "v1"
1600
+ 8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
1601
+ 9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
1602
+ 10. No bare $('NodeName').json \u2014 always use .first().json.field or .all()
1344
1603
 
1345
1604
  ---
1346
1605
 
1347
1606
  Respond ONLY with a generate_workflow tool call. No prose. No markdown outside the tool call.
1348
1607
  If the request is impossible or unclear, set the error field instead of generating a workflow.`;
1349
1608
 
1350
- // src/generation/prompt-builder.ts
1351
- var RULE_REMEDIES = {
1609
+ // src/validation/rule-metadata.ts
1610
+ var VALIDATOR_RULE_IDS = Array.from({ length: 26 }, (_, i) => i + 1);
1611
+ var RULE_PIPELINE_STAGES = {
1612
+ 1: "node_generation",
1613
+ 2: "node_generation",
1614
+ 3: "node_generation",
1615
+ 4: "node_generation",
1616
+ 5: "node_generation",
1617
+ 6: "node_generation",
1618
+ 7: "node_generation",
1619
+ 8: "node_generation",
1620
+ 9: "connection_wiring",
1621
+ 10: "connection_wiring",
1622
+ 11: "connection_wiring",
1623
+ 12: "workflow_structure",
1624
+ 13: "node_generation",
1625
+ 14: "workflow_structure",
1626
+ 15: "node_generation",
1627
+ 16: "node_generation",
1628
+ 17: "credential_injection",
1629
+ 18: "connection_wiring",
1630
+ 19: "node_generation",
1631
+ 20: "connection_wiring",
1632
+ 21: "workflow_structure",
1633
+ 22: "workflow_structure",
1634
+ 23: "node_generation",
1635
+ 24: "expression_syntax",
1636
+ 25: "expression_syntax",
1637
+ 26: "expression_syntax"
1638
+ };
1639
+ var RULE_EXAMPLES = {
1640
+ 17: {
1641
+ bad: '"credentials": { "slackOAuth2Api": "my-token" }',
1642
+ good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
1643
+ },
1644
+ 24: {
1645
+ bad: '$node["Fetch Data"].json.email',
1646
+ good: "$('Fetch Data').item.json.email"
1647
+ },
1648
+ 25: {
1649
+ bad: "$json.items[0].email",
1650
+ good: "$json.email"
1651
+ },
1652
+ 26: {
1653
+ bad: "$('Fetch Data').json.email",
1654
+ good: "$('Fetch Data').first().json.email"
1655
+ }
1656
+ };
1657
+ var RULE_MITIGATIONS = {
1352
1658
  1: "Provide a non-empty workflow name string",
1353
1659
  2: "Include at least one node in the nodes array",
1354
1660
  3: "Every node must have a unique UUID v4 string as its id field",
@@ -1359,18 +1665,51 @@ var RULE_REMEDIES = {
1359
1665
  8: "Every node must have a non-empty name string",
1360
1666
  9: "connections must be a plain object (use {} if no connections)",
1361
1667
  10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
1668
+ 11: "Every non-trigger node should have at least one incoming connection",
1362
1669
  12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
1363
- 14: "Include at least one trigger node (e.g. webhook, scheduleTrigger, manualTrigger)",
1670
+ 13: "workflow.settings must be a plain object if present",
1671
+ 14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
1364
1672
  15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1365
1673
  16: "All node names must be unique within the workflow",
1366
- 17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
1674
+ 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',
1367
1675
  18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
1368
1676
  19: "Use known safe typeVersion values for each node type",
1369
1677
  20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1370
1678
  21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1371
- 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
1679
+ 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
1680
+ 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
1681
+ 24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
1682
+ 25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
1683
+ 26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
1372
1684
  };
1685
+
1686
+ // src/generation/prompt-builder.ts
1687
+ var CRITICAL_SCORE_THRESHOLD = 0.15;
1688
+ function resolveProfile() {
1689
+ const env = process.env["KAIROS_PROMPT_PROFILE"];
1690
+ if (env === "minimal" || env === "standard" || env === "rich") return env;
1691
+ return "standard";
1692
+ }
1693
+ var PROACTIVE_EXPRESSION_GUIDANCE = `## Expression Syntax Quick Reference
1694
+
1695
+ Always use these patterns in expressions:
1696
+ - Access node data: $('NodeName').item.json.field (not $node["NodeName"].json)
1697
+ - Access JSON field: $json.field (not $json.items[0].field)
1698
+ - Single item: $('NodeName').first().json.field
1699
+ - All items: $('NodeName').all()`;
1373
1700
  var PromptBuilder = class {
1701
+ patternsPath;
1702
+ profile;
1703
+ _lastActivePatterns = null;
1704
+ constructor(patternsPath, profile) {
1705
+ this.patternsPath = patternsPath ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "patterns.json");
1706
+ this.profile = profile ?? resolveProfile();
1707
+ }
1708
+ resolveMaxPatterns() {
1709
+ if (this.profile === "minimal") return 3;
1710
+ if (this.profile === "rich") return 15;
1711
+ return 10;
1712
+ }
1374
1713
  build(request, matches, globalFailureRates = [], dynamicCatalog) {
1375
1714
  const mode = this.resolveMode(matches);
1376
1715
  const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
@@ -1407,69 +1746,178 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
1407
1746
  cache_control: { type: "ephemeral" }
1408
1747
  }
1409
1748
  ];
1410
- if (mode === "reference" && matches.length > 0) {
1411
- const refText = matches.slice(0, 3).map((m) => {
1412
- const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1413
- return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1749
+ if (this.profile !== "minimal") {
1750
+ if (mode === "reference" && matches.length > 0) {
1751
+ const refText = matches.slice(0, 3).map((m) => {
1752
+ const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1753
+ return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1414
1754
  Nodes:
1415
1755
  ${nodes}`;
1416
- }).join("\n\n");
1417
- blocks.push({
1418
- type: "text",
1419
- text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1420
-
1421
- ${refText}`
1422
- });
1423
- }
1424
- if (mode === "direct" && matches[0]) {
1425
- const match = matches[0];
1426
- const json = JSON.stringify(match.workflow.workflow, null, 2);
1427
- if (json.length > 3e4) {
1428
- const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1756
+ }).join("\n\n");
1429
1757
  blocks.push({
1430
1758
  type: "text",
1431
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1759
+ text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1760
+
1761
+ ${refText}`
1762
+ });
1763
+ }
1764
+ if (mode === "direct" && matches[0]) {
1765
+ const match = matches[0];
1766
+ const json = JSON.stringify(match.workflow.workflow, null, 2);
1767
+ if (json.length > 3e4) {
1768
+ const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1769
+ blocks.push({
1770
+ type: "text",
1771
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1432
1772
  Nodes:
1433
1773
  ${nodes}`
1434
- });
1435
- } else {
1436
- blocks.push({
1437
- type: "text",
1438
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1774
+ });
1775
+ } else {
1776
+ blocks.push({
1777
+ type: "text",
1778
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1439
1779
 
1440
1780
  ${json}`
1441
- });
1781
+ });
1782
+ }
1442
1783
  }
1443
- }
1444
- if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1445
- const hint = matches[0];
1446
- const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1447
- blocks.push({
1448
- type: "text",
1449
- text: `## Weak Structural Hint
1784
+ if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1785
+ const hint = matches[0];
1786
+ const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1787
+ blocks.push({
1788
+ type: "text",
1789
+ text: `## Weak Structural Hint
1450
1790
  A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
1451
- });
1791
+ });
1792
+ }
1452
1793
  }
1453
1794
  const warnings = this.buildFailureWarnings(matches, globalFailureRates);
1454
1795
  if (warnings) {
1455
1796
  blocks.push({ type: "text", text: warnings });
1456
1797
  }
1798
+ if (this.profile === "rich") {
1799
+ const expressionRules = /* @__PURE__ */ new Set([24, 25, 26]);
1800
+ const expressionAlreadyCovered = (this._lastActivePatterns ?? []).some((p) => expressionRules.has(p.rule));
1801
+ if (!expressionAlreadyCovered) {
1802
+ blocks.push({ type: "text", text: PROACTIVE_EXPRESSION_GUIDANCE });
1803
+ }
1804
+ }
1457
1805
  return blocks;
1458
1806
  }
1807
+ loadPatterns() {
1808
+ try {
1809
+ const raw = (0, import_node_fs.readFileSync)(this.patternsPath, "utf-8");
1810
+ const analysis = JSON.parse(raw);
1811
+ const patterns = analysis.topFailureRules ?? [];
1812
+ return patterns.filter((p) => typeof p.pipelineStage === "string" && typeof p.state === "string");
1813
+ } catch {
1814
+ return [];
1815
+ }
1816
+ }
1817
+ getWarnedRules() {
1818
+ const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
1819
+ return patterns.map((p) => p.rule);
1820
+ }
1821
+ getActivePatterns(maxCount = 10) {
1822
+ const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
1823
+ const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
1824
+ const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1825
+ const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1826
+ return [...regressed, ...confirmed, ...drafts].slice(0, maxCount);
1827
+ }
1459
1828
  buildFailureWarnings(matches, globalFailureRates) {
1829
+ const richPatterns = this.getActivePatterns(this.resolveMaxPatterns());
1830
+ this._lastActivePatterns = richPatterns;
1831
+ if (richPatterns.length > 0) {
1832
+ return this.buildStageGroupedWarnings(richPatterns, matches);
1833
+ }
1834
+ return this.buildLegacyWarnings(matches, globalFailureRates);
1835
+ }
1836
+ buildStageGroupedWarnings(patterns, matches) {
1837
+ const stageLabels = {
1838
+ credential_injection: "CREDENTIAL FORMATTING",
1839
+ connection_wiring: "CONNECTION WIRING",
1840
+ node_generation: "NODE GENERATION",
1841
+ workflow_structure: "WORKFLOW STRUCTURE",
1842
+ expression_syntax: "EXPRESSION SYNTAX"
1843
+ };
1844
+ const byStage = /* @__PURE__ */ new Map();
1845
+ for (const p of patterns) {
1846
+ const list = byStage.get(p.pipelineStage) ?? [];
1847
+ list.push(p);
1848
+ byStage.set(p.pipelineStage, list);
1849
+ }
1850
+ const sections = [];
1851
+ for (const [stage, stagePatterns] of byStage) {
1852
+ const label = stageLabels[stage] ?? stage;
1853
+ const byMitigation = /* @__PURE__ */ new Map();
1854
+ for (const p of stagePatterns) {
1855
+ const key = p.mitigation ?? `rule_${p.rule}`;
1856
+ const list = byMitigation.get(key) ?? [];
1857
+ list.push(p);
1858
+ byMitigation.set(key, list);
1859
+ }
1860
+ const lines = [];
1861
+ for (const group of byMitigation.values()) {
1862
+ if (group.length === 1) {
1863
+ const p = group[0];
1864
+ const urgency = p.regressed ? "CRITICAL REGRESSION: " : (p.compositeScore ?? 0) >= CRITICAL_SCORE_THRESHOLD ? "CRITICAL: " : "";
1865
+ const statePrefix = p.state === "confirmed" ? "[CONFIRMED] " : "";
1866
+ const trendSuffix = p.trend === "worsening" ? " (GETTING WORSE)" : p.trend === "improving" ? " (improving)" : "";
1867
+ const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
1868
+ const remedyStr = remedy ? `
1869
+ Fix: ${remedy}` : "";
1870
+ const ex = RULE_EXAMPLES[p.rule];
1871
+ const exampleStr = ex ? `
1872
+ Bad: ${ex.bad}
1873
+ Good: ${ex.good}` : "";
1874
+ lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}${exampleStr}`);
1875
+ } else {
1876
+ const ruleNums = group.map((p) => p.rule).join(", ");
1877
+ const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
1878
+ const hasConfirmed = group.some((p) => p.state === "confirmed");
1879
+ const statePrefix = hasConfirmed ? "[CONFIRMED] " : "";
1880
+ const remedy = group[0].mitigation;
1881
+ const remedyStr = remedy ? `
1882
+ Fix: ${remedy}` : "";
1883
+ lines.push(`- ${statePrefix}Rules ${ruleNums} (${totalFailures} failures combined): same root cause${remedyStr}`);
1884
+ }
1885
+ }
1886
+ sections.push(`### ${label}
1887
+ ${lines.join("\n")}`);
1888
+ }
1889
+ for (const match of matches) {
1890
+ const fps = match.workflow.failurePatterns;
1891
+ if (!fps?.length) continue;
1892
+ const coveredRules = new Set(patterns.map((p) => p.rule));
1893
+ const extra = fps.filter((fp) => !coveredRules.has(fp.rule));
1894
+ for (const fp of extra) {
1895
+ const remedy = RULE_MITIGATIONS[fp.rule];
1896
+ const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1897
+ sections.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen in similar workflows)`);
1898
+ }
1899
+ }
1900
+ if (sections.length === 0) return null;
1901
+ return `## Known Failure Patterns \u2014 AVOID THESE
1902
+
1903
+ Grouped by generation stage. Fix these BEFORE outputting your response:
1904
+
1905
+ ${sections.join("\n\n")}`;
1906
+ }
1907
+ buildLegacyWarnings(matches, globalFailureRates) {
1460
1908
  const lines = [];
1461
1909
  for (const match of matches) {
1462
1910
  const patterns = match.workflow.failurePatterns;
1463
1911
  if (!patterns?.length) continue;
1464
1912
  for (const fp of patterns) {
1465
- const remedy = RULE_REMEDIES[fp.rule];
1913
+ const remedy = RULE_MITIGATIONS[fp.rule];
1466
1914
  const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1467
1915
  lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
1468
1916
  }
1469
1917
  }
1470
1918
  const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
1471
1919
  for (const rule of highFreqRules) {
1472
- const remedy = RULE_REMEDIES[rule.rule];
1920
+ const remedy = RULE_MITIGATIONS[rule.rule];
1473
1921
  const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1474
1922
  lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
1475
1923
  }
@@ -1488,15 +1936,55 @@ Workflow name: "${request.name}"` : "";
1488
1936
  };
1489
1937
 
1490
1938
  // src/telemetry/reader.ts
1939
+ var import_node_os3 = require("os");
1940
+ var import_node_path4 = require("path");
1941
+
1942
+ // src/telemetry/event-reader.ts
1491
1943
  var import_promises2 = require("fs/promises");
1492
- var import_node_path2 = require("path");
1493
- var import_node_os2 = require("os");
1944
+ var import_node_fs2 = require("fs");
1945
+ var import_node_path3 = require("path");
1946
+ var import_node_readline = require("readline");
1947
+ async function readTelemetryEvents(dir, days) {
1948
+ let files;
1949
+ try {
1950
+ files = await (0, import_promises2.readdir)(dir);
1951
+ } catch {
1952
+ return [];
1953
+ }
1954
+ const cutoff = /* @__PURE__ */ new Date();
1955
+ cutoff.setDate(cutoff.getDate() - days);
1956
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
1957
+ const todayStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1958
+ const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1959
+ const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr && f <= `${todayStr}.jsonl`).sort();
1960
+ const events = [];
1961
+ for (const file of recentFiles) {
1962
+ const fileDate = file.replace(".jsonl", "");
1963
+ try {
1964
+ const rl = (0, import_node_readline.createInterface)({
1965
+ input: (0, import_node_fs2.createReadStream)((0, import_node_path3.join)(dir, file), "utf-8"),
1966
+ crlfDelay: Infinity
1967
+ });
1968
+ for await (const line of rl) {
1969
+ if (!line.trim()) continue;
1970
+ try {
1971
+ events.push({ ...JSON.parse(line), fileDate });
1972
+ } catch {
1973
+ }
1974
+ }
1975
+ } catch {
1976
+ }
1977
+ }
1978
+ return events;
1979
+ }
1980
+
1981
+ // src/telemetry/reader.ts
1494
1982
  var TelemetryReader = class {
1495
1983
  dir;
1496
1984
  cache = null;
1497
1985
  cacheTime = 0;
1498
1986
  constructor(dir) {
1499
- this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
1987
+ this.dir = dir ?? (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".kairos", "telemetry");
1500
1988
  }
1501
1989
  async getFailureRates(days = 30) {
1502
1990
  const now = Date.now();
@@ -1505,9 +1993,10 @@ var TelemetryReader = class {
1505
1993
  }
1506
1994
  const events = await this.readRecentEvents(days);
1507
1995
  const buildSessions = new Set(
1508
- events.filter((e) => e.eventType === "build_complete" && !e.data.dryRun).map((e) => e.sessionId)
1996
+ events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
1509
1997
  );
1510
- if (buildSessions.size === 0) return [];
1998
+ const MIN_BUILDS_FOR_RATES = 3;
1999
+ if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
1511
2000
  const ruleSessions = /* @__PURE__ */ new Map();
1512
2001
  for (const event of events) {
1513
2002
  if (event.eventType !== "generation_attempt") continue;
@@ -1545,32 +2034,487 @@ var TelemetryReader = class {
1545
2034
  return rates;
1546
2035
  }
1547
2036
  async readRecentEvents(days) {
1548
- let files;
2037
+ return readTelemetryEvents(this.dir, days);
2038
+ }
2039
+ };
2040
+
2041
+ // src/telemetry/pattern-analyzer.ts
2042
+ var import_promises3 = require("fs/promises");
2043
+ var import_node_path5 = require("path");
2044
+ var import_node_os4 = require("os");
2045
+ var PATTERN_SCHEMA_VERSION = 2;
2046
+ var PatternAnalyzer = class _PatternAnalyzer {
2047
+ telemetryDir;
2048
+ outputDir;
2049
+ _cachedEvents = null;
2050
+ constructor(telemetryDir) {
2051
+ const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
2052
+ this.telemetryDir = telemetryDir ?? defaultDir;
2053
+ this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
2054
+ }
2055
+ async loadPreviousPatterns() {
1549
2056
  try {
1550
- files = await (0, import_promises2.readdir)(this.dir);
2057
+ const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
2058
+ const prev = JSON.parse(raw);
2059
+ const version = prev.schemaVersion ?? 0;
2060
+ const patterns = prev.topFailureRules ?? [];
2061
+ if (version === PATTERN_SCHEMA_VERSION) return patterns;
2062
+ return this.migratePatterns(patterns, version);
1551
2063
  } catch {
1552
2064
  return [];
1553
2065
  }
1554
- const cutoff = /* @__PURE__ */ new Date();
1555
- cutoff.setDate(cutoff.getDate() - days);
1556
- const cutoffStr = cutoff.toISOString().slice(0, 10);
1557
- const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1558
- const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr).sort();
1559
- const events = [];
1560
- for (const file of recentFiles) {
1561
- try {
1562
- const content = await (0, import_promises2.readFile)((0, import_node_path2.join)(this.dir, file), "utf-8");
1563
- for (const line of content.split("\n")) {
1564
- if (!line.trim()) continue;
1565
- try {
1566
- events.push(JSON.parse(line));
1567
- } catch {
2066
+ }
2067
+ migratePatterns(patterns, fromVersion) {
2068
+ let migrated = patterns;
2069
+ if (fromVersion < 1) {
2070
+ migrated = migrated.map((p) => ({
2071
+ ...p,
2072
+ compositeScore: p.compositeScore ?? 0,
2073
+ scoringFactors: p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
2074
+ pipelineStage: p.pipelineStage ?? "node_generation"
2075
+ }));
2076
+ }
2077
+ if (fromVersion < 2) {
2078
+ migrated = migrated.map((p) => {
2079
+ const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
2080
+ return {
2081
+ ...p,
2082
+ scoringFactors: {
2083
+ ...sf,
2084
+ stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
2085
+ }
2086
+ };
2087
+ });
2088
+ }
2089
+ return migrated;
2090
+ }
2091
+ async analyze(days = 30) {
2092
+ const previousPatterns = await this.loadPreviousPatterns();
2093
+ const events = await this.readAllEvents(days);
2094
+ this._cachedEvents = events;
2095
+ const starts = events.filter((e) => e.eventType === "build_start");
2096
+ const attempts = events.filter((e) => e.eventType === "generation_attempt");
2097
+ const passed = attempts.filter(
2098
+ (a) => a.data.validationPassed === true
2099
+ );
2100
+ const failed = attempts.filter(
2101
+ (a) => a.data.validationPassed === false
2102
+ );
2103
+ const ruleFailures = /* @__PURE__ */ new Map();
2104
+ const credentialFailures = /* @__PURE__ */ new Map();
2105
+ for (const a of failed) {
2106
+ const weight = this.recencyWeight(a.fileDate);
2107
+ const buildId = a.runId ?? a.sessionId;
2108
+ const data = a.data;
2109
+ for (const issue of data.issues ?? []) {
2110
+ if (issue.severity === "warn") continue;
2111
+ const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
2112
+ entry.count++;
2113
+ entry.sessions.add(buildId);
2114
+ entry.recencyWeights.push(weight);
2115
+ entry.allMessages.push(issue.message);
2116
+ if (data.workflowType) {
2117
+ entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
2118
+ }
2119
+ ruleFailures.set(issue.rule, entry);
2120
+ if (issue.rule === 17) {
2121
+ const credPatterns = [
2122
+ /credential\s+"([^"]+)"/,
2123
+ /credentialType[:\s]+"?([^"'\s]+)"?/,
2124
+ /missing\s+credential\s+(?:for\s+)?["']?([^"'\s]+)/i
2125
+ ];
2126
+ let credType = "unknown";
2127
+ for (const re of credPatterns) {
2128
+ const m = issue.message.match(re);
2129
+ if (m?.[1]) {
2130
+ credType = m[1];
2131
+ break;
2132
+ }
1568
2133
  }
2134
+ credentialFailures.set(credType, (credentialFailures.get(credType) ?? 0) + 1);
1569
2135
  }
1570
- } catch {
1571
2136
  }
1572
2137
  }
1573
- return events;
2138
+ const failedByDate = /* @__PURE__ */ new Map();
2139
+ for (const a of failed) {
2140
+ failedByDate.set(a.fileDate, (failedByDate.get(a.fileDate) ?? 0) + 1);
2141
+ }
2142
+ const sortedFailDates = [...failedByDate.entries()].sort((a, b) => a[0].localeCompare(b[0]));
2143
+ const hasTrendData = sortedFailDates.length >= 3;
2144
+ let midDate = "";
2145
+ if (hasTrendData) {
2146
+ const halfTotal = failed.length / 2;
2147
+ let cumulative = 0;
2148
+ for (const [date, count] of sortedFailDates) {
2149
+ cumulative += count;
2150
+ if (cumulative >= halfTotal) {
2151
+ midDate = date;
2152
+ break;
2153
+ }
2154
+ }
2155
+ }
2156
+ const ruleTrends = /* @__PURE__ */ new Map();
2157
+ if (hasTrendData) {
2158
+ for (const a of failed) {
2159
+ const data = a.data;
2160
+ const isNewer = a.fileDate > midDate;
2161
+ for (const issue of data.issues ?? []) {
2162
+ const entry = ruleTrends.get(issue.rule) ?? { older: 0, newer: 0 };
2163
+ if (isNewer) entry.newer++;
2164
+ else entry.older++;
2165
+ ruleTrends.set(issue.rule, entry);
2166
+ }
2167
+ }
2168
+ }
2169
+ const sessions = /* @__PURE__ */ new Map();
2170
+ for (const a of attempts) {
2171
+ const buildId = a.runId ?? a.sessionId;
2172
+ const list = sessions.get(buildId) ?? [];
2173
+ list.push(a);
2174
+ sessions.set(buildId, list);
2175
+ }
2176
+ let firstTryPass = 0;
2177
+ let correctionNeeded = 0;
2178
+ let singleAttemptFail = 0;
2179
+ for (const sessionAttempts of sessions.values()) {
2180
+ const lastAttempt = sessionAttempts[sessionAttempts.length - 1];
2181
+ const lastPassed = lastAttempt.data.validationPassed === true;
2182
+ if (sessionAttempts.length === 1 && lastPassed) {
2183
+ firstTryPass++;
2184
+ } else if (sessionAttempts.length > 1 && lastPassed) {
2185
+ correctionNeeded++;
2186
+ } else {
2187
+ singleAttemptFail++;
2188
+ }
2189
+ }
2190
+ const durations = attempts.map((a) => a.data.durationMs).filter((d) => typeof d === "number" && d > 0);
2191
+ const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
2192
+ const totalInput = attempts.reduce((s, a) => s + (a.data.tokensInput ?? 0), 0);
2193
+ const totalOutput = attempts.reduce((s, a) => s + (a.data.tokensOutput ?? 0), 0);
2194
+ const totalSessions = Math.max(sessions.size, 1);
2195
+ const stickinessCount = /* @__PURE__ */ new Map();
2196
+ for (const sessionAttempts of sessions.values()) {
2197
+ if (sessionAttempts.length < 2) continue;
2198
+ for (let i = 0; i < sessionAttempts.length - 1; i++) {
2199
+ const curr = sessionAttempts[i].data;
2200
+ const next = sessionAttempts[i + 1].data;
2201
+ if (curr.validationPassed !== false || next.validationPassed !== false) continue;
2202
+ const currRules = new Set((curr.issues ?? []).map((iss) => iss.rule));
2203
+ const nextRules = new Set((next.issues ?? []).map((iss) => iss.rule));
2204
+ for (const rule of currRules) {
2205
+ if (nextRules.has(rule)) {
2206
+ stickinessCount.set(rule, (stickinessCount.get(rule) ?? 0) + 1);
2207
+ }
2208
+ }
2209
+ }
2210
+ }
2211
+ const CONFIRMED_THRESHOLD = 3;
2212
+ const BUILDS_SINCE_LAST_FAILURE_THRESHOLD = 5;
2213
+ const RESOLVED_TTL_DAYS = 90;
2214
+ const activePatterns = [...ruleFailures.entries()].map(([rule, entry]) => {
2215
+ const t = ruleTrends.get(rule) ?? { older: 0, newer: 0 };
2216
+ const rawConfidence = Math.min(entry.sessions.size / totalSessions, 1);
2217
+ const state = entry.count >= CONFIRMED_THRESHOLD ? "confirmed" : "draft";
2218
+ const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
2219
+ const stickiness = stickinessCount.get(rule) ?? 0;
2220
+ const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
2221
+ const pattern = {
2222
+ rule,
2223
+ failureCount: entry.count,
2224
+ confidence: Math.round(rawConfidence * 1e3) / 1e3,
2225
+ compositeScore,
2226
+ scoringFactors: factors,
2227
+ state,
2228
+ trend: this.classifyTrend(t.older, t.newer),
2229
+ pipelineStage: RULE_PIPELINE_STAGES[rule] ?? "node_generation",
2230
+ exampleMessages: this.deduplicateMessages(entry.allMessages),
2231
+ mitigation: RULE_MITIGATIONS[rule] ?? null
2232
+ };
2233
+ if (entry.workflowTypes.size > 0) {
2234
+ pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
2235
+ }
2236
+ return pattern;
2237
+ }).sort((a, b) => b.compositeScore - a.compositeScore);
2238
+ const activeRules = new Set(activePatterns.map((p) => p.rule));
2239
+ for (const p of activePatterns) {
2240
+ const prev = previousPatterns.find((pp) => pp.rule === p.rule);
2241
+ if (prev?.state === "resolved") {
2242
+ p.trend = "worsening";
2243
+ p.regressed = true;
2244
+ }
2245
+ }
2246
+ const ruleLastFailureDate = /* @__PURE__ */ new Map();
2247
+ for (const a of failed) {
2248
+ const data = a.data;
2249
+ for (const issue of data.issues ?? []) {
2250
+ const existing = ruleLastFailureDate.get(issue.rule);
2251
+ if (!existing || a.fileDate > existing) {
2252
+ ruleLastFailureDate.set(issue.rule, a.fileDate);
2253
+ }
2254
+ }
2255
+ }
2256
+ const newlyResolved = previousPatterns.filter((p) => {
2257
+ if (p.state !== "confirmed" || activeRules.has(p.rule)) return false;
2258
+ const lastFailDate = ruleLastFailureDate.get(p.rule) ?? "";
2259
+ const buildsSince = starts.filter((s) => s.fileDate > lastFailDate).length;
2260
+ return buildsSince >= BUILDS_SINCE_LAST_FAILURE_THRESHOLD;
2261
+ }).map((p) => ({
2262
+ ...p,
2263
+ state: "resolved",
2264
+ trend: "improving",
2265
+ pipelineStage: p.pipelineStage ?? RULE_PIPELINE_STAGES[p.rule] ?? "node_generation",
2266
+ confidence: 0,
2267
+ compositeScore: 0,
2268
+ scoringFactors: { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
2269
+ failureCount: 0,
2270
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
2271
+ }));
2272
+ const ttlCutoff = /* @__PURE__ */ new Date();
2273
+ ttlCutoff.setDate(ttlCutoff.getDate() - RESOLVED_TTL_DAYS);
2274
+ const ttlCutoffStr = ttlCutoff.toISOString();
2275
+ const carriedResolved = previousPatterns.filter((p) => p.state === "resolved" && !activeRules.has(p.rule) && (!p.resolvedAt || p.resolvedAt >= ttlCutoffStr)).map((p) => ({ ...p }));
2276
+ const newlyResolvedRules = new Set(newlyResolved.map((p) => p.rule));
2277
+ const pendingResolution = previousPatterns.filter((p) => p.state === "confirmed" && !activeRules.has(p.rule) && !newlyResolvedRules.has(p.rule)).map((p) => ({ ...p }));
2278
+ const deduped = [
2279
+ ...newlyResolved,
2280
+ ...carriedResolved.filter((p) => !newlyResolvedRules.has(p.rule)),
2281
+ ...pendingResolution
2282
+ ];
2283
+ const patterns = [...activePatterns, ...deduped];
2284
+ const credTypes = [...credentialFailures.entries()].sort((a, b) => b[1] - a[1]).map(([type, count]) => ({ type, count }));
2285
+ const drift = this.detectDrift(patterns);
2286
+ const warnEffMap = /* @__PURE__ */ new Map();
2287
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
2288
+ for (const bc of buildCompletes) {
2289
+ const bcData = bc.data;
2290
+ const warned = bcData.warnedRules ?? [];
2291
+ if (warned.length === 0) continue;
2292
+ const sessionFailedRules = /* @__PURE__ */ new Set();
2293
+ const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
2294
+ for (const a of sessionAttempts) {
2295
+ const ad = a.data;
2296
+ if (ad.validationPassed === false) {
2297
+ for (const issue of ad.issues ?? []) {
2298
+ sessionFailedRules.add(issue.rule);
2299
+ }
2300
+ }
2301
+ }
2302
+ for (const rule of warned) {
2303
+ const entry = warnEffMap.get(rule) ?? { warned: 0, passed: 0, failed: 0 };
2304
+ entry.warned++;
2305
+ if (sessionFailedRules.has(rule)) entry.failed++;
2306
+ else entry.passed++;
2307
+ warnEffMap.set(rule, entry);
2308
+ }
2309
+ }
2310
+ const warningEffectiveness = [...warnEffMap.entries()].map(([rule, e]) => ({
2311
+ rule,
2312
+ timesWarned: e.warned,
2313
+ timesWarnedAndPassed: e.passed,
2314
+ timesWarnedAndFailed: e.failed,
2315
+ effectivenessRate: e.warned > 0 ? Math.round(e.passed / e.warned * 1e3) / 1e3 : 0
2316
+ })).sort((a, b) => b.timesWarned - a.timesWarned);
2317
+ const coOccurrenceMap = /* @__PURE__ */ new Map();
2318
+ for (const a of failed) {
2319
+ const data = a.data;
2320
+ const rules = [...new Set((data.issues ?? []).map((i) => i.rule))].sort((x, y) => x - y);
2321
+ for (let i = 0; i < rules.length; i++) {
2322
+ for (let j = i + 1; j < rules.length; j++) {
2323
+ const key = `${rules[i]},${rules[j]}`;
2324
+ coOccurrenceMap.set(key, (coOccurrenceMap.get(key) ?? 0) + 1);
2325
+ }
2326
+ }
2327
+ }
2328
+ const ruleCoOccurrence = [...coOccurrenceMap.entries()].filter(([, count]) => count >= 3).map(([key, count]) => {
2329
+ const [a, b] = key.split(",").map(Number);
2330
+ return { rules: [a, b], count };
2331
+ }).sort((a, b) => b.count - a.count);
2332
+ const attemptDistribution = {};
2333
+ for (const sessionAttempts of sessions.values()) {
2334
+ const depth = sessionAttempts.length;
2335
+ attemptDistribution[depth] = (attemptDistribution[depth] ?? 0) + 1;
2336
+ }
2337
+ return {
2338
+ schemaVersion: PATTERN_SCHEMA_VERSION,
2339
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2340
+ summary: {
2341
+ totalBuilds: starts.length,
2342
+ totalAttempts: attempts.length,
2343
+ firstTryPassRate: Math.round(firstTryPass / totalSessions * 1e3) / 1e3,
2344
+ correctionRate: Math.round(correctionNeeded / totalSessions * 1e3) / 1e3,
2345
+ singleAttemptFailRate: Math.round(singleAttemptFail / totalSessions * 1e3) / 1e3,
2346
+ avgDurationMs: Math.round(avgDuration),
2347
+ totalTokensInput: totalInput,
2348
+ totalTokensOutput: totalOutput,
2349
+ attemptDistribution
2350
+ },
2351
+ topFailureRules: patterns,
2352
+ failingCredentialTypes: credTypes,
2353
+ drift,
2354
+ warningEffectiveness,
2355
+ ruleCoOccurrence
2356
+ };
2357
+ }
2358
+ async analyzeAndSave(days = 30) {
2359
+ const analysis = await this.analyze(days);
2360
+ await (0, import_promises3.mkdir)(this.outputDir, { recursive: true });
2361
+ const outputPath = (0, import_node_path5.join)(this.outputDir, "patterns.json");
2362
+ const tmpPath = `${outputPath}.tmp`;
2363
+ await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
2364
+ await (0, import_promises3.rename)(tmpPath, outputPath);
2365
+ const historySummary = {
2366
+ timestamp: analysis.generatedAt,
2367
+ totalBuilds: analysis.summary.totalBuilds,
2368
+ firstTryPassRate: analysis.summary.firstTryPassRate,
2369
+ correctionRate: analysis.summary.correctionRate,
2370
+ singleAttemptFailRate: analysis.summary.singleAttemptFailRate,
2371
+ activePatternCount: analysis.topFailureRules.filter((p) => p.state !== "resolved").length,
2372
+ topRules: analysis.topFailureRules.filter((p) => p.state !== "resolved").slice(0, 5).map((p) => ({ rule: p.rule, compositeScore: p.compositeScore, state: p.state }))
2373
+ };
2374
+ const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
2375
+ await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
2376
+ const sessions = await this.buildSessionSummaries(days);
2377
+ const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
2378
+ const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
2379
+ await (0, import_promises3.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
2380
+ await (0, import_promises3.rename)(sessionHistoryTmp, sessionHistoryPath);
2381
+ return analysis;
2382
+ }
2383
+ async getSessions(limit = 20) {
2384
+ try {
2385
+ const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
2386
+ const all = JSON.parse(raw);
2387
+ return all.slice(-limit);
2388
+ } catch {
2389
+ return [];
2390
+ }
2391
+ }
2392
+ async buildSessionSummaries(days = 30) {
2393
+ const events = this._cachedEvents ?? await this.readAllEvents(days);
2394
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
2395
+ const attemptsByBuild = /* @__PURE__ */ new Map();
2396
+ for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
2397
+ const buildId = e.runId ?? e.sessionId;
2398
+ const list = attemptsByBuild.get(buildId) ?? [];
2399
+ list.push(e);
2400
+ attemptsByBuild.set(buildId, list);
2401
+ }
2402
+ const summaries = buildCompletes.map((bc) => {
2403
+ const data = bc.data;
2404
+ const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
2405
+ const failedRules = Array.from(new Set(
2406
+ sessionAttempts.flatMap((a) => {
2407
+ const ad = a.data;
2408
+ if (ad.validationPassed !== false) return [];
2409
+ return (ad.issues ?? []).map((i) => i.rule);
2410
+ })
2411
+ ));
2412
+ return {
2413
+ sessionId: bc.sessionId,
2414
+ date: bc.fileDate,
2415
+ description: data.description ?? "",
2416
+ workflowType: data.workflowType ?? null,
2417
+ attempts: data.totalAttempts ?? 1,
2418
+ success: data.success ?? false,
2419
+ failedRules,
2420
+ workflowName: data.workflowName ?? null
2421
+ };
2422
+ });
2423
+ return summaries.sort((a, b) => a.date.localeCompare(b.date));
2424
+ }
2425
+ async getHistory(limit = 20) {
2426
+ try {
2427
+ const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
2428
+ return raw.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)).slice(-limit);
2429
+ } catch {
2430
+ return [];
2431
+ }
2432
+ }
2433
+ static fromEnv() {
2434
+ const dir = process.env["KAIROS_TELEMETRY"];
2435
+ return dir && dir !== "true" && dir !== "false" ? new _PatternAnalyzer(dir) : new _PatternAnalyzer();
2436
+ }
2437
+ detectDrift(patterns) {
2438
+ const VALIDATOR_RULES = VALIDATOR_RULE_IDS;
2439
+ const validatorRuleSet = new Set(VALIDATOR_RULES);
2440
+ const alerts = [];
2441
+ for (const p of patterns) {
2442
+ if (p.state !== "resolved" && !validatorRuleSet.has(p.rule)) {
2443
+ alerts.push({
2444
+ type: "stale_pattern",
2445
+ rule: p.rule,
2446
+ message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-26)`
2447
+ });
2448
+ }
2449
+ }
2450
+ for (const rule of VALIDATOR_RULES) {
2451
+ if (!(rule in RULE_MITIGATIONS)) {
2452
+ alerts.push({
2453
+ type: "missing_mitigation",
2454
+ rule,
2455
+ message: `Rule ${rule} has no mitigation text \u2014 if it fails, the system can't advise the LLM how to fix it`
2456
+ });
2457
+ }
2458
+ if (!(rule in RULE_PIPELINE_STAGES)) {
2459
+ alerts.push({
2460
+ type: "missing_stage_mapping",
2461
+ rule,
2462
+ message: `Rule ${rule} has no pipeline stage mapping \u2014 failures won't be grouped correctly`
2463
+ });
2464
+ }
2465
+ }
2466
+ const coveredRules = VALIDATOR_RULES.filter((r) => r in RULE_MITIGATIONS && r in RULE_PIPELINE_STAGES).length;
2467
+ return {
2468
+ healthy: alerts.length === 0,
2469
+ alerts,
2470
+ coveredRules,
2471
+ totalRules: VALIDATOR_RULES.length
2472
+ };
2473
+ }
2474
+ computeCompositeScore(rawConfidence, sampleSize, state, avgRecency, stickiness) {
2475
+ const stateWeights = { draft: 0.3, confirmed: 0.8, resolved: 0.1 };
2476
+ const stateWeight = stateWeights[state];
2477
+ const impact = (1 - Math.exp(-sampleSize / 5)) * stateWeight;
2478
+ const stickinessBoost = Math.min(0.15, stickiness * 0.05);
2479
+ const compositeScore = Math.min(Math.round(rawConfidence * impact * avgRecency * (1 + stickinessBoost) * 1e3) / 1e3, 1);
2480
+ return {
2481
+ compositeScore,
2482
+ factors: {
2483
+ rawConfidence: Math.round(rawConfidence * 1e3) / 1e3,
2484
+ impact: Math.round(impact * 1e3) / 1e3,
2485
+ recency: Math.round(avgRecency * 1e3) / 1e3,
2486
+ stickinessBoost: Math.round(stickinessBoost * 1e3) / 1e3
2487
+ }
2488
+ };
2489
+ }
2490
+ classifyTrend(older, newer) {
2491
+ const total = older + newer;
2492
+ if (total === 0) return "stable";
2493
+ if (older === 0) return "new";
2494
+ const newerRatio = newer / total;
2495
+ if (newerRatio >= 0.65) return "worsening";
2496
+ if (newerRatio <= 0.35) return "improving";
2497
+ return "stable";
2498
+ }
2499
+ deduplicateMessages(messages, maxCount = 3) {
2500
+ const normalize = (msg) => msg.replace(/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/gi, "...").replace(/\bnode\s+"[^"]+"/g, 'node "..."').replace(/\s+/g, " ").trim();
2501
+ const seen = /* @__PURE__ */ new Set();
2502
+ const unique = [];
2503
+ for (const msg of messages) {
2504
+ const key = normalize(msg);
2505
+ if (!seen.has(key) && unique.length < maxCount) {
2506
+ seen.add(key);
2507
+ unique.push(msg);
2508
+ }
2509
+ }
2510
+ return unique;
2511
+ }
2512
+ recencyWeight(fileDate, halfLifeDays = 30) {
2513
+ const daysAgo = Math.max(0, (Date.now() - (/* @__PURE__ */ new Date(fileDate + "T12:00:00Z")).getTime()) / (1e3 * 60 * 60 * 24));
2514
+ return Math.max(0.1, Math.exp(-Math.LN2 * daysAgo / halfLifeDays));
2515
+ }
2516
+ async readAllEvents(days) {
2517
+ return readTelemetryEvents(this.telemetryDir, days);
1574
2518
  }
1575
2519
  };
1576
2520
 
@@ -1631,6 +2575,43 @@ ${regularLines}`;
1631
2575
  }
1632
2576
  };
1633
2577
 
2578
+ // src/telemetry/collector.ts
2579
+ var import_promises4 = require("fs/promises");
2580
+ var import_node_path6 = require("path");
2581
+ var import_node_os5 = require("os");
2582
+
2583
+ // src/telemetry/types.ts
2584
+ var TELEMETRY_SCHEMA_VERSION = 2;
2585
+
2586
+ // src/telemetry/collector.ts
2587
+ var TelemetryCollector = class {
2588
+ dir;
2589
+ sessionId;
2590
+ dirReady = null;
2591
+ constructor(dir) {
2592
+ this.dir = dir ?? (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".kairos", "telemetry");
2593
+ this.sessionId = generateUUID();
2594
+ }
2595
+ async emit(eventType, data, runId) {
2596
+ const event = {
2597
+ schemaVersion: TELEMETRY_SCHEMA_VERSION,
2598
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2599
+ sessionId: this.sessionId,
2600
+ ...runId ? { runId } : {},
2601
+ eventType,
2602
+ data
2603
+ };
2604
+ if (!this.dirReady) {
2605
+ this.dirReady = (0, import_promises4.mkdir)(this.dir, { recursive: true }).then(() => {
2606
+ });
2607
+ }
2608
+ await this.dirReady;
2609
+ const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
2610
+ const filepath = (0, import_node_path6.join)(this.dir, filename);
2611
+ await (0, import_promises4.appendFile)(filepath, JSON.stringify(event) + "\n", "utf-8");
2612
+ }
2613
+ };
2614
+
1634
2615
  // src/utils/logger.ts
1635
2616
  var nullLogger = {
1636
2617
  debug() {
@@ -1643,19 +2624,91 @@ var nullLogger = {
1643
2624
  }
1644
2625
  };
1645
2626
 
2627
+ // src/utils/workflow-type.ts
2628
+ var TYPE_KEYWORDS = [
2629
+ ["gmail", "email"],
2630
+ ["imap", "email"],
2631
+ ["smtp", "email"],
2632
+ [" email", "email"],
2633
+ ["slack", "slack"],
2634
+ ["telegram", "messaging"],
2635
+ ["discord", "messaging"],
2636
+ [" sms", "messaging"],
2637
+ ["twilio", "messaging"],
2638
+ ["webhook", "webhook"],
2639
+ ["google sheets", "data"],
2640
+ ["spreadsheet", "data"],
2641
+ ["airtable", "data"],
2642
+ ["notion", "data"],
2643
+ ["github", "devops"],
2644
+ ["gitlab", "devops"],
2645
+ ["schedule", "schedule"],
2646
+ [" cron", "schedule"],
2647
+ ["daily", "schedule"],
2648
+ ["weekly", "schedule"],
2649
+ ["hourly", "schedule"],
2650
+ ["every day", "schedule"],
2651
+ ["every hour", "schedule"],
2652
+ ["every morning", "schedule"],
2653
+ ["postgres", "database"],
2654
+ ["mysql", "database"],
2655
+ ["supabase", "database"],
2656
+ ["redis", "database"],
2657
+ [" database", "database"],
2658
+ [" llm", "ai"],
2659
+ [" gpt", "ai"],
2660
+ ["claude", "ai"],
2661
+ [" agent", "ai"],
2662
+ ["langchain", "ai"],
2663
+ [" ai ", "ai"],
2664
+ [" ai", "ai"],
2665
+ ["http request", "api"],
2666
+ ["rest api", "api"],
2667
+ [" api", "api"]
2668
+ ];
2669
+ function inferWorkflowType(description) {
2670
+ const lower = " " + description.toLowerCase();
2671
+ for (const [keyword, type] of TYPE_KEYWORDS) {
2672
+ if (lower.includes(keyword)) return type;
2673
+ }
2674
+ return null;
2675
+ }
2676
+
1646
2677
  // src/mcp-server.ts
1647
- var import_node_fs = require("fs");
1648
- var import_node_path3 = require("path");
2678
+ var import_node_fs3 = require("fs");
2679
+ var import_node_path7 = require("path");
2680
+ var import_node_os6 = require("os");
1649
2681
  var import_node_url = require("url");
1650
2682
  var import_meta = {};
1651
- var __dirname = (0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
1652
- var pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path3.join)(__dirname, "..", "package.json"), "utf-8"));
2683
+ var __dirname = (0, import_node_path7.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
2684
+ var pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path7.join)(__dirname, "..", "package.json"), "utf-8"));
1653
2685
  var library = new FileLibrary();
1654
2686
  var validator = new N8nValidator();
1655
2687
  var nodeSyncer = new NodeSyncer();
1656
2688
  var lastSync = null;
1657
2689
  var stripper = new N8nFieldStripper();
1658
- var promptBuilder = new PromptBuilder();
2690
+ var promptBuilder = new PromptBuilder(getMcpPatternsPath());
2691
+ function getMcpTelemetry() {
2692
+ const val = process.env["KAIROS_TELEMETRY"];
2693
+ if (!val || val === "false") return null;
2694
+ return val === "true" ? new TelemetryCollector() : new TelemetryCollector(val);
2695
+ }
2696
+ function getMcpPatternsPath() {
2697
+ const val = process.env["KAIROS_TELEMETRY"];
2698
+ if (val && val !== "false" && val !== "true") {
2699
+ return (0, import_node_path7.join)(val, "..", "patterns.json");
2700
+ }
2701
+ return (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".kairos", "patterns.json");
2702
+ }
2703
+ var mcpTelemetry = getMcpTelemetry();
2704
+ var mcpSessions = /* @__PURE__ */ new Map();
2705
+ var SESSION_TTL_MS = 60 * 60 * 1e3;
2706
+ function evictStaleSessions() {
2707
+ const cutoff = Date.now() - SESSION_TTL_MS;
2708
+ for (const [id, session] of mcpSessions) {
2709
+ if (session.startTime < cutoff) mcpSessions.delete(id);
2710
+ }
2711
+ }
1659
2712
  function getTelemetryReader() {
1660
2713
  try {
1661
2714
  return new TelemetryReader();
@@ -1703,6 +2756,7 @@ server.tool(
1703
2756
  name: import_zod.z.string().optional().describe("Optional workflow name override")
1704
2757
  },
1705
2758
  async ({ description, name }) => {
2759
+ evictStaleSessions();
1706
2760
  const baseUrl = process.env["N8N_BASE_URL"];
1707
2761
  const apiKey = process.env["N8N_API_KEY"];
1708
2762
  if (!baseUrl || !apiKey) {
@@ -1714,6 +2768,8 @@ server.tool(
1714
2768
  isError: true
1715
2769
  };
1716
2770
  }
2771
+ const runId = generateUUID();
2772
+ const workflowType = inferWorkflowType(description);
1717
2773
  await library.initialize();
1718
2774
  const syncResult = await autoSync();
1719
2775
  const matches = await library.search(description);
@@ -1721,11 +2777,22 @@ server.tool(
1721
2777
  const failureRates = await telemetryReader?.getFailureRates() ?? [];
1722
2778
  const request = { description, ...name ? { name } : {} };
1723
2779
  const built = promptBuilder.build(request, matches, failureRates, syncResult?.catalogText);
2780
+ if (mcpTelemetry) {
2781
+ mcpSessions.set(runId, {
2782
+ description,
2783
+ startTime: Date.now(),
2784
+ validateAttempts: 0,
2785
+ warnedRules: promptBuilder.getWarnedRules(),
2786
+ workflowType
2787
+ });
2788
+ await mcpTelemetry.emit("build_start", { description, model: "mcp-decomposed", dryRun: false }, runId);
2789
+ }
1724
2790
  const systemText = built.system.map((block) => block.text).join("\n\n---\n\n");
1725
2791
  return {
1726
2792
  content: [{
1727
2793
  type: "text",
1728
2794
  text: JSON.stringify({
2795
+ kairos_run_id: runId,
1729
2796
  mode: built.mode,
1730
2797
  matchCount: matches.length,
1731
2798
  topMatchScore: matches[0]?.score ?? null,
@@ -1757,11 +2824,12 @@ server.tool(
1757
2824
  );
1758
2825
  server.tool(
1759
2826
  "kairos_validate",
1760
- "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.",
2827
+ "Validate n8n workflow JSON against 26 structural rules. Returns pass/fail with specific issues. If validation fails, fix the issues and call this again. Errors block deployment; warnings are advisory.",
1761
2828
  {
1762
- workflow: import_zod.z.string().describe("The workflow JSON string to validate")
2829
+ workflow: import_zod.z.string().describe("The workflow JSON string to validate"),
2830
+ kairos_run_id: import_zod.z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation")
1763
2831
  },
1764
- async ({ workflow: workflowStr }) => {
2832
+ async ({ workflow: workflowStr, kairos_run_id }) => {
1765
2833
  let parsed;
1766
2834
  try {
1767
2835
  parsed = JSON.parse(workflowStr);
@@ -1779,6 +2847,24 @@ server.tool(
1779
2847
  const result = validator.validate(parsed);
1780
2848
  const errors = result.issues.filter((i) => i.severity === "error");
1781
2849
  const warnings = result.issues.filter((i) => i.severity === "warn");
2850
+ if (mcpTelemetry && kairos_run_id) {
2851
+ const session = mcpSessions.get(kairos_run_id);
2852
+ if (session) {
2853
+ session.validateAttempts++;
2854
+ await mcpTelemetry.emit("generation_attempt", {
2855
+ description: session.description,
2856
+ attempt: session.validateAttempts,
2857
+ temperature: 0,
2858
+ durationMs: 0,
2859
+ tokensInput: 0,
2860
+ tokensOutput: 0,
2861
+ validationPassed: result.valid,
2862
+ issueCount: result.issues.length,
2863
+ issues: result.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null })),
2864
+ workflowType: session.workflowType
2865
+ }, kairos_run_id);
2866
+ }
2867
+ }
1782
2868
  return {
1783
2869
  content: [{
1784
2870
  type: "text",
@@ -1807,9 +2893,10 @@ server.tool(
1807
2893
  "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.",
1808
2894
  {
1809
2895
  workflow: import_zod.z.string().describe("The validated workflow JSON string to deploy"),
1810
- activate: import_zod.z.boolean().default(false).describe("Activate the workflow immediately after deployment")
2896
+ activate: import_zod.z.boolean().default(false).describe("Activate the workflow immediately after deployment"),
2897
+ kairos_run_id: import_zod.z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation")
1811
2898
  },
1812
- async ({ workflow: workflowStr, activate }) => {
2899
+ async ({ workflow: workflowStr, activate, kairos_run_id }) => {
1813
2900
  if (!isAllowed("deploy")) {
1814
2901
  return {
1815
2902
  content: [{
@@ -1869,6 +2956,28 @@ server.tool(
1869
2956
  generationMode: "scratch",
1870
2957
  generationAttempts: 1
1871
2958
  });
2959
+ if (mcpTelemetry && kairos_run_id) {
2960
+ const session = mcpSessions.get(kairos_run_id);
2961
+ if (session) {
2962
+ await mcpTelemetry.emit("build_complete", {
2963
+ description: session.description,
2964
+ success: true,
2965
+ totalAttempts: session.validateAttempts,
2966
+ totalDurationMs: Date.now() - session.startTime,
2967
+ totalTokensInput: 0,
2968
+ totalTokensOutput: 0,
2969
+ workflowName: response.name,
2970
+ workflowId: response.id,
2971
+ dryRun: false,
2972
+ credentialsNeeded: 0,
2973
+ warnedRules: session.warnedRules,
2974
+ workflowType: session.workflowType
2975
+ }, kairos_run_id);
2976
+ mcpSessions.delete(kairos_run_id);
2977
+ PatternAnalyzer.fromEnv().analyzeAndSave().catch(() => {
2978
+ });
2979
+ }
2980
+ }
1872
2981
  return {
1873
2982
  content: [{
1874
2983
  type: "text",
@@ -1951,6 +3060,27 @@ server.tool(
1951
3060
  };
1952
3061
  }
1953
3062
  );
3063
+ server.tool(
3064
+ "kairos_patterns",
3065
+ "Analyze telemetry data and return failure patterns, build stats, and credential breakdowns. Useful for understanding what goes wrong most often and how to prevent it.",
3066
+ {
3067
+ days: import_zod.z.number().default(30).describe("Number of days of telemetry to analyze"),
3068
+ limit: import_zod.z.number().optional().describe("Maximum number of failure patterns to return")
3069
+ },
3070
+ async ({ days, limit }) => {
3071
+ const analyzer = PatternAnalyzer.fromEnv();
3072
+ const analysis = await analyzer.analyzeAndSave(days);
3073
+ if (limit !== void 0 && limit > 0) {
3074
+ analysis.topFailureRules = analysis.topFailureRules.slice(0, limit);
3075
+ }
3076
+ return {
3077
+ content: [{
3078
+ type: "text",
3079
+ text: JSON.stringify(analysis, null, 2)
3080
+ }]
3081
+ };
3082
+ }
3083
+ );
1954
3084
  server.tool(
1955
3085
  "kairos_list",
1956
3086
  "List all workflows deployed on the connected n8n instance.",