@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.
package/dist/cli.cjs CHANGED
@@ -524,6 +524,9 @@ var N8nValidator = class {
524
524
  this.checkRule21(workflow, issues);
525
525
  this.checkRule22(workflow, issues);
526
526
  this.checkRule23(workflow, issues);
527
+ this.checkRule24(workflow, issues);
528
+ this.checkRule25(workflow, issues);
529
+ this.checkRule26(workflow, issues);
527
530
  if (Array.isArray(workflow.nodes)) {
528
531
  const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
529
532
  for (const issue of issues) {
@@ -656,10 +659,14 @@ var N8nValidator = class {
656
659
  checkRule11(w, issues) {
657
660
  if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
658
661
  const reachable = /* @__PURE__ */ new Set();
659
- for (const [, outputs] of Object.entries(w.connections)) {
662
+ const aiSubNodeSources = /* @__PURE__ */ new Set();
663
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
660
664
  if (typeof outputs !== "object" || outputs === null) continue;
661
- for (const portGroup of Object.values(outputs)) {
665
+ let hasAiPort = false;
666
+ for (const [portName, portGroup] of Object.entries(outputs)) {
662
667
  if (!Array.isArray(portGroup)) continue;
668
+ const isAiPort = portName.startsWith("ai_");
669
+ if (isAiPort) hasAiPort = true;
663
670
  for (const targets of portGroup) {
664
671
  if (!Array.isArray(targets)) continue;
665
672
  for (const target of targets) {
@@ -668,10 +675,13 @@ var N8nValidator = class {
668
675
  }
669
676
  }
670
677
  }
678
+ if (hasAiPort) aiSubNodeSources.add(sourceName);
671
679
  }
672
680
  for (const node of w.nodes) {
673
681
  if (node.type.includes("stickyNote")) continue;
674
- if (!this.isTriggerNode(node) && !reachable.has(node.name)) {
682
+ if (this.isTriggerNode(node)) continue;
683
+ if (aiSubNodeSources.has(node.name)) continue;
684
+ if (!reachable.has(node.name)) {
675
685
  this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
676
686
  }
677
687
  }
@@ -867,6 +877,76 @@ var N8nValidator = class {
867
877
  }
868
878
  }
869
879
  }
880
+ // Rule 24 (WARN): deprecated accessor syntax in expressions
881
+ checkRule24(w, issues) {
882
+ if (!Array.isArray(w.nodes)) return;
883
+ const deprecated = /\$node\s*\[/;
884
+ for (const node of w.nodes) {
885
+ for (const expr of this.extractExpressions(node.parameters)) {
886
+ if (deprecated.test(expr)) {
887
+ this.warn(
888
+ issues,
889
+ 24,
890
+ `Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
891
+ node.id
892
+ );
893
+ break;
894
+ }
895
+ }
896
+ }
897
+ }
898
+ // Rule 25 (WARN): wrong item index assumptions in expressions
899
+ checkRule25(w, issues) {
900
+ if (!Array.isArray(w.nodes)) return;
901
+ const itemIndex = /\$json\s*\.\s*items\s*\[/;
902
+ for (const node of w.nodes) {
903
+ for (const expr of this.extractExpressions(node.parameters)) {
904
+ if (itemIndex.test(expr)) {
905
+ this.warn(
906
+ issues,
907
+ 25,
908
+ `Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
909
+ node.id
910
+ );
911
+ break;
912
+ }
913
+ }
914
+ }
915
+ }
916
+ // Rule 26 (WARN): missing .first() or .all() on node references
917
+ checkRule26(w, issues) {
918
+ if (!Array.isArray(w.nodes)) return;
919
+ const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
920
+ for (const node of w.nodes) {
921
+ for (const expr of this.extractExpressions(node.parameters)) {
922
+ if (bareRef.test(expr)) {
923
+ this.warn(
924
+ issues,
925
+ 26,
926
+ `Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
927
+ node.id
928
+ );
929
+ break;
930
+ }
931
+ }
932
+ }
933
+ }
934
+ extractExpressions(params) {
935
+ const expressions = [];
936
+ const walk = (val) => {
937
+ if (typeof val === "string") {
938
+ if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
939
+ expressions.push(val);
940
+ }
941
+ } else if (Array.isArray(val)) {
942
+ for (const item of val) walk(item);
943
+ } else if (val !== null && typeof val === "object") {
944
+ for (const v of Object.values(val)) walk(v);
945
+ }
946
+ };
947
+ walk(params);
948
+ return expressions;
949
+ }
870
950
  // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
871
951
  checkRule21(w, issues) {
872
952
  if (!Array.isArray(w.nodes)) return;
@@ -954,9 +1034,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
954
1034
  - Never reuse IDs, never use sequential fake IDs like "node-1"
955
1035
 
956
1036
  ### Credentials:
957
- - Only reference credentials with exact type names (see catalog below)
958
- - If credential ID is unknown, OMIT the credentials block entirely \u2014 never invent credential IDs
959
- - Never put API keys or tokens in parameters when a credential type exists
1037
+ - Each credential is keyed by its type string, with an object value containing id and name:
1038
+ "credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack Credential" } }
1039
+ - Use "placeholder-id" as the id \u2014 users replace this with their real credential ID from n8n after deployment
1040
+ - The credentialsNeeded field in your response declares what credentials the user must configure
1041
+ - Never put API keys or tokens directly in node parameters when a credential type exists
960
1042
 
961
1043
  ### Node names:
962
1044
  - All node names must be unique within the workflow
@@ -1003,6 +1085,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
1003
1085
 
1004
1086
  ---
1005
1087
 
1088
+ ## EXPRESSION SYNTAX \u2014 how to reference upstream node data
1089
+
1090
+ ### Accessing a field from an upstream node:
1091
+ - CORRECT: $('NodeName').item.json.field
1092
+ - WRONG: $node["NodeName"].json.field \u2190 deprecated accessor, fails at runtime (Rule 24)
1093
+
1094
+ ### Accessing array items from $json:
1095
+ - CORRECT: $json.field \u2190 n8n auto-flattens items; each item is already a flat object
1096
+ - WRONG: $json.items[0].field \u2190 do not index into items[] (Rule 25)
1097
+
1098
+ ### Calling node data \u2014 always qualify with .first() or .all():
1099
+ - CORRECT: $('NodeName').first().json.field \u2190 single item
1100
+ - CORRECT: $('NodeName').all() \u2190 array of all items
1101
+ - WRONG: $('NodeName').json \u2190 throws at runtime without .first() or .all() (Rule 26)
1102
+
1103
+ ---
1104
+
1006
1105
  ## NODE CATALOG \u2014 exact type strings and safe typeVersions
1007
1106
 
1008
1107
  ### Triggers (always at least one required):
@@ -1102,6 +1201,9 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
1102
1201
  5. At least one trigger node present
1103
1202
  6. Every AI Agent has an ai_languageModel sub-node
1104
1203
  7. settings block is complete with executionOrder: "v1"
1204
+ 8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
1205
+ 9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
1206
+ 10. No bare $('NodeName').json \u2014 always use .first().json.field or .all()
1105
1207
 
1106
1208
  ---
1107
1209
 
@@ -1118,7 +1220,7 @@ function scoreToMode(score) {
1118
1220
  }
1119
1221
 
1120
1222
  // src/validation/rule-metadata.ts
1121
- var VALIDATOR_RULE_IDS = Array.from({ length: 23 }, (_, i) => i + 1);
1223
+ var VALIDATOR_RULE_IDS = Array.from({ length: 26 }, (_, i) => i + 1);
1122
1224
  var RULE_PIPELINE_STAGES = {
1123
1225
  1: "node_generation",
1124
1226
  2: "node_generation",
@@ -1142,7 +1244,28 @@ var RULE_PIPELINE_STAGES = {
1142
1244
  20: "connection_wiring",
1143
1245
  21: "workflow_structure",
1144
1246
  22: "workflow_structure",
1145
- 23: "node_generation"
1247
+ 23: "node_generation",
1248
+ 24: "expression_syntax",
1249
+ 25: "expression_syntax",
1250
+ 26: "expression_syntax"
1251
+ };
1252
+ var RULE_EXAMPLES = {
1253
+ 17: {
1254
+ bad: '"credentials": { "slackOAuth2Api": "my-token" }',
1255
+ good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
1256
+ },
1257
+ 24: {
1258
+ bad: '$node["Fetch Data"].json.email',
1259
+ good: "$('Fetch Data').item.json.email"
1260
+ },
1261
+ 25: {
1262
+ bad: "$json.items[0].email",
1263
+ good: "$json.email"
1264
+ },
1265
+ 26: {
1266
+ bad: "$('Fetch Data').json.email",
1267
+ good: "$('Fetch Data').first().json.email"
1268
+ }
1146
1269
  };
1147
1270
  var RULE_MITIGATIONS = {
1148
1271
  1: "Provide a non-empty workflow name string",
@@ -1161,21 +1284,44 @@ var RULE_MITIGATIONS = {
1161
1284
  14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
1162
1285
  15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1163
1286
  16: "All node names must be unique within the workflow",
1164
- 17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
1287
+ 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',
1165
1288
  18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
1166
1289
  19: "Use known safe typeVersion values for each node type",
1167
1290
  20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1168
1291
  21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1169
1292
  22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
1170
- 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync"
1293
+ 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
1294
+ 24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
1295
+ 25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
1296
+ 26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
1171
1297
  };
1172
1298
 
1173
1299
  // src/generation/prompt-builder.ts
1174
1300
  var CRITICAL_SCORE_THRESHOLD = 0.15;
1301
+ function resolveProfile() {
1302
+ const env = process.env["KAIROS_PROMPT_PROFILE"];
1303
+ if (env === "minimal" || env === "standard" || env === "rich") return env;
1304
+ return "standard";
1305
+ }
1306
+ var PROACTIVE_EXPRESSION_GUIDANCE = `## Expression Syntax Quick Reference
1307
+
1308
+ Always use these patterns in expressions:
1309
+ - Access node data: $('NodeName').item.json.field (not $node["NodeName"].json)
1310
+ - Access JSON field: $json.field (not $json.items[0].field)
1311
+ - Single item: $('NodeName').first().json.field
1312
+ - All items: $('NodeName').all()`;
1175
1313
  var PromptBuilder = class {
1176
1314
  patternsPath;
1177
- constructor(patternsPath) {
1315
+ profile;
1316
+ _lastActivePatterns = null;
1317
+ constructor(patternsPath, profile) {
1178
1318
  this.patternsPath = patternsPath ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "patterns.json");
1319
+ this.profile = profile ?? resolveProfile();
1320
+ }
1321
+ resolveMaxPatterns() {
1322
+ if (this.profile === "minimal") return 3;
1323
+ if (this.profile === "rich") return 15;
1324
+ return 10;
1179
1325
  }
1180
1326
  build(request, matches, globalFailureRates = [], dynamicCatalog) {
1181
1327
  const mode = this.resolveMode(matches);
@@ -1213,53 +1359,62 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
1213
1359
  cache_control: { type: "ephemeral" }
1214
1360
  }
1215
1361
  ];
1216
- if (mode === "reference" && matches.length > 0) {
1217
- const refText = matches.slice(0, 3).map((m) => {
1218
- const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1219
- return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1362
+ if (this.profile !== "minimal") {
1363
+ if (mode === "reference" && matches.length > 0) {
1364
+ const refText = matches.slice(0, 3).map((m) => {
1365
+ const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1366
+ return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1220
1367
  Nodes:
1221
1368
  ${nodes}`;
1222
- }).join("\n\n");
1223
- blocks.push({
1224
- type: "text",
1225
- text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1226
-
1227
- ${refText}`
1228
- });
1229
- }
1230
- if (mode === "direct" && matches[0]) {
1231
- const match = matches[0];
1232
- const json = JSON.stringify(match.workflow.workflow, null, 2);
1233
- if (json.length > 3e4) {
1234
- const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1369
+ }).join("\n\n");
1235
1370
  blocks.push({
1236
1371
  type: "text",
1237
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1372
+ text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1373
+
1374
+ ${refText}`
1375
+ });
1376
+ }
1377
+ if (mode === "direct" && matches[0]) {
1378
+ const match = matches[0];
1379
+ const json = JSON.stringify(match.workflow.workflow, null, 2);
1380
+ if (json.length > 3e4) {
1381
+ const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1382
+ blocks.push({
1383
+ type: "text",
1384
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1238
1385
  Nodes:
1239
1386
  ${nodes}`
1240
- });
1241
- } else {
1242
- blocks.push({
1243
- type: "text",
1244
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1387
+ });
1388
+ } else {
1389
+ blocks.push({
1390
+ type: "text",
1391
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1245
1392
 
1246
1393
  ${json}`
1247
- });
1394
+ });
1395
+ }
1248
1396
  }
1249
- }
1250
- if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1251
- const hint = matches[0];
1252
- const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1253
- blocks.push({
1254
- type: "text",
1255
- text: `## Weak Structural Hint
1397
+ if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1398
+ const hint = matches[0];
1399
+ const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1400
+ blocks.push({
1401
+ type: "text",
1402
+ text: `## Weak Structural Hint
1256
1403
  A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
1257
- });
1404
+ });
1405
+ }
1258
1406
  }
1259
1407
  const warnings = this.buildFailureWarnings(matches, globalFailureRates);
1260
1408
  if (warnings) {
1261
1409
  blocks.push({ type: "text", text: warnings });
1262
1410
  }
1411
+ if (this.profile === "rich") {
1412
+ const expressionRules = /* @__PURE__ */ new Set([24, 25, 26]);
1413
+ const expressionAlreadyCovered = (this._lastActivePatterns ?? []).some((p) => expressionRules.has(p.rule));
1414
+ if (!expressionAlreadyCovered) {
1415
+ blocks.push({ type: "text", text: PROACTIVE_EXPRESSION_GUIDANCE });
1416
+ }
1417
+ }
1263
1418
  return blocks;
1264
1419
  }
1265
1420
  loadPatterns() {
@@ -1273,18 +1428,19 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1273
1428
  }
1274
1429
  }
