@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/index.cjs CHANGED
@@ -559,6 +559,9 @@ var N8nValidator = class {
559
559
  this.checkRule21(workflow, issues);
560
560
  this.checkRule22(workflow, issues);
561
561
  this.checkRule23(workflow, issues);
562
+ this.checkRule24(workflow, issues);
563
+ this.checkRule25(workflow, issues);
564
+ this.checkRule26(workflow, issues);
562
565
  if (Array.isArray(workflow.nodes)) {
563
566
  const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
564
567
  for (const issue of issues) {
@@ -691,10 +694,14 @@ var N8nValidator = class {
691
694
  checkRule11(w, issues) {
692
695
  if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
693
696
  const reachable = /* @__PURE__ */ new Set();
694
- for (const [, outputs] of Object.entries(w.connections)) {
697
+ const aiSubNodeSources = /* @__PURE__ */ new Set();
698
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
695
699
  if (typeof outputs !== "object" || outputs === null) continue;
696
- for (const portGroup of Object.values(outputs)) {
700
+ let hasAiPort = false;
701
+ for (const [portName, portGroup] of Object.entries(outputs)) {
697
702
  if (!Array.isArray(portGroup)) continue;
703
+ const isAiPort = portName.startsWith("ai_");
704
+ if (isAiPort) hasAiPort = true;
698
705
  for (const targets of portGroup) {
699
706
  if (!Array.isArray(targets)) continue;
700
707
  for (const target of targets) {
@@ -703,10 +710,13 @@ var N8nValidator = class {
703
710
  }
704
711
  }
705
712
  }
713
+ if (hasAiPort) aiSubNodeSources.add(sourceName);
706
714
  }
707
715
  for (const node of w.nodes) {
708
716
  if (node.type.includes("stickyNote")) continue;
709
- if (!this.isTriggerNode(node) && !reachable.has(node.name)) {
717
+ if (this.isTriggerNode(node)) continue;
718
+ if (aiSubNodeSources.has(node.name)) continue;
719
+ if (!reachable.has(node.name)) {
710
720
  this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
711
721
  }
712
722
  }
@@ -902,6 +912,76 @@ var N8nValidator = class {
902
912
  }
903
913
  }
904
914
  }
915
+ // Rule 24 (WARN): deprecated accessor syntax in expressions
916
+ checkRule24(w, issues) {
917
+ if (!Array.isArray(w.nodes)) return;
918
+ const deprecated = /\$node\s*\[/;
919
+ for (const node of w.nodes) {
920
+ for (const expr of this.extractExpressions(node.parameters)) {
921
+ if (deprecated.test(expr)) {
922
+ this.warn(
923
+ issues,
924
+ 24,
925
+ `Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
926
+ node.id
927
+ );
928
+ break;
929
+ }
930
+ }
931
+ }
932
+ }
933
+ // Rule 25 (WARN): wrong item index assumptions in expressions
934
+ checkRule25(w, issues) {
935
+ if (!Array.isArray(w.nodes)) return;
936
+ const itemIndex = /\$json\s*\.\s*items\s*\[/;
937
+ for (const node of w.nodes) {
938
+ for (const expr of this.extractExpressions(node.parameters)) {
939
+ if (itemIndex.test(expr)) {
940
+ this.warn(
941
+ issues,
942
+ 25,
943
+ `Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
944
+ node.id
945
+ );
946
+ break;
947
+ }
948
+ }
949
+ }
950
+ }
951
+ // Rule 26 (WARN): missing .first() or .all() on node references
952
+ checkRule26(w, issues) {
953
+ if (!Array.isArray(w.nodes)) return;
954
+ const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
955
+ for (const node of w.nodes) {
956
+ for (const expr of this.extractExpressions(node.parameters)) {
957
+ if (bareRef.test(expr)) {
958
+ this.warn(
959
+ issues,
960
+ 26,
961
+ `Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
962
+ node.id
963
+ );
964
+ break;
965
+ }
966
+ }
967
+ }
968
+ }
969
+ extractExpressions(params) {
970
+ const expressions = [];
971
+ const walk = (val) => {
972
+ if (typeof val === "string") {
973
+ if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
974
+ expressions.push(val);
975
+ }
976
+ } else if (Array.isArray(val)) {
977
+ for (const item of val) walk(item);
978
+ } else if (val !== null && typeof val === "object") {
979
+ for (const v of Object.values(val)) walk(v);
980
+ }
981
+ };
982
+ walk(params);
983
+ return expressions;
984
+ }
905
985
  // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
906
986
  checkRule21(w, issues) {
907
987
  if (!Array.isArray(w.nodes)) return;
@@ -989,9 +1069,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
989
1069
  - Never reuse IDs, never use sequential fake IDs like "node-1"
990
1070
 
991
1071
  ### Credentials:
992
- - Only reference credentials with exact type names (see catalog below)
993
- - If credential ID is unknown, OMIT the credentials block entirely \u2014 never invent credential IDs
994
- - Never put API keys or tokens in parameters when a credential type exists
1072
+ - Each credential is keyed by its type string, with an object value containing id and name:
1073
+ "credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack Credential" } }
1074
+ - Use "placeholder-id" as the id \u2014 users replace this with their real credential ID from n8n after deployment
1075
+ - The credentialsNeeded field in your response declares what credentials the user must configure
1076
+ - Never put API keys or tokens directly in node parameters when a credential type exists
995
1077
 
996
1078
  ### Node names:
997
1079
  - All node names must be unique within the workflow
@@ -1038,6 +1120,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
1038
1120
 
1039
1121
  ---
1040
1122
 
1123
+ ## EXPRESSION SYNTAX \u2014 how to reference upstream node data
1124
+
1125
+ ### Accessing a field from an upstream node:
1126
+ - CORRECT: $('NodeName').item.json.field
1127
+ - WRONG: $node["NodeName"].json.field \u2190 deprecated accessor, fails at runtime (Rule 24)
1128
+
1129
+ ### Accessing array items from $json:
1130
+ - CORRECT: $json.field \u2190 n8n auto-flattens items; each item is already a flat object
1131
+ - WRONG: $json.items[0].field \u2190 do not index into items[] (Rule 25)
1132
+
1133
+ ### Calling node data \u2014 always qualify with .first() or .all():
1134
+ - CORRECT: $('NodeName').first().json.field \u2190 single item
1135
+ - CORRECT: $('NodeName').all() \u2190 array of all items
1136
+ - WRONG: $('NodeName').json \u2190 throws at runtime without .first() or .all() (Rule 26)
1137
+
1138
+ ---
1139
+
1041
1140
  ## NODE CATALOG \u2014 exact type strings and safe typeVersions
1042
1141
 
1043
1142
  ### Triggers (always at least one required):
@@ -1137,6 +1236,9 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
1137
1236
  5. At least one trigger node present
1138
1237
  6. Every AI Agent has an ai_languageModel sub-node
1139
1238
  7. settings block is complete with executionOrder: "v1"
1239
+ 8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
1240
+ 9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
1241
+ 10. No bare $('NodeName').json \u2014 always use .first().json.field or .all()
1140
1242
 
1141
1243
  ---
1142
1244
 
@@ -1153,7 +1255,7 @@ function scoreToMode(score) {
1153
1255
  }
1154
1256
 
1155
1257
  // src/validation/rule-metadata.ts
1156
- var VALIDATOR_RULE_IDS = Array.from({ length: 23 }, (_, i) => i + 1);
1258
+ var VALIDATOR_RULE_IDS = Array.from({ length: 26 }, (_, i) => i + 1);
1157
1259
  var RULE_PIPELINE_STAGES = {
1158
1260
  1: "node_generation",
1159
1261
  2: "node_generation",
@@ -1177,7 +1279,28 @@ var RULE_PIPELINE_STAGES = {
1177
1279
  20: "connection_wiring",
1178
1280
  21: "workflow_structure",
1179
1281
  22: "workflow_structure",
1180
- 23: "node_generation"
1282
+ 23: "node_generation",
1283
+ 24: "expression_syntax",
1284
+ 25: "expression_syntax",
1285
+ 26: "expression_syntax"
1286
+ };
1287
+ var RULE_EXAMPLES = {
1288
+ 17: {
1289
+ bad: '"credentials": { "slackOAuth2Api": "my-token" }',
1290
+ good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
1291
+ },
1292
+ 24: {
1293
+ bad: '$node["Fetch Data"].json.email',
1294
+ good: "$('Fetch Data').item.json.email"
1295
+ },
1296
+ 25: {
1297
+ bad: "$json.items[0].email",
1298
+ good: "$json.email"
1299
+ },
1300
+ 26: {
1301
+ bad: "$('Fetch Data').json.email",
1302
+ good: "$('Fetch Data').first().json.email"
1303
+ }
1181
1304
  };
1182
1305
  var RULE_MITIGATIONS = {
1183
1306
  1: "Provide a non-empty workflow name string",
@@ -1196,21 +1319,44 @@ var RULE_MITIGATIONS = {
1196
1319
  14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
1197
1320
  15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1198
1321
  16: "All node names must be unique within the workflow",
1199
- 17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
1322
+ 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',
1200
1323
  18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
1201
1324
  19: "Use known safe typeVersion values for each node type",
1202
1325
  20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1203
1326
  21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1204
1327
  22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
1205
- 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync"
1328
+ 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
1329
+ 24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
1330
+ 25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
1331
+ 26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
1206
1332
  };
1207
1333
 
1208
1334
  // src/generation/prompt-builder.ts
1209
1335
  var CRITICAL_SCORE_THRESHOLD = 0.15;
1336
+ function resolveProfile() {
1337
+ const env = process.env["KAIROS_PROMPT_PROFILE"];
1338
+ if (env === "minimal" || env === "standard" || env === "rich") return env;
1339
+ return "standard";
1340
+ }
1341
+ var PROACTIVE_EXPRESSION_GUIDANCE = `## Expression Syntax Quick Reference
1342
+
1343
+ Always use these patterns in expressions:
1344
+ - Access node data: $('NodeName').item.json.field (not $node["NodeName"].json)
1345
+ - Access JSON field: $json.field (not $json.items[0].field)
1346
+ - Single item: $('NodeName').first().json.field
1347
+ - All items: $('NodeName').all()`;
1210
1348
  var PromptBuilder = class {
1211
1349
  patternsPath;
1212
- constructor(patternsPath) {
1350
+ profile;
1351
+ _lastActivePatterns = null;
1352
+ constructor(patternsPath, profile) {
1213
1353
  this.patternsPath = patternsPath ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "patterns.json");
1354
+ this.profile = profile ?? resolveProfile();
1355
+ }
1356
+ resolveMaxPatterns() {
1357
+ if (this.profile === "minimal") return 3;
1358
+ if (this.profile === "rich") return 15;
1359
+ return 10;
1214
1360
  }
1215
1361
  build(request, matches, globalFailureRates = [], dynamicCatalog) {
1216
1362
  const mode = this.resolveMode(matches);
@@ -1248,53 +1394,62 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
1248
1394
  cache_control: { type: "ephemeral" }
1249
1395
  }
1250
1396
  ];
1251
- if (mode === "reference" && matches.length > 0) {
1252
- const refText = matches.slice(0, 3).map((m) => {
1253
- const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1254
- return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1397
+ if (this.profile !== "minimal") {
1398
+ if (mode === "reference" && matches.length > 0) {
1399
+ const refText = matches.slice(0, 3).map((m) => {
1400
+ const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1401
+ return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1255
1402
  Nodes:
1256
1403
  ${nodes}`;
1257
- }).join("\n\n");
1258
- blocks.push({
1259
- type: "text",
1260
- text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1261
-
1262
- ${refText}`
1263
- });
1264
- }
1265
- if (mode === "direct" && matches[0]) {
1266
- const match = matches[0];
1267
- const json = JSON.stringify(match.workflow.workflow, null, 2);
1268
- if (json.length > 3e4) {
1269
- const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1404
+ }).join("\n\n");
1270
1405
  blocks.push({
1271
1406
  type: "text",
1272
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1407
+ text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1408
+
1409
+ ${refText}`
1410
+ });
1411
+ }
1412
+ if (mode === "direct" && matches[0]) {
1413
+ const match = matches[0];
1414
+ const json = JSON.stringify(match.workflow.workflow, null, 2);
1415
+ if (json.length > 3e4) {
1416
+ const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1417
+ blocks.push({
1418
+ type: "text",
1419
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1273
1420
  Nodes:
1274
1421
  ${nodes}`
1275
- });
1276
- } else {
1277
- blocks.push({
1278
- type: "text",
1279
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1422
+ });
1423
+ } else {
1424
+ blocks.push({
1425
+ type: "text",
1426
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1280
1427
 
1281
1428
  ${json}`
1282
- });
1429
+ });
1430
+ }
1283
1431
  }
1284
- }
1285
- if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1286
- const hint = matches[0];
1287
- const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1288
- blocks.push({
1289
- type: "text",
1290
- text: `## Weak Structural Hint
1432
+ if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1433
+ const hint = matches[0];
1434
+ const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1435
+ blocks.push({
1436
+ type: "text",
1437
+ text: `## Weak Structural Hint
1291
1438
  A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
1292
- });
1439
+ });
1440
+ }
1293
1441
  }
1294
1442
  const warnings = this.buildFailureWarnings(matches, globalFailureRates);
1295
1443
  if (warnings) {
1296
1444
  blocks.push({ type: "text", text: warnings });
1297
1445
  }
1446
+ if (this.profile === "rich") {
1447
+ const expressionRules = /* @__PURE__ */ new Set([24, 25, 26]);
1448
+ const expressionAlreadyCovered = (this._lastActivePatterns ?? []).some((p) => expressionRules.has(p.rule));
1449
+ if (!expressionAlreadyCovered) {
1450
+ blocks.push({ type: "text", text: PROACTIVE_EXPRESSION_GUIDANCE });
1451
+ }
1452
+ }
1298
1453
  return blocks;
1299
1454
  }
1300
1455
  loadPatterns() {
@@ -1308,18 +1463,19 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1308
1463
  }
1309
1464
  }
