@kairos-sdk/core 0.4.0 → 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,6 +718,9 @@ 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);
580
724
  if (Array.isArray(workflow.nodes)) {
581
725
  const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
582
726
  for (const issue of issues) {
@@ -709,10 +853,14 @@ var N8nValidator = class {
709
853
  checkRule11(w, issues) {
710
854
  if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
711
855
  const reachable = /* @__PURE__ */ new Set();
712
- for (const [, outputs] of Object.entries(w.connections)) {
856
+ const aiSubNodeSources = /* @__PURE__ */ new Set();
857
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
713
858
  if (typeof outputs !== "object" || outputs === null) continue;
714
- for (const portGroup of Object.values(outputs)) {
859
+ let hasAiPort = false;
860
+ for (const [portName, portGroup] of Object.entries(outputs)) {
715
861
  if (!Array.isArray(portGroup)) continue;
862
+ const isAiPort = portName.startsWith("ai_");
863
+ if (isAiPort) hasAiPort = true;
716
864
  for (const targets of portGroup) {
717
865
  if (!Array.isArray(targets)) continue;
718
866
  for (const target of targets) {
@@ -721,10 +869,13 @@ var N8nValidator = class {
721
869
  }
722
870
  }
723
871
  }
872
+ if (hasAiPort) aiSubNodeSources.add(sourceName);
724
873
  }
725
874
  for (const node of w.nodes) {
726
875
  if (node.type.includes("stickyNote")) continue;
727
- 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)) {
728
879
  this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
729
880
  }
730
881
  }
@@ -920,6 +1071,76 @@ var N8nValidator = class {
920
1071
  }
921
1072
  }
922
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
+ }
923
1144
  // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
924
1145
  checkRule21(w, issues) {
925
1146
  if (!Array.isArray(w.nodes)) return;
@@ -1209,9 +1430,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
1209
1430
  - Never reuse IDs, never use sequential fake IDs like "node-1"
1210
1431
 
1211
1432
  ### 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
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
1215
1438
 
1216
1439
  ### Node names:
1217
1440
  - All node names must be unique within the workflow
@@ -1258,6 +1481,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
1258
1481
 
1259
1482
  ---
1260
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
+
1261
1501
  ## NODE CATALOG \u2014 exact type strings and safe typeVersions
1262
1502
 
1263
1503
  ### Triggers (always at least one required):
@@ -1357,6 +1597,9 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
1357
1597
  5. At least one trigger node present
1358
1598
  6. Every AI Agent has an ai_languageModel sub-node
1359
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()
1360
1603
 
1361
1604
  ---
1362
1605
 
@@ -1364,7 +1607,7 @@ Respond ONLY with a generate_workflow tool call. No prose. No markdown outside t
1364
1607
  If the request is impossible or unclear, set the error field instead of generating a workflow.`;
1365
1608
 
1366
1609
  // src/validation/rule-metadata.ts
1367
- var VALIDATOR_RULE_IDS = Array.from({ length: 23 }, (_, i) => i + 1);
1610
+ var VALIDATOR_RULE_IDS = Array.from({ length: 26 }, (_, i) => i + 1);
1368
1611
  var RULE_PIPELINE_STAGES = {
1369
1612
  1: "node_generation",
1370
1613
  2: "node_generation",
@@ -1388,7 +1631,28 @@ var RULE_PIPELINE_STAGES = {
1388
1631
  20: "connection_wiring",
1389
1632
  21: "workflow_structure",
1390
1633
  22: "workflow_structure",
1391
- 23: "node_generation"
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
+ }
1392
1656
  };
1393
1657
  var RULE_MITIGATIONS = {
1394
1658
  1: "Provide a non-empty workflow name string",
@@ -1407,21 +1671,44 @@ var RULE_MITIGATIONS = {
1407
1671
  14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
1408
1672
  15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1409
1673
  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" }',
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',
1411
1675
  18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
1412
1676
  19: "Use known safe typeVersion values for each node type",
1413
1677
  20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1414
1678
  21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1415
1679
  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"
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'
1417
1684
  };
1418
1685
 
1419
1686
  // src/generation/prompt-builder.ts
1420
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()`;
1421
1700
  var PromptBuilder = class {
1422
1701
  patternsPath;
1423
- constructor(patternsPath) {
1702
+ profile;
1703
+ _lastActivePatterns = null;
1704
+ constructor(patternsPath, profile) {
1424
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;
1425
1712
  }
1426
1713
  build(request, matches, globalFailureRates = [], dynamicCatalog) {
1427
1714
  const mode = this.resolveMode(matches);
@@ -1459,53 +1746,62 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
1459
1746
  cache_control: { type: "ephemeral" }
1460
1747
  }
1461
1748
  ];
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)})
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)})
1466
1754
  Nodes:
1467
1755
  ${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");
1756
+ }).join("\n\n");
1481
1757
  blocks.push({
1482
1758
  type: "text",
1483
- 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:
1484
1772
  Nodes:
1485
1773
  ${nodes}`
1486
- });
1487
- } else {
1488
- blocks.push({
1489
- type: "text",
1490
- 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:
1491
1779
 
1492
1780
  ${json}`
1493
- });
1781
+ });
1782
+ }
1494
1783
  }
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
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
1502
1790
  A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
1503
- });
1791
+ });
1792
+ }
1504
1793
  }
1505
1794
  const warnings = this.buildFailureWarnings(matches, globalFailureRates);
1506
1795
  if (warnings) {
1507
1796
  blocks.push({ type: "text", text: warnings });
1508
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
+ }
1509
1805
  return blocks;
1510
1806
  }
1511
1807
  loadPatterns() {
@@ -1519,18 +1815,19 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1519
1815
  }
1520
1816
  }
1521
1817
  getWarnedRules() {
1522
- return this.getActivePatterns().map((p) => p.rule);
1818
+ const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
1819
+ return patterns.map((p) => p.rule);
1523
1820
  }
1524
- getActivePatterns() {
1525
- const MAX_WARNED = 10;
1821
+ getActivePatterns(maxCount = 10) {
1526
1822
  const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
1527
1823
  const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
1528
1824
  const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1529
1825
  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);
1826
+ return [...regressed, ...confirmed, ...drafts].slice(0, maxCount);
1531
1827
  }
1532
1828
  buildFailureWarnings(matches, globalFailureRates) {
1533
- const richPatterns = this.getActivePatterns();
1829
+ const richPatterns = this.getActivePatterns(this.resolveMaxPatterns());
1830
+ this._lastActivePatterns = richPatterns;
1534
1831
  if (richPatterns.length > 0) {
1535
1832
  return this.buildStageGroupedWarnings(richPatterns, matches);
1536
1833
  }
@@ -1541,7 +1838,8 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1541
1838
  credential_injection: "CREDENTIAL FORMATTING",
1542
1839
  connection_wiring: "CONNECTION WIRING",
1543
1840
  node_generation: "NODE GENERATION",
1544
- workflow_structure: "WORKFLOW STRUCTURE"
1841
+ workflow_structure: "WORKFLOW STRUCTURE",
1842
+ expression_syntax: "EXPRESSION SYNTAX"
1545
1843
  };
1546
1844
  const byStage = /* @__PURE__ */ new Map();
1547
1845
  for (const p of patterns) {
@@ -1569,7 +1867,11 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1569
1867
  const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
1570
1868
  const remedyStr = remedy ? `
1571
1869
  Fix: ${remedy}` : "";
1572
- lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}`);
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}`);
1573
1875
  } else {
1574
1876
  const ruleNums = group.map((p) => p.rule).join(", ");
1575
1877
  const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
@@ -1744,6 +2046,7 @@ var PATTERN_SCHEMA_VERSION = 2;
1744
2046
  var PatternAnalyzer = class _PatternAnalyzer {
1745
2047
  telemetryDir;
1746
2048
  outputDir;
2049
+ _cachedEvents = null;
1747
2050
  constructor(telemetryDir) {
1748
2051
  const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
1749
2052
  this.telemetryDir = telemetryDir ?? defaultDir;
@@ -1772,19 +2075,23 @@ var PatternAnalyzer = class _PatternAnalyzer {
1772
2075
  }));
1773
2076
  }