1275
1430
  getWarnedRules() {
1276
- return this.getActivePatterns().map((p) => p.rule);
1431
+ const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
1432
+ return patterns.map((p) => p.rule);
1277
1433
  }
1278
- getActivePatterns() {
1279
- const MAX_WARNED = 10;
1434
+ getActivePatterns(maxCount = 10) {
1280
1435
  const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
1281
1436
  const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
1282
1437
  const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1283
1438
  const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1284
- return [...regressed, ...confirmed, ...drafts].slice(0, MAX_WARNED);
1439
+ return [...regressed, ...confirmed, ...drafts].slice(0, maxCount);
1285
1440
  }
1286
1441
  buildFailureWarnings(matches, globalFailureRates) {
1287
- const richPatterns = this.getActivePatterns();
1442
+ const richPatterns = this.getActivePatterns(this.resolveMaxPatterns());
1443
+ this._lastActivePatterns = richPatterns;
1288
1444
  if (richPatterns.length > 0) {
1289
1445
  return this.buildStageGroupedWarnings(richPatterns, matches);
1290
1446
  }
@@ -1295,7 +1451,8 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1295
1451
  credential_injection: "CREDENTIAL FORMATTING",
1296
1452
  connection_wiring: "CONNECTION WIRING",
1297
1453
  node_generation: "NODE GENERATION",
1298
- workflow_structure: "WORKFLOW STRUCTURE"
1454
+ workflow_structure: "WORKFLOW STRUCTURE",
1455
+ expression_syntax: "EXPRESSION SYNTAX"
1299
1456
  };
1300
1457
  const byStage = /* @__PURE__ */ new Map();
1301
1458
  for (const p of patterns) {
@@ -1323,7 +1480,11 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1323
1480
  const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
1324
1481
  const remedyStr = remedy ? `
1325
1482
  Fix: ${remedy}` : "";
1326
- lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}`);
1483
+ const ex = RULE_EXAMPLES[p.rule];
1484
+ const exampleStr = ex ? `
1485
+ Bad: ${ex.bad}
1486
+ Good: ${ex.good}` : "";
1487
+ lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}${exampleStr}`);
1327
1488
  } else {
1328
1489
  const ruleNums = group.map((p) => p.rule).join(", ");
1329
1490
  const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
@@ -1430,12 +1591,12 @@ var GENERATE_WORKFLOW_TOOL = {
1430
1591
  }
1431
1592
  };