1310
1465
  getWarnedRules() {
1311
- return this.getActivePatterns().map((p) => p.rule);
1466
+ const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
1467
+ return patterns.map((p) => p.rule);
1312
1468
  }
1313
- getActivePatterns() {
1314
- const MAX_WARNED = 10;
1469
+ getActivePatterns(maxCount = 10) {
1315
1470
  const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
1316
1471
  const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
1317
1472
  const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1318
1473
  const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1319
- return [...regressed, ...confirmed, ...drafts].slice(0, MAX_WARNED);
1474
+ return [...regressed, ...confirmed, ...drafts].slice(0, maxCount);
1320
1475
  }
1321
1476
  buildFailureWarnings(matches, globalFailureRates) {
1322
- const richPatterns = this.getActivePatterns();
1477
+ const richPatterns = this.getActivePatterns(this.resolveMaxPatterns());
1478
+ this._lastActivePatterns = richPatterns;
1323
1479
  if (richPatterns.length > 0) {
1324
1480
  return this.buildStageGroupedWarnings(richPatterns, matches);
1325
1481
  }
@@ -1330,7 +1486,8 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1330
1486
  credential_injection: "CREDENTIAL FORMATTING",
1331
1487
  connection_wiring: "CONNECTION WIRING",
1332
1488
  node_generation: "NODE GENERATION",
1333
- workflow_structure: "WORKFLOW STRUCTURE"
1489
+ workflow_structure: "WORKFLOW STRUCTURE",
1490
+ expression_syntax: "EXPRESSION SYNTAX"
1334
1491
  };
1335
1492
  const byStage = /* @__PURE__ */ new Map();
1336
1493
  for (const p of patterns) {
@@ -1358,7 +1515,11 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1358
1515
  const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
1359
1516
  const remedyStr = remedy ? `
1360
1517
  Fix: ${remedy}` : "";
1361
- lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}`);
1518
+ const ex = RULE_EXAMPLES[p.rule];
1519
+ const exampleStr = ex ? `
1520
+ Bad: ${ex.bad}
1521
+ Good: ${ex.good}` : "";
1522
+ lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}${exampleStr}`);
1362
1523
  } else {
1363
1524
  const ruleNums = group.map((p) => p.rule).join(", ");
1364
1525
  const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
@@ -1465,12 +1626,12 @@ var GENERATE_WORKFLOW_TOOL = {
1465
1626
  }
1466
1627
  };