1774
2077
  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
- }));
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
+ });
1782
2088
  }
1783
2089
  return migrated;
1784
2090
  }
1785
2091
  async analyze(days = 30) {
1786
2092
  const previousPatterns = await this.loadPreviousPatterns();
1787
2093
  const events = await this.readAllEvents(days);
2094
+ this._cachedEvents = events;
1788
2095
  const starts = events.filter((e) => e.eventType === "build_start");
1789
2096
  const attempts = events.filter((e) => e.eventType === "generation_attempt");
1790
2097
  const passed = attempts.filter(
@@ -1797,13 +2104,18 @@ var PatternAnalyzer = class _PatternAnalyzer {
1797
2104
  const credentialFailures = /* @__PURE__ */ new Map();
1798
2105
  for (const a of failed) {
1799
2106
  const weight = this.recencyWeight(a.fileDate);
2107
+ const buildId = a.runId ?? a.sessionId;
1800
2108
  const data = a.data;
1801
2109
  for (const issue of data.issues ?? []) {
1802
- const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [] };
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() };
1803
2112
  entry.count++;
1804
- entry.sessions.add(a.sessionId);
2113
+ entry.sessions.add(buildId);
1805
2114
  entry.recencyWeights.push(weight);
1806
2115
  entry.allMessages.push(issue.message);
2116
+ if (data.workflowType) {
2117
+ entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
2118
+ }
1807
2119
  ruleFailures.set(issue.rule, entry);
1808
2120
  if (issue.rule === 17) {
1809
2121
  const credPatterns = [
@@ -1856,9 +2168,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
1856
2168
  }
1857
2169
  const sessions = /* @__PURE__ */ new Map();
1858
2170
  for (const a of attempts) {
1859
- const list = sessions.get(a.sessionId) ?? [];
2171
+ const buildId = a.runId ?? a.sessionId;
2172
+ const list = sessions.get(buildId) ?? [];
1860
2173
  list.push(a);
1861
- sessions.set(a.sessionId, list);
2174
+ sessions.set(buildId, list);
1862
2175
  }
1863
2176
  let firstTryPass = 0;
1864
2177
  let correctionNeeded = 0;
@@ -1905,7 +2218,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1905
2218
  const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
1906
2219
  const stickiness = stickinessCount.get(rule) ?? 0;
1907
2220
  const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
1908
- return {
2221
+ const pattern = {
1909
2222
  rule,
1910
2223
  failureCount: entry.count,
1911
2224
  confidence: Math.round(rawConfidence * 1e3) / 1e3,
@@ -1917,6 +2230,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
1917
2230
  exampleMessages: this.deduplicateMessages(entry.allMessages),
1918
2231
  mitigation: RULE_MITIGATIONS[rule] ?? null
1919
2232
  };
2233
+ if (entry.workflowTypes.size > 0) {
2234
+ pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
2235
+ }
2236
+ return pattern;
1920
2237
  }).sort((a, b) => b.compositeScore - a.compositeScore);
1921
2238
  const activeRules = new Set(activePatterns.map((p) => p.rule));
1922
2239
  for (const p of activePatterns) {
@@ -1973,7 +2290,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1973
2290
  const warned = bcData.warnedRules ?? [];
1974
2291
  if (warned.length === 0) continue;
1975
2292
  const sessionFailedRules = /* @__PURE__ */ new Set();
1976
- const sessionAttempts = sessions.get(bc.sessionId) ?? [];
2293
+ const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
1977
2294
  for (const a of sessionAttempts) {
1978
2295
  const ad = a.data;
1979
2296
  if (ad.validationPassed === false) {
@@ -2056,8 +2373,55 @@ var PatternAnalyzer = class _PatternAnalyzer {
2056
2373
  };
2057
2374
  const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
2058
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);
2059
2381
  return analysis;
2060
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
+ }
2061
2425
  async getHistory(limit = 20) {
2062
2426
  try {
2063
2427
  const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
@@ -2079,7 +2443,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
2079
2443
  alerts.push({
2080
2444
  type: "stale_pattern",
2081
2445
  rule: p.rule,
2082
- message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-23)`
2446
+ message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-26)`
2083
2447
  });
2084
2448
  }
2085
2449
  }
@@ -2211,6 +2575,43 @@ ${regularLines}`;
2211
2575
  }
2212
2576
  };
2213
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
+
2214
2615
  // src/utils/logger.ts
2215
2616
  var nullLogger = {
2216
2617
  debug() {
@@ -2223,19 +2624,91 @@ var nullLogger = {
2223
2624
  }
2224
2625
  };
2225
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
+
2226
2677
  // src/mcp-server.ts
2227
2678
  var import_node_fs3 = require("fs");
2228
- var import_node_path6 = require("path");
2679
+ var import_node_path7 = require("path");
2680
+ var import_node_os6 = require("os");
2229
2681
  var import_node_url = require("url");
2230
2682
  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"));
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"));
2233
2685
  var library = new FileLibrary();
2234
2686
  var validator = new N8nValidator();
2235
2687
  var nodeSyncer = new NodeSyncer();
2236
2688
  var lastSync = null;
2237
2689
  var stripper = new N8nFieldStripper();
2238
- 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
+ }
2239
2712
  function getTelemetryReader() {
2240
2713
  try {
2241
2714
  return new TelemetryReader();
@@ -2283,6 +2756,7 @@ server.tool(
2283
2756
  name: import_zod.z.string().optional().describe("Optional workflow name override")
2284
2757
  },
2285
2758
  async ({ description, name }) => {
2759
+ evictStaleSessions();
2286
2760
  const baseUrl = process.env["N8N_BASE_URL"];
2287
2761
  const apiKey = process.env["N8N_API_KEY"];
2288
2762
  if (!baseUrl || !apiKey) {
@@ -2294,6 +2768,8 @@ server.tool(
2294
2768
  isError: true
2295
2769
  };
2296
2770
  }
2771
+ const runId = generateUUID();
2772
+ const workflowType = inferWorkflowType(description);
2297
2773
  await library.initialize();
2298
2774
  const syncResult = await autoSync();
2299
2775
  const matches = await library.search(description);
@@ -2301,11 +2777,22 @@ server.tool(
2301
2777
  const failureRates = await telemetryReader?.getFailureRates() ?? [];
2302
2778
  const request = { description, ...name ? { name } : {} };
2303
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
+ }
2304
2790
  const systemText = built.system.map((block) => block.text).join("\n\n---\n\n");
2305
2791
  return {
2306
2792
  content: [{
2307
2793
  type: "text",
2308
2794
  text: JSON.stringify({
2795
+ kairos_run_id: runId,
2309
2796
  mode: built.mode,
2310
2797
  matchCount: matches.length,
2311
2798
  topMatchScore: matches[0]?.score ?? null,
@@ -2337,11 +2824,12 @@ server.tool(
2337
2824
  );
2338
2825
  server.tool(
2339
2826
  "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.",
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.",
2341
2828
  {
2342
- 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")
2343
2831
  },
2344
- async ({ workflow: workflowStr }) => {
2832
+ async ({ workflow: workflowStr, kairos_run_id }) => {
2345
2833
  let parsed;
2346
2834
  try {
2347
2835
  parsed = JSON.parse(workflowStr);
@@ -2359,6 +2847,24 @@ server.tool(
2359
2847
  const result = validator.validate(parsed);
2360
2848
  const errors = result.issues.filter((i) => i.severity === "error");
2361
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
+ }
2362
2868
  return {
2363
2869
  content: [{
2364
2870
  type: "text",
@@ -2387,9 +2893,10 @@ server.tool(
2387
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.",
2388
2894
  {
2389
2895
  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")
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")
2391
2898
  },
2392
- async ({ workflow: workflowStr, activate }) => {
2899
+ async ({ workflow: workflowStr, activate, kairos_run_id }) => {
2393
2900
  if (!isAllowed("deploy")) {
2394
2901
  return {
2395
2902
  content: [{
@@ -2449,6 +2956,28 @@ server.tool(
2449
2956
  generationMode: "scratch",
2450
2957
  generationAttempts: 1
2451
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
+ }
2452
2981
  return {
2453
2982
  content: [{
2454
2983
  type: "text",