1432
1593
  var WorkflowDesigner = class {
1433
- constructor(anthropic, model, logger) {
1594
+ constructor(anthropic, model, logger, patternsPath) {
1434
1595
  this.anthropic = anthropic;
1435
1596
  this.model = model;
1436
1597
  this.logger = logger;
1437
1598
  this.validator = new N8nValidator();
1438
- this.promptBuilder = new PromptBuilder();
1599
+ this.promptBuilder = new PromptBuilder(patternsPath);
1439
1600
  }
1440
1601
  anthropic;
1441
1602
  model;
@@ -1519,6 +1680,11 @@ var WorkflowDesigner = class {
1519
1680
  }
1520
1681
  }
1521
1682
  extractToolUse(message) {
1683
+ if (message.stop_reason === "max_tokens") {
1684
+ throw new GenerationError(
1685
+ "Claude response was truncated (max_tokens reached) \u2014 the workflow may be too large. Try a simpler description or break it into smaller workflows."
1686
+ );
1687
+ }
1522
1688
  const toolUseBlock = message.content.find(
1523
1689
  (block) => block.type === "tool_use"
1524
1690
  );
@@ -1561,11 +1727,12 @@ var TelemetryCollector = class {
1561
1727
  this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
1562
1728
  this.sessionId = generateUUID();
1563
1729
  }
1564
- async emit(eventType, data) {
1730
+ async emit(eventType, data, runId) {
1565
1731
  const event = {
1566
1732
  schemaVersion: TELEMETRY_SCHEMA_VERSION,
1567
1733
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1568
1734
  sessionId: this.sessionId,
1735
+ ...runId ? { runId } : {},
1569
1736
  eventType,
1570
1737
  data
1571
1738
  };
@@ -1691,6 +1858,7 @@ var PATTERN_SCHEMA_VERSION = 2;
1691
1858
  var PatternAnalyzer = class _PatternAnalyzer {
1692
1859
  telemetryDir;
1693
1860
  outputDir;
1861
+ _cachedEvents = null;
1694
1862
  constructor(telemetryDir) {
1695
1863
  const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
1696
1864
  this.telemetryDir = telemetryDir ?? defaultDir;
@@ -1719,19 +1887,23 @@ var PatternAnalyzer = class _PatternAnalyzer {
1719
1887
  }));
1720
1888
  }
1721
1889
  if (fromVersion < 2) {
1722
- migrated = migrated.map((p) => ({
1723
- ...p,
1724
- scoringFactors: {
1725
- ...p.scoringFactors,
1726
- stickinessBoost: p.scoringFactors.stickinessBoost ?? p.scoringFactors["validationBoost"] ?? 0
1727
- }
1728
- }));
1890
+ migrated = migrated.map((p) => {
1891
+ const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
1892
+ return {
1893
+ ...p,
1894
+ scoringFactors: {
1895
+ ...sf,
1896
+ stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
1897
+ }
1898
+ };
1899
+ });
1729
1900
  }
1730
1901
  return migrated;
1731
1902
  }
1732
1903
  async analyze(days = 30) {
1733
1904
  const previousPatterns = await this.loadPreviousPatterns();
1734
1905
  const events = await this.readAllEvents(days);
1906
+ this._cachedEvents = events;
1735
1907
  const starts = events.filter((e) => e.eventType === "build_start");
1736
1908
  const attempts = events.filter((e) => e.eventType === "generation_attempt");
1737
1909
  const passed = attempts.filter(
@@ -1744,13 +1916,18 @@ var PatternAnalyzer = class _PatternAnalyzer {
1744
1916
  const credentialFailures = /* @__PURE__ */ new Map();
1745
1917
  for (const a of failed) {
1746
1918
  const weight = this.recencyWeight(a.fileDate);
1919
+ const buildId = a.runId ?? a.sessionId;
1747
1920
  const data = a.data;
1748
1921
  for (const issue of data.issues ?? []) {
1749
- const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [] };
1922
+ if (issue.severity === "warn") continue;
1923
+ const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
1750
1924
  entry.count++;
1751
- entry.sessions.add(a.sessionId);
1925
+ entry.sessions.add(buildId);
1752
1926
  entry.recencyWeights.push(weight);
1753
1927
  entry.allMessages.push(issue.message);
1928
+ if (data.workflowType) {
1929
+ entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
1930
+ }
1754
1931
  ruleFailures.set(issue.rule, entry);
1755
1932
  if (issue.rule === 17) {
1756
1933
  const credPatterns = [
@@ -1803,9 +1980,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
1803
1980
  }
1804
1981
  const sessions = /* @__PURE__ */ new Map();
1805
1982
  for (const a of attempts) {
1806
- const list = sessions.get(a.sessionId) ?? [];
1983
+ const buildId = a.runId ?? a.sessionId;
1984
+ const list = sessions.get(buildId) ?? [];
1807
1985
  list.push(a);
1808
- sessions.set(a.sessionId, list);
1986
+ sessions.set(buildId, list);
1809
1987
  }
1810
1988
  let firstTryPass = 0;
1811
1989
  let correctionNeeded = 0;
@@ -1852,7 +2030,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1852
2030
  const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
1853
2031
  const stickiness = stickinessCount.get(rule) ?? 0;
1854
2032
  const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
1855
- return {
2033
+ const pattern = {
1856
2034
  rule,
1857
2035
  failureCount: entry.count,
1858
2036
  confidence: Math.round(rawConfidence * 1e3) / 1e3,
@@ -1864,6 +2042,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
1864
2042
  exampleMessages: this.deduplicateMessages(entry.allMessages),
1865
2043
  mitigation: RULE_MITIGATIONS[rule] ?? null
1866
2044
  };
2045
+ if (entry.workflowTypes.size > 0) {
2046
+ pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
2047
+ }
2048
+ return pattern;
1867
2049
  }).sort((a, b) => b.compositeScore - a.compositeScore);
1868
2050
  const activeRules = new Set(activePatterns.map((p) => p.rule));
1869
2051
  for (const p of activePatterns) {
@@ -1920,7 +2102,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1920
2102
  const warned = bcData.warnedRules ?? [];
1921
2103
  if (warned.length === 0) continue;
1922
2104
  const sessionFailedRules = /* @__PURE__ */ new Set();
1923
- const sessionAttempts = sessions.get(bc.sessionId) ?? [];
2105
+ const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
1924
2106
  for (const a of sessionAttempts) {
1925
2107
  const ad = a.data;
1926
2108
  if (ad.validationPassed === false) {
@@ -2003,8 +2185,55 @@ var PatternAnalyzer = class _PatternAnalyzer {
2003
2185
  };
2004
2186
  const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
2005
2187
  await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
2188
+ const sessions = await this.buildSessionSummaries(days);
2189
+ const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
2190
+ const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
2191
+ await (0, import_promises3.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
2192
+ await (0, import_promises3.rename)(sessionHistoryTmp, sessionHistoryPath);
2006
2193
  return analysis;
2007
2194
  }
2195
+ async getSessions(limit = 20) {
2196
+ try {
2197
+ const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
2198
+ const all = JSON.parse(raw);
2199
+ return all.slice(-limit);
2200
+ } catch {
2201
+ return [];
2202
+ }
2203
+ }
2204
+ async buildSessionSummaries(days = 30) {
2205
+ const events = this._cachedEvents ?? await this.readAllEvents(days);
2206
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
2207
+ const attemptsByBuild = /* @__PURE__ */ new Map();
2208
+ for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
2209
+ const buildId = e.runId ?? e.sessionId;
2210
+ const list = attemptsByBuild.get(buildId) ?? [];
2211
+ list.push(e);
2212
+ attemptsByBuild.set(buildId, list);
2213
+ }
2214
+ const summaries = buildCompletes.map((bc) => {
2215
+ const data = bc.data;
2216
+ const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
2217
+ const failedRules = Array.from(new Set(
2218
+ sessionAttempts.flatMap((a) => {
2219
+ const ad = a.data;
2220
+ if (ad.validationPassed !== false) return [];
2221
+ return (ad.issues ?? []).map((i) => i.rule);
2222
+ })
2223
+ ));
2224
+ return {
2225
+ sessionId: bc.sessionId,
2226
+ date: bc.fileDate,
2227
+ description: data.description ?? "",
2228
+ workflowType: data.workflowType ?? null,
2229
+ attempts: data.totalAttempts ?? 1,
2230
+ success: data.success ?? false,
2231
+ failedRules,
2232
+ workflowName: data.workflowName ?? null
2233
+ };
2234
+ });
2235
+ return summaries.sort((a, b) => a.date.localeCompare(b.date));
2236
+ }
2008
2237
  async getHistory(limit = 20) {
2009
2238
  try {
2010
2239
  const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
@@ -2026,7 +2255,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
2026
2255
  alerts.push({
2027
2256
  type: "stale_pattern",
2028
2257
  rule: p.rule,
2029
- message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-23)`
2258
+ message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-26)`
2030
2259
  });
2031
2260
  }
2032
2261
  }
@@ -2113,7 +2342,59 @@ var nullLogger = {
2113
2342
  }
2114
2343
  };
2115
2344
 
2345
+ // src/utils/workflow-type.ts
2346
+ var TYPE_KEYWORDS = [
2347
+ ["gmail", "email"],
2348
+ ["imap", "email"],
2349
+ ["smtp", "email"],
2350
+ [" email", "email"],
2351
+ ["slack", "slack"],
2352
+ ["telegram", "messaging"],
2353
+ ["discord", "messaging"],
2354
+ [" sms", "messaging"],
2355
+ ["twilio", "messaging"],
2356
+ ["webhook", "webhook"],
2357
+ ["google sheets", "data"],
2358
+ ["spreadsheet", "data"],
2359
+ ["airtable", "data"],
2360
+ ["notion", "data"],
2361
+ ["github", "devops"],
2362
+ ["gitlab", "devops"],
2363
+ ["schedule", "schedule"],
2364
+ [" cron", "schedule"],
2365
+ ["daily", "schedule"],
2366
+ ["weekly", "schedule"],
2367
+ ["hourly", "schedule"],
2368
+ ["every day", "schedule"],
2369
+ ["every hour", "schedule"],
2370
+ ["every morning", "schedule"],
2371
+ ["postgres", "database"],
2372
+ ["mysql", "database"],
2373
+ ["supabase", "database"],
2374
+ ["redis", "database"],
2375
+ [" database", "database"],
2376
+ [" llm", "ai"],
2377
+ [" gpt", "ai"],
2378
+ ["claude", "ai"],
2379
+ [" agent", "ai"],
2380
+ ["langchain", "ai"],
2381
+ [" ai ", "ai"],
2382
+ [" ai", "ai"],
2383
+ ["http request", "api"],
2384
+ ["rest api", "api"],
2385
+ [" api", "api"]
2386
+ ];
2387
+ function inferWorkflowType(description) {
2388
+ const lower = " " + description.toLowerCase();
2389
+ for (const [keyword, type] of TYPE_KEYWORDS) {
2390
+ if (lower.includes(keyword)) return type;
2391
+ }
2392
+ return null;
2393
+ }
2394
+
2116
2395
  // src/client.ts
2396
+ var import_node_os5 = require("os");
2397
+ var import_node_path6 = require("path");
2117
2398
  var DEFAULT_MODEL = "claude-sonnet-4-6";
2118
2399
  var Kairos = class {
2119
2400
  provider;
@@ -2142,7 +2423,8 @@ var Kairos = class {
2142
2423
  this.provider = null;
2143
2424
  }
2144
2425
  const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
2145
- this.designer = new WorkflowDesigner(anthropic, this.model, logger);
2426
+ const patternsPath = typeof options.telemetry === "string" ? (0, import_node_path6.join)(options.telemetry, "..", "patterns.json") : (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".kairos", "patterns.json");
2427
+ this.designer = new WorkflowDesigner(anthropic, this.model, logger, patternsPath);
2146
2428
  this.validator = new N8nValidator();
2147
2429
  this.library = options.library ?? new NullLibrary();
2148
2430
  this.logger = logger;
@@ -2175,11 +2457,13 @@ var Kairos = class {
2175
2457
  this.validateDescription(description);
2176
2458
  this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
2177
2459
  const buildStart = Date.now();
2460
+ const runId = generateUUID();
2461
+ const workflowType = inferWorkflowType(description);
2178
2462
  await this.telemetry?.emit("build_start", {
2179
2463
  description,
2180
2464
  model: this.model,
2181
2465
  dryRun: options?.dryRun ?? false
2182
- });
2466
+ }, runId);
2183
2467
  await this.library.initialize();
2184
2468
  const matches = await this.library.search(description);
2185
2469
  if (matches.length > 0) {
@@ -2214,8 +2498,9 @@ var Kairos = class {
2214
2498
  tokensOutput: meta.tokensOutput,
2215
2499
  validationPassed: meta.validationPassed,
2216
2500
  issueCount: meta.issues.length,
2217
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
2218
- });
2501
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
2502
+ workflowType
2503
+ }, runId);
2219
2504
  }
2220
2505
  await this.telemetry?.emit("build_complete", {
2221
2506
  description,
@@ -2228,13 +2513,14 @@ var Kairos = class {
2228
2513
  workflowId: null,
2229
2514
  dryRun: options?.dryRun ?? false,
2230
2515
  credentialsNeeded: 0,
2231
- warnedRules: err.warnedRules ?? []
2232
- });
2516
+ warnedRules: err.warnedRules ?? [],
2517
+ workflowType
2518
+ }, runId);
2233
2519
  this.updatePatterns();
2234
2520
  }
2235
2521
  throw err;
2236
2522
  }