1467
1628
  var WorkflowDesigner = class {
1468
- constructor(anthropic, model, logger) {
1629
+ constructor(anthropic, model, logger, patternsPath) {
1469
1630
  this.anthropic = anthropic;
1470
1631
  this.model = model;
1471
1632
  this.logger = logger;
1472
1633
  this.validator = new N8nValidator();
1473
- this.promptBuilder = new PromptBuilder();
1634
+ this.promptBuilder = new PromptBuilder(patternsPath);
1474
1635
  }
1475
1636
  anthropic;
1476
1637
  model;
@@ -1554,6 +1715,11 @@ var WorkflowDesigner = class {
1554
1715
  }
1555
1716
  }
1556
1717
  extractToolUse(message) {
1718
+ if (message.stop_reason === "max_tokens") {
1719
+ throw new GenerationError(
1720
+ "Claude response was truncated (max_tokens reached) \u2014 the workflow may be too large. Try a simpler description or break it into smaller workflows."
1721
+ );
1722
+ }
1557
1723
  const toolUseBlock = message.content.find(
1558
1724
  (block) => block.type === "tool_use"
1559
1725
  );
@@ -1596,11 +1762,12 @@ var TelemetryCollector = class {
1596
1762
  this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
1597
1763
  this.sessionId = generateUUID();
1598
1764
  }
1599
- async emit(eventType, data) {
1765
+ async emit(eventType, data, runId) {
1600
1766
  const event = {
1601
1767
  schemaVersion: TELEMETRY_SCHEMA_VERSION,
1602
1768
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1603
1769
  sessionId: this.sessionId,
1770
+ ...runId ? { runId } : {},
1604
1771
  eventType,
1605
1772
  data
1606
1773
  };
@@ -1726,6 +1893,7 @@ var PATTERN_SCHEMA_VERSION = 2;
1726
1893
  var PatternAnalyzer = class _PatternAnalyzer {
1727
1894
  telemetryDir;
1728
1895
  outputDir;
1896
+ _cachedEvents = null;
1729
1897
  constructor(telemetryDir) {
1730
1898
  const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
1731
1899
  this.telemetryDir = telemetryDir ?? defaultDir;
@@ -1754,19 +1922,23 @@ var PatternAnalyzer = class _PatternAnalyzer {
1754
1922
  }));
1755
1923
  }
1756
1924
  if (fromVersion < 2) {
1757
- migrated = migrated.map((p) => ({
1758
- ...p,
1759
- scoringFactors: {
1760
- ...p.scoringFactors,
1761
- stickinessBoost: p.scoringFactors.stickinessBoost ?? p.scoringFactors["validationBoost"] ?? 0
1762
- }
1763
- }));
1925
+ migrated = migrated.map((p) => {
1926
+ const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
1927
+ return {
1928
+ ...p,
1929
+ scoringFactors: {
1930
+ ...sf,
1931
+ stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
1932
+ }
1933
+ };
1934
+ });
1764
1935
  }
1765
1936
  return migrated;
1766
1937
  }
1767
1938
  async analyze(days = 30) {
1768
1939
  const previousPatterns = await this.loadPreviousPatterns();
1769
1940
  const events = await this.readAllEvents(days);
1941
+ this._cachedEvents = events;
1770
1942
  const starts = events.filter((e) => e.eventType === "build_start");
1771
1943
  const attempts = events.filter((e) => e.eventType === "generation_attempt");
1772
1944
  const passed = attempts.filter(
@@ -1779,13 +1951,18 @@ var PatternAnalyzer = class _PatternAnalyzer {
1779
1951
  const credentialFailures = /* @__PURE__ */ new Map();
1780
1952
  for (const a of failed) {
1781
1953
  const weight = this.recencyWeight(a.fileDate);
1954
+ const buildId = a.runId ?? a.sessionId;
1782
1955
  const data = a.data;
1783
1956
  for (const issue of data.issues ?? []) {
1784
- const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [] };
1957
+ if (issue.severity === "warn") continue;
1958
+ const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
1785
1959
  entry.count++;
1786
- entry.sessions.add(a.sessionId);
1960
+ entry.sessions.add(buildId);
1787
1961
  entry.recencyWeights.push(weight);
1788
1962
  entry.allMessages.push(issue.message);
1963
+ if (data.workflowType) {
1964
+ entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
1965
+ }
1789
1966
  ruleFailures.set(issue.rule, entry);
1790
1967
  if (issue.rule === 17) {
1791
1968
  const credPatterns = [
@@ -1838,9 +2015,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
1838
2015
  }
1839
2016
  const sessions = /* @__PURE__ */ new Map();
1840
2017
  for (const a of attempts) {
1841
- const list = sessions.get(a.sessionId) ?? [];
2018
+ const buildId = a.runId ?? a.sessionId;
2019
+ const list = sessions.get(buildId) ?? [];
1842
2020
  list.push(a);
1843
- sessions.set(a.sessionId, list);
2021
+ sessions.set(buildId, list);
1844
2022
  }
1845
2023
  let firstTryPass = 0;
1846
2024
  let correctionNeeded = 0;
@@ -1887,7 +2065,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1887
2065
  const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
1888
2066
  const stickiness = stickinessCount.get(rule) ?? 0;
1889
2067
  const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
1890
- return {
2068
+ const pattern = {
1891
2069
  rule,
1892
2070
  failureCount: entry.count,
1893
2071
  confidence: Math.round(rawConfidence * 1e3) / 1e3,
@@ -1899,6 +2077,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
1899
2077
  exampleMessages: this.deduplicateMessages(entry.allMessages),
1900
2078
  mitigation: RULE_MITIGATIONS[rule] ?? null
1901
2079
  };
2080
+ if (entry.workflowTypes.size > 0) {
2081
+ pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
2082
+ }
2083
+ return pattern;
1902
2084
  }).sort((a, b) => b.compositeScore - a.compositeScore);
1903
2085
  const activeRules = new Set(activePatterns.map((p) => p.rule));
1904
2086
  for (const p of activePatterns) {
@@ -1955,7 +2137,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1955
2137
  const warned = bcData.warnedRules ?? [];
1956
2138
  if (warned.length === 0) continue;
1957
2139
  const sessionFailedRules = /* @__PURE__ */ new Set();
1958
- const sessionAttempts = sessions.get(bc.sessionId) ?? [];
2140
+ const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
1959
2141
  for (const a of sessionAttempts) {
1960
2142
  const ad = a.data;
1961
2143
  if (ad.validationPassed === false) {
@@ -2038,8 +2220,55 @@ var PatternAnalyzer = class _PatternAnalyzer {
2038
2220
  };
2039
2221
  const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
2040
2222
  await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
2223
+ const sessions = await this.buildSessionSummaries(days);
2224
+ const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
2225
+ const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
2226
+ await (0, import_promises3.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
2227
+ await (0, import_promises3.rename)(sessionHistoryTmp, sessionHistoryPath);
2041
2228
  return analysis;
2042
2229
  }
2230
+ async getSessions(limit = 20) {
2231
+ try {
2232
+ const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
2233
+ const all = JSON.parse(raw);
2234
+ return all.slice(-limit);
2235
+ } catch {
2236
+ return [];
2237
+ }
2238
+ }
2239
+ async buildSessionSummaries(days = 30) {
2240
+ const events = this._cachedEvents ?? await this.readAllEvents(days);
2241
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
2242
+ const attemptsByBuild = /* @__PURE__ */ new Map();
2243
+ for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
2244
+ const buildId = e.runId ?? e.sessionId;
2245
+ const list = attemptsByBuild.get(buildId) ?? [];
2246
+ list.push(e);
2247
+ attemptsByBuild.set(buildId, list);
2248
+ }
2249
+ const summaries = buildCompletes.map((bc) => {
2250
+ const data = bc.data;
2251
+ const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
2252
+ const failedRules = Array.from(new Set(
2253
+ sessionAttempts.flatMap((a) => {
2254
+ const ad = a.data;
2255
+ if (ad.validationPassed !== false) return [];
2256
+ return (ad.issues ?? []).map((i) => i.rule);
2257
+ })
2258
+ ));
2259
+ return {
2260
+ sessionId: bc.sessionId,
2261
+ date: bc.fileDate,
2262
+ description: data.description ?? "",
2263
+ workflowType: data.workflowType ?? null,
2264
+ attempts: data.totalAttempts ?? 1,
2265
+ success: data.success ?? false,
2266
+ failedRules,
2267
+ workflowName: data.workflowName ?? null
2268
+ };
2269
+ });
2270
+ return summaries.sort((a, b) => a.date.localeCompare(b.date));
2271
+ }
2043
2272
  async getHistory(limit = 20) {
2044
2273
  try {
2045
2274
  const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
@@ -2061,7 +2290,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
2061
2290
  alerts.push({
2062
2291
  type: "stale_pattern",
2063
2292
  rule: p.rule,
2064
- message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-23)`
2293
+ message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-26)`
2065
2294
  });
2066
2295
  }
2067
2296
  }
@@ -2148,7 +2377,59 @@ var nullLogger = {
2148
2377
  }
2149
2378
  };
2150
2379
 
2380
+ // src/utils/workflow-type.ts
2381
+ var TYPE_KEYWORDS = [
2382
+ ["gmail", "email"],
2383
+ ["imap", "email"],
2384
+ ["smtp", "email"],
2385
+ [" email", "email"],
2386
+ ["slack", "slack"],
2387
+ ["telegram", "messaging"],
2388
+ ["discord", "messaging"],
2389
+ [" sms", "messaging"],
2390
+ ["twilio", "messaging"],
2391
+ ["webhook", "webhook"],
2392
+ ["google sheets", "data"],
2393
+ ["spreadsheet", "data"],
2394
+ ["airtable", "data"],
2395
+ ["notion", "data"],
2396
+ ["github", "devops"],
2397
+ ["gitlab", "devops"],
2398
+ ["schedule", "schedule"],
2399
+ [" cron", "schedule"],
2400
+ ["daily", "schedule"],
2401
+ ["weekly", "schedule"],
2402
+ ["hourly", "schedule"],
2403
+ ["every day", "schedule"],
2404
+ ["every hour", "schedule"],
2405
+ ["every morning", "schedule"],
2406
+ ["postgres", "database"],
2407
+ ["mysql", "database"],
2408
+ ["supabase", "database"],
2409
+ ["redis", "database"],
2410
+ [" database", "database"],
2411
+ [" llm", "ai"],
2412
+ [" gpt", "ai"],
2413
+ ["claude", "ai"],
2414
+ [" agent", "ai"],
2415
+ ["langchain", "ai"],
2416
+ [" ai ", "ai"],
2417
+ [" ai", "ai"],
2418
+ ["http request", "api"],
2419
+ ["rest api", "api"],
2420
+ [" api", "api"]
2421
+ ];
2422
+ function inferWorkflowType(description) {
2423
+ const lower = " " + description.toLowerCase();
2424
+ for (const [keyword, type] of TYPE_KEYWORDS) {
2425
+ if (lower.includes(keyword)) return type;
2426
+ }
2427
+ return null;
2428
+ }
2429
+
2151
2430
  // src/client.ts
2431
+ var import_node_os5 = require("os");
2432
+ var import_node_path6 = require("path");
2152
2433
  var DEFAULT_MODEL = "claude-sonnet-4-6";
2153
2434
  var Kairos = class {
2154
2435
  provider;
@@ -2177,7 +2458,8 @@ var Kairos = class {
2177
2458
  this.provider = null;
2178
2459
  }
2179
2460
  const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
2180
- this.designer = new WorkflowDesigner(anthropic, this.model, logger);
2461
+ 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");
2462
+ this.designer = new WorkflowDesigner(anthropic, this.model, logger, patternsPath);
2181
2463
  this.validator = new N8nValidator();
2182
2464
  this.library = options.library ?? new NullLibrary();
2183
2465
  this.logger = logger;
@@ -2210,11 +2492,13 @@ var Kairos = class {
2210
2492
  this.validateDescription(description);
2211
2493
  this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
2212
2494
  const buildStart = Date.now();
2495
+ const runId = generateUUID();
2496
+ const workflowType = inferWorkflowType(description);
2213
2497
  await this.telemetry?.emit("build_start", {
2214
2498
  description,
2215
2499
  model: this.model,
2216
2500
  dryRun: options?.dryRun ?? false
2217
- });
2501
+ }, runId);
2218
2502
  await this.library.initialize();
2219
2503
  const matches = await this.library.search(description);
2220
2504
  if (matches.length > 0) {
@@ -2249,8 +2533,9 @@ var Kairos = class {
2249
2533
  tokensOutput: meta.tokensOutput,
2250
2534
  validationPassed: meta.validationPassed,
2251
2535
  issueCount: meta.issues.length,
2252
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
2253
- });
2536
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
2537
+ workflowType
2538
+ }, runId);
2254
2539
  }
2255
2540
  await this.telemetry?.emit("build_complete", {
2256
2541
  description,
@@ -2263,13 +2548,14 @@ var Kairos = class {
2263
2548
  workflowId: null,
2264
2549
  dryRun: options?.dryRun ?? false,
2265
2550
  credentialsNeeded: 0,
2266
- warnedRules: err.warnedRules ?? []
2267
- });
2551
+ warnedRules: err.warnedRules ?? [],
2552
+ workflowType
2553
+ }, runId);
2268
2554
  this.updatePatterns();
2269
2555
  }
2270
2556
  throw err;
2271
2557
  }
2272
- await this.emitAttemptTelemetry(description, designResult);
2558
+ await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
2273
2559
  const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
2274
2560
  this.saveToLibrary(workflow, description, designResult, matches);
2275
2561
  if (options?.dryRun) {
@@ -2286,8 +2572,9 @@ var Kairos = class {
2286
2572
  workflowId: null,
2287
2573
  dryRun: true,
2288
2574
  credentialsNeeded: designResult.credentialsNeeded.length,
2289
- warnedRules: designResult.warnedRules
2290
- });
2575
+ warnedRules: designResult.warnedRules,
2576
+ workflowType
2577
+ }, runId);
2291
2578
  this.updatePatterns();
2292
2579
  return {
2293
2580
  workflowId: null,
@@ -2318,8 +2605,9 @@ var Kairos = class {
2318
2605
  workflowId: deployed.workflowId,
2319
2606
  dryRun: false,
2320
2607
  credentialsNeeded: designResult.credentialsNeeded.length,
2321
- warnedRules: designResult.warnedRules
2322
- });
2608
+ warnedRules: designResult.warnedRules,
2609
+ workflowType
2610
+ }, runId);
2323
2611
  this.updatePatterns();
2324
2612
  return {
2325
2613
  workflowId: deployed.workflowId,
@@ -2335,11 +2623,13 @@ var Kairos = class {
2335
2623
  this.validateDescription(description);
2336
2624
  this.logger.info("Kairos.update", { id, description });
2337
2625
  const buildStart = Date.now();
2626
+ const runId = generateUUID();
2627
+ const workflowType = inferWorkflowType(description);
2338
2628
  await this.telemetry?.emit("build_start", {
2339
2629
  description,
2340
2630
  model: this.model,
2341
2631
  dryRun: false
2342
- });
2632
+ }, runId);
2343
2633
  await this.library.initialize();