2237
- await this.emitAttemptTelemetry(description, designResult);
2523
+ await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
2238
2524
  const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
2239
2525
  this.saveToLibrary(workflow, description, designResult, matches);
2240
2526
  if (options?.dryRun) {
@@ -2251,8 +2537,9 @@ var Kairos = class {
2251
2537
  workflowId: null,
2252
2538
  dryRun: true,
2253
2539
  credentialsNeeded: designResult.credentialsNeeded.length,
2254
- warnedRules: designResult.warnedRules
2255
- });
2540
+ warnedRules: designResult.warnedRules,
2541
+ workflowType
2542
+ }, runId);
2256
2543
  this.updatePatterns();
2257
2544
  return {
2258
2545
  workflowId: null,
@@ -2283,8 +2570,9 @@ var Kairos = class {
2283
2570
  workflowId: deployed.workflowId,
2284
2571
  dryRun: false,
2285
2572
  credentialsNeeded: designResult.credentialsNeeded.length,
2286
- warnedRules: designResult.warnedRules
2287
- });
2573
+ warnedRules: designResult.warnedRules,
2574
+ workflowType
2575
+ }, runId);
2288
2576
  this.updatePatterns();
2289
2577
  return {
2290
2578
  workflowId: deployed.workflowId,
@@ -2300,11 +2588,13 @@ var Kairos = class {
2300
2588
  this.validateDescription(description);
2301
2589
  this.logger.info("Kairos.update", { id, description });
2302
2590
  const buildStart = Date.now();
2591
+ const runId = generateUUID();
2592
+ const workflowType = inferWorkflowType(description);
2303
2593
  await this.telemetry?.emit("build_start", {
2304
2594
  description,
2305
2595
  model: this.model,
2306
2596
  dryRun: false
2307
- });
2597
+ }, runId);
2308
2598
  await this.library.initialize();