2344
2634
  const matches = await this.library.search(description);
2345
2635
  const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
@@ -2358,8 +2648,9 @@ var Kairos = class {
2358
2648
  tokensOutput: meta.tokensOutput,
2359
2649
  validationPassed: meta.validationPassed,
2360
2650
  issueCount: meta.issues.length,
2361
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
2362
- });
2651
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
2652
+ workflowType
2653
+ }, runId);
2363
2654
  }
2364
2655
  await this.telemetry?.emit("build_complete", {
2365
2656
  description,
@@ -2372,13 +2663,14 @@ var Kairos = class {
2372
2663
  workflowId: null,
2373
2664
  dryRun: false,
2374
2665
  credentialsNeeded: 0,
2375
- warnedRules: err.warnedRules ?? []
2376
- });
2666
+ warnedRules: err.warnedRules ?? [],
2667
+ workflowType
2668
+ }, runId);
2377
2669
  this.updatePatterns();
2378
2670
  }
2379
2671
  throw err;
2380
2672
  }
2381
- await this.emitAttemptTelemetry(description, designResult);
2673
+ await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
2382
2674
  const provider = this.requireProvider();
2383
2675
  const deployed = await provider.update(id, designResult.workflow);
2384
2676
  this.saveToLibrary(designResult.workflow, description, designResult, matches);
@@ -2396,8 +2688,9 @@ var Kairos = class {
2396
2688
  workflowId: deployed.workflowId,
2397
2689
  dryRun: false,
2398
2690
  credentialsNeeded: designResult.credentialsNeeded.length,
2399
- warnedRules: designResult.warnedRules
2400
- });
2691
+ warnedRules: designResult.warnedRules,
2692
+ workflowType
2693
+ }, runId);
2401
2694
  this.updatePatterns();
2402
2695
  return {
2403
2696
  workflowId: deployed.workflowId,
@@ -2420,7 +2713,7 @@ var Kairos = class {
2420
2713
  return null;
2421
2714
  });
2422
2715
  }
2423
- async emitAttemptTelemetry(description, designResult) {
2716
+ async emitAttemptTelemetry(description, designResult, workflowType, runId) {
2424
2717
  for (const meta of designResult.attemptMetadata) {
2425
2718
  await this.telemetry?.emit("generation_attempt", {
2426
2719
  description,
@@ -2431,8 +2724,9 @@ var Kairos = class {
2431
2724
  tokensOutput: meta.tokensOutput,
2432
2725
  validationPassed: meta.validationPassed,
2433
2726
  issueCount: meta.issues.length,
2434
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
2435
- });
2727
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
2728
+ workflowType
2729
+ }, runId);
2436
2730
  }
2437
2731
  }