2309
2599
  const matches = await this.library.search(description);
2310
2600
  const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
@@ -2323,8 +2613,9 @@ var Kairos = class {
2323
2613
  tokensOutput: meta.tokensOutput,
2324
2614
  validationPassed: meta.validationPassed,
2325
2615
  issueCount: meta.issues.length,
2326
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
2327
- });
2616
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
2617
+ workflowType
2618
+ }, runId);
2328
2619
  }
2329
2620
  await this.telemetry?.emit("build_complete", {
2330
2621
  description,
@@ -2337,13 +2628,14 @@ var Kairos = class {
2337
2628
  workflowId: null,
2338
2629
  dryRun: false,
2339
2630
  credentialsNeeded: 0,
2340
- warnedRules: err.warnedRules ?? []
2341
- });
2631
+ warnedRules: err.warnedRules ?? [],
2632
+ workflowType
2633
+ }, runId);
2342
2634
  this.updatePatterns();
2343
2635
  }
2344
2636
  throw err;
2345
2637
  }
2346
- await this.emitAttemptTelemetry(description, designResult);
2638
+ await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
2347
2639
  const provider = this.requireProvider();
2348
2640
  const deployed = await provider.update(id, designResult.workflow);
2349
2641
  this.saveToLibrary(designResult.workflow, description, designResult, matches);
@@ -2361,8 +2653,9 @@ var Kairos = class {
2361
2653
  workflowId: deployed.workflowId,
2362
2654
  dryRun: false,
2363
2655
  credentialsNeeded: designResult.credentialsNeeded.length,
2364
- warnedRules: designResult.warnedRules
2365
- });
2656
+ warnedRules: designResult.warnedRules,
2657
+ workflowType
2658
+ }, runId);
2366
2659
  this.updatePatterns();
2367
2660
  return {
2368
2661
  workflowId: deployed.workflowId,
@@ -2385,7 +2678,7 @@ var Kairos = class {
2385
2678
  return null;
2386
2679
  });
2387
2680
  }
2388
- async emitAttemptTelemetry(description, designResult) {
2681
+ async emitAttemptTelemetry(description, designResult, workflowType, runId) {
2389
2682
  for (const meta of designResult.attemptMetadata) {
2390
2683
  await this.telemetry?.emit("generation_attempt", {
2391
2684
  description,
@@ -2396,8 +2689,9 @@ var Kairos = class {
2396
2689
  tokensOutput: meta.tokensOutput,
2397
2690
  validationPassed: meta.validationPassed,
2398
2691
  issueCount: meta.issues.length,
2399
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
2400
- });
2692
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
2693
+ workflowType
2694
+ }, runId);
2401
2695
  }
2402
2696
  }
2403
2697
  recordDeploy() {
@@ -2496,8 +2790,8 @@ var Kairos = class {
2496
2790
 
2497
2791
  // src/library/file-library.ts
2498
2792
  var import_promises4 = require("fs/promises");
2499
- var import_node_path6 = require("path");
2500
- var import_node_os5 = require("os");
2793
+ var import_node_path7 = require("path");
2794
+ var import_node_os6 = require("os");
2501
2795
 
2502
2796
  // src/library/scorer.ts
2503
2797
  var WEIGHTS = {
@@ -2727,13 +3021,27 @@ function buildSearchCorpus(w) {
2727
3021
  return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
2728
3022
  }
2729
3023
  var MAX_LIBRARY_SIZE = 500;
3024
+ function isValidMeta(item) {
3025
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
3026
+ }
3027
+ function isValidOldEntry(item) {
3028
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
3029
+ item.workflow.nodes
3030
+ );
3031
+ }
2730
3032
  var FileLibrary = class {
2731
3033
  dir;
2732
- workflows = [];
3034
+ meta = [];
2733
3035
  initPromise = null;
2734
3036
  writeQueue = Promise.resolve();
2735
3037
  constructor(dir) {
2736
- this.dir = dir ?? (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".kairos", "library");
3038
+ this.dir = dir ?? (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".kairos", "library");
3039
+ }
3040
+ get workflowsDir() {
3041
+ return (0, import_node_path7.join)(this.dir, "workflows");
3042
+ }
3043
+ workflowFilePath(id) {
3044
+ return (0, import_node_path7.join)(this.workflowsDir, `${id}.json`);
2737
3045
  }
2738
3046
  async initialize() {
2739
3047
  if (!this.initPromise) {
@@ -2743,61 +3051,148 @@ var FileLibrary = class {
2743
3051
  }
2744
3052
  async doInitialize() {
2745
3053
  await (0, import_promises4.mkdir)(this.dir, { recursive: true });
2746
- const indexPath = (0, import_node_path6.join)(this.dir, "index.json");
3054
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
3055
+ let workflowsDirExists = false;
2747
3056
  try {
2748
- const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
2749
- const parsed = JSON.parse(raw);
2750
- if (!Array.isArray(parsed)) {
2751
- this.workflows = [];
2752
- } else {
2753
- this.workflows = parsed.filter(
2754
- (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)
2755
- );
3057
+ await (0, import_promises4.stat)(this.workflowsDir);
3058
+ workflowsDirExists = true;
3059
+ } catch {
3060
+ }
3061
+ if (workflowsDirExists) {
3062
+ try {
3063
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3064
+ const parsed = JSON.parse(raw);
3065
+ if (Array.isArray(parsed)) {
3066
+ this.meta = parsed.filter(isValidMeta);
3067
+ }
3068
+ } catch {
3069
+ this.meta = [];
3070
+ }
3071
+ } else {
3072
+ try {
3073
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3074
+ const parsed = JSON.parse(raw);
3075
+ if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
3076
+ await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
3077
+ return;
3078
+ }
3079
+ } catch {
2756
3080
  }
3081
+ this.meta = [];
3082
+ await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
3083
+ }
3084
+ }
3085
+ /**
3086
+ * One-time transparent migration from v0.4.x monolithic index.json.
3087
+ * Splits each stored workflow into a per-file workflow JSON and a lightweight
3088
+ * meta entry. Rewrites index.json in the new format.
3089
+ */
3090
+ async migrateFromMonolithic(oldEntries) {
3091
+ await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
3092
+ const newMeta = [];
3093
+ for (const entry of oldEntries) {
3094
+ const wfPath = this.workflowFilePath(entry.id);
3095
+ const tmpPath = `${wfPath}.tmp`;
3096
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
3097
+ await (0, import_promises4.rename)(tmpPath, wfPath);
3098
+ const { workflow, ...metaFields } = entry;
3099
+ newMeta.push({
3100
+ ...metaFields,
3101
+ workflowName: workflow.name,
3102
+ cachedNodeTypes: workflow.nodes.map((n) => n.type)
3103
+ });
3104
+ }
3105
+ this.meta = newMeta;
3106
+ await this.persistNow();
3107
+ }
3108
+ async loadWorkflowFile(id) {
3109
+ try {
3110
+ const raw = await (0, import_promises4.readFile)(this.workflowFilePath(id), "utf-8");
3111
+ return JSON.parse(raw);
2757
3112
  } catch {
2758
- this.workflows = [];
3113
+ return null;
2759
3114
  }
2760
3115
  }
3116
+ async writeWorkflowFile(id, workflow) {
3117
+ const wfPath = this.workflowFilePath(id);
3118
+ const tmpPath = `${wfPath}.tmp`;
3119
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
3120
+ await (0, import_promises4.rename)(tmpPath, wfPath);
3121
+ }
3122
+ /**
3123
+ * Build a lightweight StoredWorkflow shell from a meta entry for use in
3124
+ * scoring / clustering. Only node.type is populated in each node — no other
3125
+ * node fields are used by hybridScore or clusterWorkflows.
3126
+ */
3127
+ makeSearchShell(m) {
3128
+ return {
3129
+ ...m,
3130
+ workflow: {
3131
+ name: m.workflowName,
3132
+ nodes: m.cachedNodeTypes.map((type) => ({
3133
+ id: "",
3134
+ name: "",
3135
+ type,
3136
+ typeVersion: 1,
3137
+ position: [0, 0],
3138
+ parameters: {}
3139
+ })),
3140
+ connections: {}
3141
+ }
3142
+ };
3143
+ }
2761
3144
  async search(description, options) {
2762
- const searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
2763
- if (searchable.length === 0) return [];
3145
+ const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
3146
+ if (filteredMeta.length === 0) return [];
2764
3147
  const limit = options?.limit ?? 3;
2765
3148
  const queryTokens = tokenize(description);
2766
3149
  if (queryTokens.length === 0) return [];
2767
- const docTokenArrays = searchable.map((w) => tokenize(buildSearchCorpus(w)));
3150
+ const shells = filteredMeta.map((m) => this.makeSearchShell(m));
3151
+ const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
2768
3152
  const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
2769
- const docCount = searchable.length;
3153
+ const docCount = shells.length;
2770
3154
  const idf = /* @__PURE__ */ new Map();
2771
3155
  const allTokens = new Set(queryTokens);
2772
3156
  for (const token of allTokens) {
2773
3157
  const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
2774
3158
  idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
2775
3159
  }
2776
- const scored = hybridScore(queryTokens, description, searchable, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
2777
- const clusters = clusterWorkflows(searchable);
3160
+ const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
3161
+ const clusters = clusterWorkflows(shells);
2778
3162
  const reranked = rerank(scored, clusters).slice(0, limit);
2779
- const results = reranked.map((m) => {
2780
- return { workflow: m.workflow, score: m.score, mode: scoreToMode(m.score) };
2781
- });
2782
- if (results.length > 0) {
2783
- for (const r of results) {
2784
- r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
2785
- }
2786
- this.persist();
2787
- }
2788
- return results;
3163
+ if (reranked.length === 0) return [];
3164
+ for (const r of reranked) {
3165
+ const m = this.meta.find((m2) => m2.id === r.workflow.id);
3166
+ if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
3167
+ }
3168
+ this.persist();
3169
+ const results = await Promise.all(
3170
+ reranked.map(async (r) => {
3171
+ const m = this.meta.find((meta) => meta.id === r.workflow.id);
3172
+ const workflow = await this.loadWorkflowFile(r.workflow.id);
3173
+ if (!workflow) return null;
3174
+ return {
3175
+ workflow: { ...m, workflow },
3176
+ score: r.score,
3177
+ mode: scoreToMode(r.score)
3178
+ };
3179
+ })
3180
+ );
3181
+ return results.filter((r) => r !== null);
2789
3182
  }
2790
3183
  async save(workflow, metadata) {
2791
3184
  const id = generateUUID();
3185
+ await this.writeWorkflowFile(id, workflow);
2792
3186
  const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
2793
- const stored = {
3187
+ const meta = {
2794
3188
  id,
2795
- workflow,
2796
3189
  description: metadata.description,
2797
3190
  tags: metadata.tags ?? [],
2798
3191
  platform: metadata.platform ?? "n8n",
2799
3192
  deployCount: 0,
2800
3193
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3194
+ workflowName: workflow.name,
3195
+ cachedNodeTypes: workflow.nodes.map((n) => n.type),
2801
3196
  ...failurePatterns?.length ? { failurePatterns } : {},
2802
3197
  ...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
2803
3198
  ...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
@@ -2809,31 +3204,35 @@ var FileLibrary = class {
2809
3204
  ...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
2810
3205
  ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
2811
3206
  };
2812
- this.workflows.push(stored);
2813
- if (this.workflows.length > MAX_LIBRARY_SIZE) {
2814
- this.workflows.sort((a, b) => (b.deployCount ?? 1) - (a.deployCount ?? 1));
2815
- this.workflows = this.workflows.slice(0, MAX_LIBRARY_SIZE);
3207
+ this.meta.push(meta);
3208
+ if (this.meta.length > MAX_LIBRARY_SIZE) {
3209
+ this.meta.sort((a, b) => {
3210
+ if (a.id === id) return -1;
3211
+ if (b.id === id) return 1;
3212
+ return (b.deployCount ?? 0) - (a.deployCount ?? 0);
3213
+ });
3214
+ this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
2816
3215
  }
2817
3216
  await this.persist();
2818
3217
  return id;
2819
3218
  }
2820
3219
  async recordDeployment(id) {
2821
- const w = this.workflows.find((w2) => w2.id === id);
2822
- if (w) {
2823
- w.deployCount++;
2824
- w.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
3220
+ const m = this.meta.find((m2) => m2.id === id);
3221
+ if (m) {
3222
+ m.deployCount++;
3223
+ m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
2825
3224
  await this.persist();
2826
3225
  }
2827
3226
  }
2828
3227
  async recordOutcome(id, outcome) {
2829
- const w = this.workflows.find((w2) => w2.id === id);
2830
- if (!w) return;
3228
+ const m = this.meta.find((m2) => m2.id === id);
3229
+ if (!m) return;
2831
3230
  if (outcome.mode === "direct") {
2832
- w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
3231
+ m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
2833
3232
  } else {
2834
- w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
3233
+ m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
2835
3234
  }
2836
- const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
3235
+ const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
2837
3236
  stats.totalUses++;
2838
3237
  stats.totalAttempts += outcome.attempts;
2839
3238
  if (outcome.firstTryPass) stats.firstTryPasses++;
@@ -2841,24 +3240,35 @@ var FileLibrary = class {
2841
3240
  const key = String(rule);
2842
3241
  stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
2843
3242
  }
2844
- w.outcomeStats = stats;
3243
+ m.outcomeStats = stats;
2845
3244
  await this.persist();
2846
3245
  }
2847
3246
  async drain() {
2848
3247
  await this.writeQueue;
2849
3248
  }
2850
3249
  async get(id) {
2851
- return this.workflows.find((w) => w.id === id) ?? null;
3250
+ const m = this.meta.find((m2) => m2.id === id);
3251
+ if (!m) return null;
3252
+ const workflow = await this.loadWorkflowFile(id);
3253
+ if (!workflow) return null;
3254
+ return { ...m, workflow };
2852
3255
  }
2853
3256
  async list(filters) {
2854
- let result = this.workflows;
3257
+ let filtered = this.meta;
2855
3258
  if (filters?.platform) {
2856
- result = result.filter((w) => w.platform === filters.platform);
3259
+ filtered = filtered.filter((m) => m.platform === filters.platform);
2857
3260
  }
2858
3261
  if (filters?.tags && filters.tags.length > 0) {
2859
- result = result.filter((w) => filters.tags.some((t) => w.tags.includes(t)));
3262
+ filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
2860
3263
  }
2861
- return result;
3264
+ const results = await Promise.all(
3265
+ filtered.map(async (m) => {
3266
+ const workflow = await this.loadWorkflowFile(m.id);
3267
+ if (!workflow) return null;
3268
+ return { ...m, workflow };
3269
+ })
3270
+ );
3271
+ return results.filter((r) => r !== null);
2862
3272
  }
2863
3273
  deduplicateFailurePatterns(patterns) {
2864
3274
  if (!patterns?.length) return void 0;
@@ -2873,11 +3283,36 @@ var FileLibrary = class {
2873
3283
  }
2874
3284
  return [...map.values()];
2875
3285
  }
3286
+ /**
3287
+ * Direct write used only during migration (before writeQueue is needed).
3288
+ */
3289
+ async persistNow() {
3290
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
3291
+ const tmpPath = `${indexPath}.tmp`;
3292
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
3293
+ await (0, import_promises4.rename)(tmpPath, indexPath);
3294
+ }
2876
3295
  persist() {
2877
3296
  this.writeQueue = this.writeQueue.then(async () => {
2878
- const indexPath = (0, import_node_path6.join)(this.dir, "index.json");
3297
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
3298
+ let onDisk = [];
3299
+ try {
3300
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3301
+ const parsed = JSON.parse(raw);
3302
+ if (Array.isArray(parsed)) {
3303
+ onDisk = parsed.filter(isValidMeta);
3304
+ }
3305
+ } catch {
3306
+ }
3307
+ const ourIds = new Set(this.meta.map((m) => m.id));
3308
+ const external = onDisk.filter((m) => !ourIds.has(m.id));
3309
+ let merged = [...this.meta, ...external];
3310
+ if (merged.length > MAX_LIBRARY_SIZE) {
3311
+ merged.sort((a, b) => (b.deployCount ?? 0) - (a.deployCount ?? 0));
3312
+ merged = merged.slice(0, MAX_LIBRARY_SIZE);
3313
+ }
2879
3314
  const tmpPath = `${indexPath}.tmp`;
2880
- await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
3315
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
2881
3316
  await (0, import_promises4.rename)(tmpPath, indexPath);
2882
3317
  });
2883
3318
  return this.writeQueue;
@@ -3067,6 +3502,7 @@ Usage:
3067
3502
  kairos init First-time setup wizard
3068
3503
  kairos build <description> [options]
3069
3504
  kairos patterns [options]
3505
+ kairos sessions [options]
3070
3506
  kairos list
3071
3507
  kairos get <id>
3072
3508
  kairos activate <id>
@@ -3083,15 +3519,23 @@ Patterns options:
3083
3519
  --days <days> Analysis window (default: 30)
3084
3520
  --json Output raw JSON instead of summary
3085
3521
 
3522
+ Sessions options:
3523
+ --limit <n> Number of recent sessions to show (default: 20)
3524
+ --json Output raw JSON instead of summary
3525
+
3086
3526
  Sync options:
3087
3527
  --max <count> Maximum templates to fetch (default: 500)
3088
3528
 
3089
3529
  Environment variables:
3090
- ANTHROPIC_API_KEY Anthropic API key (required)
3091
- N8N_BASE_URL n8n instance URL (required for deploy, optional for --dry-run)
3092
- N8N_API_KEY n8n API key (required for deploy, optional for --dry-run)
3093
- KAIROS_MODEL Claude model override (default: claude-sonnet-4-6)
3094
- KAIROS_TELEMETRY Set to "true" or a directory path to enable telemetry logging
3530
+ ANTHROPIC_API_KEY Anthropic API key (required)
3531
+ N8N_BASE_URL n8n instance URL (required for deploy, optional for --dry-run)
3532
+ N8N_API_KEY n8n API key (required for deploy, optional for --dry-run)
3533
+ KAIROS_MODEL Claude model override (default: claude-sonnet-4-6)
3534
+ KAIROS_TELEMETRY Set to "true" or a directory path to enable telemetry logging
3535
+ KAIROS_PROMPT_PROFILE minimal | standard | rich (default: standard)
3536
+ minimal: base prompt only, no library context, top 3 patterns
3537
+ standard: full library context, top 10 patterns (default)
3538
+ rich: full library context, top 15 patterns, proactive expression guidance
3095
3539
  `;
3096
3540
  function getEnvOrExit(name) {
3097
3541
  const val = process.env[name];
@@ -3253,7 +3697,8 @@ async function handleDelete(positional, flags) {
3253
3697
  console.log(`Deleted workflow ${id}`);
3254
3698
  }
3255
3699
  async function handleSyncTemplates(flags) {
3256
- const max = typeof flags["max"] === "string" ? parseInt(flags["max"], 10) : 500;
3700
+ const maxRaw = typeof flags["max"] === "string" ? parseInt(flags["max"], 10) : NaN;
3701
+ const max = Number.isNaN(maxRaw) ? 500 : maxRaw;
3257
3702
  const library = new FileLibrary();
3258
3703
  const logger = {
3259
3704
  debug: () => {
@@ -3281,7 +3726,8 @@ async function handleSyncTemplates(flags) {
3281
3726
  console.error(` Paid: ${result.skippedPaid} (skipped)`);
3282
3727
  }
3283
3728
  async function handlePatterns(flags) {
3284
- const days = typeof flags["days"] === "string" ? parseInt(flags["days"], 10) : 30;
3729
+ const daysRaw = typeof flags["days"] === "string" ? parseInt(flags["days"], 10) : NaN;
3730
+ const days = Number.isNaN(daysRaw) ? 30 : daysRaw;
3285
3731
  const analyzer = PatternAnalyzer.fromEnv();
3286
3732
  const analysis = await analyzer.analyzeAndSave(days);
3287
3733
  if (flags["json"] === true) {
@@ -3315,6 +3761,10 @@ Active Failure Patterns:`);
3315
3761
  console.log(` Factors: confidence=${f.rawConfidence} \xD7 impact=${f.impact} \xD7 recency=${f.recency} + boost=${f.stickinessBoost}`);
3316
3762
  if (p.mitigation) console.log(` Fix: ${p.mitigation}`);
3317
3763
  if (p.exampleMessages.length > 0) console.log(` e.g. ${p.exampleMessages[0]}`);
3764
+ if (p.workflowTypeBreakdown) {
3765
+ const topType = Object.entries(p.workflowTypeBreakdown).sort((a, b) => b[1] - a[1])[0];
3766
+ if (topType) console.log(` Top workflow type: ${topType[0]} (${topType[1]} failures)`);
3767
+ }
3318
3768
  }
3319
3769
  } else {
3320
3770
  console.log(`
@@ -3355,10 +3805,35 @@ Drift Detection: ${drift.healthy ? "HEALTHY" : "ALERTS FOUND"}`);
3355
3805
  console.log(`
3356
3806
  Patterns saved to ~/.kairos/patterns.json`);
3357
3807
  }
3808
+ async function handleSessions(flags) {
3809
+ const limitRaw = typeof flags["limit"] === "string" ? parseInt(flags["limit"], 10) : NaN;
3810
+ const limit = Number.isNaN(limitRaw) ? 20 : limitRaw;
3811
+ const analyzer = PatternAnalyzer.fromEnv();
3812
+ const sessions = await analyzer.getSessions(limit);
3813
+ if (flags["json"] === true) {
3814
+ console.log(JSON.stringify(sessions, null, 2));
3815
+ return;
3816
+ }
3817
+ if (sessions.length === 0) {
3818
+ console.log("No session history found. Run kairos patterns first to generate session data.");
3819
+ return;
3820
+ }
3821
+ console.log(`
3822
+ Recent Sessions (last ${sessions.length})`);
3823
+ console.log("\u2500".repeat(60));
3824
+ for (const s of [...sessions].reverse()) {
3825
+ const status = s.success ? "\u2713" : "\u2717";
3826
+ const typeTag = s.workflowType ? ` [${s.workflowType}]` : "";
3827
+ const attemptsStr = s.attempts > 1 ? ` (${s.attempts} attempts)` : "";
3828
+ const nameStr = s.workflowName ? ` ${s.workflowName}` : ` ${s.description.slice(0, 50)}`;
3829
+ const rulesStr = s.failedRules.length > 0 ? ` \u2014 rules ${s.failedRules.join(", ")} failed` : "";
3830
+ console.log(`${s.date} ${status}${nameStr}${attemptsStr}${typeTag}${rulesStr}`);
3831
+ }
3832
+ }
3358
3833
  async function handleInit() {
3359
3834
  const { writeFile: writeFile3, readFile: readFile2, mkdir: mkdir4 } = await import("fs/promises");
3360
- const { join: join7 } = await import("path");
3361
- const { homedir: homedir6 } = await import("os");
3835
+ const { join: join8 } = await import("path");
3836
+ const { homedir: homedir7 } = await import("os");
3362
3837
  const readline = await import("readline");
3363
3838
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
3364
3839
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
@@ -3366,7 +3841,7 @@ async function handleInit() {
3366
3841
  console.error(" Kairos SDK \u2014 Setup Wizard");
3367
3842
  console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3368
3843
  console.error("");
3369
- const envPath = join7(process.cwd(), ".env");
3844
+ const envPath = join8(process.cwd(), ".env");
3370
3845
  let existingEnv = "";
3371
3846
  try {
3372
3847
  existingEnv = await readFile2(envPath, "utf-8");
@@ -3430,8 +3905,8 @@ async function handleInit() {
3430
3905
  });
3431
3906
  console.error(` Synced ${result.saved} templates (${result.blocked} blocked, ${result.skippedDuplicate} duplicates)`);
3432
3907
  }
3433
- const kairosDir = join7(homedir6(), ".kairos");
3434
- await mkdir4(join7(kairosDir, "telemetry"), { recursive: true });
3908
+ const kairosDir = join8(homedir7(), ".kairos");
3909
+ await mkdir4(join8(kairosDir, "telemetry"), { recursive: true });
3435
3910
  console.error("");
3436
3911
  console.error(" Setup complete! Try:");
3437
3912
  console.error("");
@@ -3454,6 +3929,9 @@ async function main() {
3454
3929
  case "patterns":
3455
3930
  await handlePatterns(flags);
3456
3931
  break;
3932
+ case "sessions":
3933
+ await handleSessions(flags);
3934
+ break;
3457
3935
  case "list":
3458
3936
  await handleList();
3459
3937
  break;