2438
2732
  recordDeploy() {
@@ -2531,8 +2825,8 @@ var Kairos = class {
2531
2825
 
2532
2826
  // src/library/file-library.ts
2533
2827
  var import_promises4 = require("fs/promises");
2534
- var import_node_path6 = require("path");
2535
- var import_node_os5 = require("os");
2828
+ var import_node_path7 = require("path");
2829
+ var import_node_os6 = require("os");
2536
2830
 
2537
2831
  // src/library/scorer.ts
2538
2832
  var WEIGHTS = {
@@ -2762,13 +3056,27 @@ function buildSearchCorpus(w) {
2762
3056
  return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
2763
3057
  }
2764
3058
  var MAX_LIBRARY_SIZE = 500;
3059
+ function isValidMeta(item) {
3060
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
3061
+ }
3062
+ function isValidOldEntry(item) {
3063
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
3064
+ item.workflow.nodes
3065
+ );
3066
+ }
2765
3067
  var FileLibrary = class {
2766
3068
  dir;
2767
- workflows = [];
3069
+ meta = [];
2768
3070
  initPromise = null;
2769
3071
  writeQueue = Promise.resolve();
2770
3072
  constructor(dir) {
2771
- this.dir = dir ?? (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".kairos", "library");
3073
+ this.dir = dir ?? (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".kairos", "library");
3074
+ }
3075
+ get workflowsDir() {
3076
+ return (0, import_node_path7.join)(this.dir, "workflows");
3077
+ }
3078
+ workflowFilePath(id) {
3079
+ return (0, import_node_path7.join)(this.workflowsDir, `${id}.json`);
2772
3080
  }
2773
3081
  async initialize() {
2774
3082
  if (!this.initPromise) {
@@ -2778,61 +3086,148 @@ var FileLibrary = class {
2778
3086
  }
2779
3087
  async doInitialize() {
2780
3088
  await (0, import_promises4.mkdir)(this.dir, { recursive: true });
2781
- const indexPath = (0, import_node_path6.join)(this.dir, "index.json");
3089
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
3090
+ let workflowsDirExists = false;
2782
3091
  try {
2783
- const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
2784
- const parsed = JSON.parse(raw);
2785
- if (!Array.isArray(parsed)) {
2786
- this.workflows = [];
2787
- } else {
2788
- this.workflows = parsed.filter(
2789
- (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)
2790
- );
3092
+ await (0, import_promises4.stat)(this.workflowsDir);
3093
+ workflowsDirExists = true;
3094
+ } catch {
3095
+ }
3096
+ if (workflowsDirExists) {
3097
+ try {
3098
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3099
+ const parsed = JSON.parse(raw);
3100
+ if (Array.isArray(parsed)) {
3101
+ this.meta = parsed.filter(isValidMeta);
3102
+ }
3103
+ } catch {
3104
+ this.meta = [];
3105
+ }
3106
+ } else {
3107
+ try {
3108
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3109
+ const parsed = JSON.parse(raw);
3110
+ if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
3111
+ await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
3112
+ return;
3113
+ }
3114
+ } catch {
2791
3115
  }
3116
+ this.meta = [];
3117
+ await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
3118
+ }
3119
+ }
3120
+ /**
3121
+ * One-time transparent migration from v0.4.x monolithic index.json.
3122
+ * Splits each stored workflow into a per-file workflow JSON and a lightweight
3123
+ * meta entry. Rewrites index.json in the new format.
3124
+ */
3125
+ async migrateFromMonolithic(oldEntries) {
3126
+ await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
3127
+ const newMeta = [];
3128
+ for (const entry of oldEntries) {
3129
+ const wfPath = this.workflowFilePath(entry.id);
3130
+ const tmpPath = `${wfPath}.tmp`;
3131
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
3132
+ await (0, import_promises4.rename)(tmpPath, wfPath);
3133
+ const { workflow, ...metaFields } = entry;
3134
+ newMeta.push({
3135
+ ...metaFields,
3136
+ workflowName: workflow.name,
3137
+ cachedNodeTypes: workflow.nodes.map((n) => n.type)
3138
+ });
3139
+ }
3140
+ this.meta = newMeta;
3141
+ await this.persistNow();
3142
+ }
3143
+ async loadWorkflowFile(id) {
3144
+ try {
3145
+ const raw = await (0, import_promises4.readFile)(this.workflowFilePath(id), "utf-8");
3146
+ return JSON.parse(raw);
2792
3147
  } catch {
2793
- this.workflows = [];
3148
+ return null;
2794
3149
  }
2795
3150
  }
3151
+ async writeWorkflowFile(id, workflow) {
3152
+ const wfPath = this.workflowFilePath(id);
3153
+ const tmpPath = `${wfPath}.tmp`;
3154
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
3155
+ await (0, import_promises4.rename)(tmpPath, wfPath);
3156
+ }
3157
+ /**
3158
+ * Build a lightweight StoredWorkflow shell from a meta entry for use in
3159
+ * scoring / clustering. Only node.type is populated in each node — no other
3160
+ * node fields are used by hybridScore or clusterWorkflows.
3161
+ */
3162
+ makeSearchShell(m) {
3163
+ return {
3164
+ ...m,
3165
+ workflow: {
3166
+ name: m.workflowName,
3167
+ nodes: m.cachedNodeTypes.map((type) => ({
3168
+ id: "",
3169
+ name: "",
3170
+ type,
3171
+ typeVersion: 1,
3172
+ position: [0, 0],
3173
+ parameters: {}
3174
+ })),
3175
+ connections: {}
3176
+ }
3177
+ };
3178
+ }
2796
3179
  async search(description, options) {
2797
- const searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
2798
- if (searchable.length === 0) return [];
3180
+ const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
3181
+ if (filteredMeta.length === 0) return [];
2799
3182
  const limit = options?.limit ?? 3;
2800
3183
  const queryTokens = tokenize(description);
2801
3184
  if (queryTokens.length === 0) return [];
2802
- const docTokenArrays = searchable.map((w) => tokenize(buildSearchCorpus(w)));
3185
+ const shells = filteredMeta.map((m) => this.makeSearchShell(m));
3186
+ const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
2803
3187
  const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
2804
- const docCount = searchable.length;
3188
+ const docCount = shells.length;
2805
3189
  const idf = /* @__PURE__ */ new Map();
2806
3190
  const allTokens = new Set(queryTokens);
2807
3191
  for (const token of allTokens) {
2808
3192
  const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
2809
3193
  idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
2810
3194
  }
2811
- const scored = hybridScore(queryTokens, description, searchable, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
2812
- const clusters = clusterWorkflows(searchable);
3195
+ const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
3196
+ const clusters = clusterWorkflows(shells);
2813
3197
  const reranked = rerank(scored, clusters).slice(0, limit);
2814
- const results = reranked.map((m) => {
2815
- return { workflow: m.workflow, score: m.score, mode: scoreToMode(m.score) };
2816
- });
2817
- if (results.length > 0) {
2818
- for (const r of results) {
2819
- r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
2820
- }
2821
- this.persist();
2822
- }
2823
- return results;
3198
+ if (reranked.length === 0) return [];
3199
+ for (const r of reranked) {
3200
+ const m = this.meta.find((m2) => m2.id === r.workflow.id);
3201
+ if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
3202
+ }
3203
+ this.persist();
3204
+ const results = await Promise.all(
3205
+ reranked.map(async (r) => {
3206
+ const m = this.meta.find((meta) => meta.id === r.workflow.id);
3207
+ const workflow = await this.loadWorkflowFile(r.workflow.id);
3208
+ if (!workflow) return null;
3209
+ return {
3210
+ workflow: { ...m, workflow },
3211
+ score: r.score,
3212
+ mode: scoreToMode(r.score)
3213
+ };
3214
+ })
3215
+ );
3216
+ return results.filter((r) => r !== null);
2824
3217
  }
2825
3218
  async save(workflow, metadata) {
2826
3219
  const id = generateUUID();
3220
+ await this.writeWorkflowFile(id, workflow);
2827
3221
  const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
2828
- const stored = {
3222
+ const meta = {
2829
3223
  id,
2830
- workflow,
2831
3224
  description: metadata.description,
2832
3225
  tags: metadata.tags ?? [],
2833
3226
  platform: metadata.platform ?? "n8n",
2834
3227
  deployCount: 0,
2835
3228
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3229
+ workflowName: workflow.name,
3230
+ cachedNodeTypes: workflow.nodes.map((n) => n.type),
2836
3231
  ...failurePatterns?.length ? { failurePatterns } : {},
2837
3232
  ...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
2838
3233
  ...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
@@ -2844,31 +3239,35 @@ var FileLibrary = class {
2844
3239
  ...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
2845
3240
  ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
2846
3241
  };
2847
- this.workflows.push(stored);
2848
- if (this.workflows.length > MAX_LIBRARY_SIZE) {
2849
- this.workflows.sort((a, b) => (b.deployCount ?? 1) - (a.deployCount ?? 1));
2850
- this.workflows = this.workflows.slice(0, MAX_LIBRARY_SIZE);
3242
+ this.meta.push(meta);
3243
+ if (this.meta.length > MAX_LIBRARY_SIZE) {
3244
+ this.meta.sort((a, b) => {
3245
+ if (a.id === id) return -1;
3246
+ if (b.id === id) return 1;
3247
+ return (b.deployCount ?? 0) - (a.deployCount ?? 0);
3248
+ });
3249
+ this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
2851
3250
  }
2852
3251
  await this.persist();
2853
3252
  return id;
2854
3253
  }
2855
3254
  async recordDeployment(id) {
2856
- const w = this.workflows.find((w2) => w2.id === id);
2857
- if (w) {
2858
- w.deployCount++;
2859
- w.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
3255
+ const m = this.meta.find((m2) => m2.id === id);
3256
+ if (m) {
3257
+ m.deployCount++;
3258
+ m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
2860
3259
  await this.persist();
2861
3260
  }
2862
3261
  }
2863
3262
  async recordOutcome(id, outcome) {
2864
- const w = this.workflows.find((w2) => w2.id === id);
2865
- if (!w) return;
3263
+ const m = this.meta.find((m2) => m2.id === id);
3264
+ if (!m) return;
2866
3265
  if (outcome.mode === "direct") {
2867
- w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
3266
+ m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
2868
3267
  } else {
2869
- w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
3268
+ m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
2870
3269
  }
2871
- const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
3270
+ const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
2872
3271
  stats.totalUses++;
2873
3272
  stats.totalAttempts += outcome.attempts;
2874
3273
  if (outcome.firstTryPass) stats.firstTryPasses++;
@@ -2876,24 +3275,35 @@ var FileLibrary = class {
2876
3275
  const key = String(rule);
2877
3276
  stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
2878
3277
  }
2879
- w.outcomeStats = stats;
3278
+ m.outcomeStats = stats;
2880
3279
  await this.persist();
2881
3280
  }
2882
3281
  async drain() {
2883
3282
  await this.writeQueue;
2884
3283
  }
2885
3284
  async get(id) {
2886
- return this.workflows.find((w) => w.id === id) ?? null;
3285
+ const m = this.meta.find((m2) => m2.id === id);
3286
+ if (!m) return null;
3287
+ const workflow = await this.loadWorkflowFile(id);
3288
+ if (!workflow) return null;
3289
+ return { ...m, workflow };
2887
3290
  }
2888
3291
  async list(filters) {
2889
- let result = this.workflows;
3292
+ let filtered = this.meta;
2890
3293
  if (filters?.platform) {
2891
- result = result.filter((w) => w.platform === filters.platform);
3294
+ filtered = filtered.filter((m) => m.platform === filters.platform);
2892
3295
  }
2893
3296
  if (filters?.tags && filters.tags.length > 0) {
2894
- result = result.filter((w) => filters.tags.some((t) => w.tags.includes(t)));
3297
+ filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
2895
3298
  }
2896
- return result;
3299
+ const results = await Promise.all(
3300
+ filtered.map(async (m) => {
3301
+ const workflow = await this.loadWorkflowFile(m.id);
3302
+ if (!workflow) return null;
3303
+ return { ...m, workflow };
3304
+ })
3305
+ );
3306
+ return results.filter((r) => r !== null);
2897
3307
  }
2898
3308
  deduplicateFailurePatterns(patterns) {
2899
3309
  if (!patterns?.length) return void 0;
@@ -2908,11 +3318,36 @@ var FileLibrary = class {
2908
3318
  }
2909
3319
  return [...map.values()];
2910
3320
  }
3321
+ /**
3322
+ * Direct write used only during migration (before writeQueue is needed).
3323
+ */
3324
+ async persistNow() {
3325
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
3326
+ const tmpPath = `${indexPath}.tmp`;
3327
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
3328
+ await (0, import_promises4.rename)(tmpPath, indexPath);
3329
+ }
2911
3330
  persist() {
2912
3331
  this.writeQueue = this.writeQueue.then(async () => {
2913
- const indexPath = (0, import_node_path6.join)(this.dir, "index.json");
3332
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
3333
+ let onDisk = [];
3334
+ try {
3335
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3336
+ const parsed = JSON.parse(raw);
3337
+ if (Array.isArray(parsed)) {
3338
+ onDisk = parsed.filter(isValidMeta);
3339
+ }
3340
+ } catch {
3341
+ }
3342
+ const ourIds = new Set(this.meta.map((m) => m.id));
3343
+ const external = onDisk.filter((m) => !ourIds.has(m.id));
3344
+ let merged = [...this.meta, ...external];
3345
+ if (merged.length > MAX_LIBRARY_SIZE) {
3346
+ merged.sort((a, b) => (b.deployCount ?? 0) - (a.deployCount ?? 0));
3347
+ merged = merged.slice(0, MAX_LIBRARY_SIZE);
3348
+ }
2914
3349
  const tmpPath = `${indexPath}.tmp`;
2915
- await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
3350
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
2916
3351
  await (0, import_promises4.rename)(tmpPath, indexPath);
2917
3352
  });
2918
3353
  return this.writeQueue;