@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.
@@ -1,3 +1,8 @@
1
+ // src/utils/uuid.ts
2
+ function generateUUID() {
3
+ return crypto.randomUUID();
4
+ }
5
+
1
6
  // src/errors/base.ts
2
7
  var KairosError = class extends Error {
3
8
  constructor(message, cause) {
@@ -397,6 +402,9 @@ var N8nValidator = class {
397
402
  this.checkRule21(workflow, issues);
398
403
  this.checkRule22(workflow, issues);
399
404
  this.checkRule23(workflow, issues);
405
+ this.checkRule24(workflow, issues);
406
+ this.checkRule25(workflow, issues);
407
+ this.checkRule26(workflow, issues);
400
408
  if (Array.isArray(workflow.nodes)) {
401
409
  const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
402
410
  for (const issue of issues) {
@@ -529,10 +537,14 @@ var N8nValidator = class {
529
537
  checkRule11(w, issues) {
530
538
  if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
531
539
  const reachable = /* @__PURE__ */ new Set();
532
- for (const [, outputs] of Object.entries(w.connections)) {
540
+ const aiSubNodeSources = /* @__PURE__ */ new Set();
541
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
533
542
  if (typeof outputs !== "object" || outputs === null) continue;
534
- for (const portGroup of Object.values(outputs)) {
543
+ let hasAiPort = false;
544
+ for (const [portName, portGroup] of Object.entries(outputs)) {
535
545
  if (!Array.isArray(portGroup)) continue;
546
+ const isAiPort = portName.startsWith("ai_");
547
+ if (isAiPort) hasAiPort = true;
536
548
  for (const targets of portGroup) {
537
549
  if (!Array.isArray(targets)) continue;
538
550
  for (const target of targets) {
@@ -541,10 +553,13 @@ var N8nValidator = class {
541
553
  }
542
554
  }
543
555
  }
556
+ if (hasAiPort) aiSubNodeSources.add(sourceName);
544
557
  }
545
558
  for (const node of w.nodes) {
546
559
  if (node.type.includes("stickyNote")) continue;
547
- if (!this.isTriggerNode(node) && !reachable.has(node.name)) {
560
+ if (this.isTriggerNode(node)) continue;
561
+ if (aiSubNodeSources.has(node.name)) continue;
562
+ if (!reachable.has(node.name)) {
548
563
  this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
549
564
  }
550
565
  }
@@ -740,6 +755,76 @@ var N8nValidator = class {
740
755
  }
741
756
  }
742
757
  }
758
+ // Rule 24 (WARN): deprecated accessor syntax in expressions
759
+ checkRule24(w, issues) {
760
+ if (!Array.isArray(w.nodes)) return;
761
+ const deprecated = /\$node\s*\[/;
762
+ for (const node of w.nodes) {
763
+ for (const expr of this.extractExpressions(node.parameters)) {
764
+ if (deprecated.test(expr)) {
765
+ this.warn(
766
+ issues,
767
+ 24,
768
+ `Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
769
+ node.id
770
+ );
771
+ break;
772
+ }
773
+ }
774
+ }
775
+ }
776
+ // Rule 25 (WARN): wrong item index assumptions in expressions
777
+ checkRule25(w, issues) {
778
+ if (!Array.isArray(w.nodes)) return;
779
+ const itemIndex = /\$json\s*\.\s*items\s*\[/;
780
+ for (const node of w.nodes) {
781
+ for (const expr of this.extractExpressions(node.parameters)) {
782
+ if (itemIndex.test(expr)) {
783
+ this.warn(
784
+ issues,
785
+ 25,
786
+ `Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
787
+ node.id
788
+ );
789
+ break;
790
+ }
791
+ }
792
+ }
793
+ }
794
+ // Rule 26 (WARN): missing .first() or .all() on node references
795
+ checkRule26(w, issues) {
796
+ if (!Array.isArray(w.nodes)) return;
797
+ const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
798
+ for (const node of w.nodes) {
799
+ for (const expr of this.extractExpressions(node.parameters)) {
800
+ if (bareRef.test(expr)) {
801
+ this.warn(
802
+ issues,
803
+ 26,
804
+ `Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
805
+ node.id
806
+ );
807
+ break;
808
+ }
809
+ }
810
+ }
811
+ }
812
+ extractExpressions(params) {
813
+ const expressions = [];
814
+ const walk = (val) => {
815
+ if (typeof val === "string") {
816
+ if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
817
+ expressions.push(val);
818
+ }
819
+ } else if (Array.isArray(val)) {
820
+ for (const item of val) walk(item);
821
+ } else if (val !== null && typeof val === "object") {
822
+ for (const v of Object.values(val)) walk(v);
823
+ }
824
+ };
825
+ walk(params);
826
+ return expressions;
827
+ }
743
828
  // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
744
829
  checkRule21(w, issues) {
745
830
  if (!Array.isArray(w.nodes)) return;
@@ -763,470 +848,40 @@ var N8nValidator = class {
763
848
  }
764
849
  };
765
850
 
766
- // src/generation/prompt-builder.ts
767
- import { readFileSync } from "fs";
851
+ // src/telemetry/collector.ts
852
+ import { appendFile, mkdir } from "fs/promises";
768
853
  import { join } from "path";
769
854
  import { homedir } from "os";
770
855
 
771
- // src/generation/prompts/v1.ts
772
- var SYSTEM_PROMPT_V1 = `You are a workflow generation engine for n8n. Your only output is a generate_workflow tool call containing valid n8n workflow JSON. You never respond with prose, explanations, or markdown. If you cannot fulfill the request, set the error field in the tool call.
773
-
774
- ## HARD RULES \u2014 violating any of these causes immediate deployment failure
775
-
776
- ### Forbidden fields \u2014 NEVER include these in the workflow object:
777
- id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId, activeVersion, pinData, triggerCount, shared, staticData
778
-
779
- ### Required top-level structure:
780
- {
781
- "name": "<descriptive name>",
782
- "nodes": [...],
783
- "connections": {...},
784
- "settings": {
785
- "saveExecutionProgress": true,
786
- "saveManualExecutions": true,
787
- "saveDataErrorExecution": "all",
788
- "saveDataSuccessExecution": "all",
789
- "executionTimeout": 3600,
790
- "timezone": "UTC",
791
- "executionOrder": "v1"
792
- }
793
- }
794
-
795
- ### Node IDs:
796
- - Every node.id must be a valid UUID v4 (random hex, format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
797
- - Never reuse IDs, never use sequential fake IDs like "node-1"
798
-
799
- ### Credentials:
800
- - Only reference credentials with exact type names (see catalog below)
801
- - If credential ID is unknown, OMIT the credentials block entirely \u2014 never invent credential IDs
802
- - Never put API keys or tokens in parameters when a credential type exists
803
-
804
- ### Node names:
805
- - All node names must be unique within the workflow
806
- - Use descriptive names: "Fetch Open Invoices" not "HTTP Request 2"
807
-
808
- ### Positioning:
809
- - Trigger node: [250, 300]
810
- - Each subsequent step: x + 220 minimum
811
- - Parallel branches: offset y by \xB1150
812
- - AI sub-nodes: place below their root node (y + 200)
813
-
814
- ---
815
-
816
- ## CONNECTION RULES \u2014 the most common source of errors
817
-
818
- ### Standard connections (main data flow):
819
- "NodeA": { "main": [ [ { "node": "NodeB", "type": "main", "index": 0 } ] ] }
820
-
821
- ### AI connections \u2014 CRITICAL: the SUB-NODE is the SOURCE, NOT the agent/chain:
822
- "OpenAI Chat Model": { "ai_languageModel": [ [ { "node": "AI Agent", "type": "ai_languageModel", "index": 0 } ] ] }
823
- "Simple Memory": { "ai_memory": [ [ { "node": "AI Agent", "type": "ai_memory", "index": 0 } ] ] }
824
- "Calculator Tool": { "ai_tool": [ [ { "node": "AI Agent", "type": "ai_tool", "index": 0 } ] ] }
825
-
826
- The AI Agent node does NOT appear in connections as a source for ai_* types.
827
- Every AI Agent must have at least one ai_languageModel sub-node connected.
828
-
829
- ### IF node \u2014 two output ports (0 = true, 1 = false):
830
- "IF Check": { "main": [ [{ "node": "True Path", "type": "main", "index": 0 }], [{ "node": "False Path", "type": "main", "index": 0 }] ] }
831
-
832
- ### SplitInBatches \u2014 two output ports (0 = done/finished, 1 = loop body per batch):
833
- Connect output 0 to the node that runs AFTER all batches complete.
834
- Connect output 1 to the processing chain for each batch. The last node in the chain loops back to SplitInBatches via main input.
835
-
836
- ### Webhook + RespondToWebhook pattern:
837
- When webhook responseMode is "responseNode", you MUST include a respondToWebhook node in the flow.
838
- "Webhook": { "main": [[{ "node": "Process Data", "type": "main", "index": 0 }]] }
839
- "Process Data": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] }
840
-
841
- ### Triggers have no incoming connections.
842
- ### Connection keys are NODE NAMES, never node IDs.
843
-
844
- ### Nested parameters:
845
- Node parameters like conditions, assignments, and rule intervals MUST include all required nested fields. Do not leave nested objects empty or partially filled.
846
-
847
- ---
848
-
849
- ## NODE CATALOG \u2014 exact type strings and safe typeVersions
850
-
851
- ### Triggers (always at least one required):
852
- n8n-nodes-base.manualTrigger typeVersion: 1 \u2014 testing only
853
- n8n-nodes-base.scheduleTrigger typeVersion: 1.2 \u2014 params: rule.interval[{field, ...}]
854
- n8n-nodes-base.webhook typeVersion: 2 \u2014 params: httpMethod, path, responseMode
855
- n8n-nodes-base.formTrigger typeVersion: 2.2
856
- n8n-nodes-base.emailReadImap typeVersion: 2 \u2014 cred: imap
857
- n8n-nodes-base.errorTrigger typeVersion: 1
858
- n8n-nodes-base.executeWorkflowTrigger typeVersion: 1.1
859
- n8n-nodes-base.gmailTrigger typeVersion: 1.2 \u2014 cred: gmailOAuth2
860
- n8n-nodes-base.slackTrigger typeVersion: 1 \u2014 cred: slackApi
861
- n8n-nodes-base.telegramTrigger typeVersion: 1.2 \u2014 cred: telegramApi
862
- n8n-nodes-base.githubTrigger typeVersion: 1 \u2014 cred: githubApi
863
- n8n-nodes-base.airtableTrigger typeVersion: 1 \u2014 cred: airtableTokenApi
864
- n8n-nodes-base.notionTrigger typeVersion: 1 \u2014 cred: notionApi
865
- @n8n/n8n-nodes-langchain.chatTrigger typeVersion: 1.1 \u2014 pairs with AI Agent
866
-
867
- ### Core logic:
868
- n8n-nodes-base.code typeVersion: 2 \u2014 params: mode, jsCode
869
- n8n-nodes-base.httpRequest typeVersion: 4.2 \u2014 params: method, url, [sendBody, jsonBody, sendHeaders, headerParameters]
870
- n8n-nodes-base.set typeVersion: 3.4 \u2014 params: assignments.assignments[{id, name, value, type}]
871
- n8n-nodes-base.if typeVersion: 2.2 \u2014 params: conditions.conditions[{id, leftValue, rightValue, operator}], combinator
872
- n8n-nodes-base.switch typeVersion: 3.2 \u2014 multi-branch routing
873
- n8n-nodes-base.filter typeVersion: 2.2 \u2014 params: conditions (same as IF), 1 output
874
- n8n-nodes-base.merge typeVersion: 3 \u2014 modes: append/combine/chooseBranch
875
- n8n-nodes-base.splitInBatches typeVersion: 3 \u2014 output 0=done, output 1=loop body
876
- n8n-nodes-base.wait typeVersion: 1.1
877
- n8n-nodes-base.executeWorkflow typeVersion: 1.2
878
- n8n-nodes-base.respondToWebhook typeVersion: 1.1 \u2014 required when webhook responseMode is "responseNode"
879
- n8n-nodes-base.noOp typeVersion: 1
880
- n8n-nodes-base.splitOut typeVersion: 1
881
- n8n-nodes-base.aggregate typeVersion: 1
882
- n8n-nodes-base.stickyNote typeVersion: 1 \u2014 never connected, canvas annotation only
883
-
884
- ### Email / messaging:
885
- n8n-nodes-base.emailSend typeVersion: 2.1 \u2014 cred: smtp
886
- n8n-nodes-base.slack typeVersion: 2.2 \u2014 cred: slackOAuth2Api \u2014 params: resource, operation, select, channelId{__rl}, text
887
- n8n-nodes-base.telegram typeVersion: 1.2 \u2014 cred: telegramApi
888
- n8n-nodes-base.discord typeVersion: 2 \u2014 cred: discordWebhookApi
889
-
890
- ### Google:
891
- n8n-nodes-base.gmail typeVersion: 2.1 \u2014 cred: gmailOAuth2 \u2014 params: resource, operation
892
- n8n-nodes-base.googleSheets typeVersion: 4.5 \u2014 cred: googleSheetsOAuth2Api \u2014 params: resource, operation, documentId{__rl}, sheetName{__rl}
893
- n8n-nodes-base.googleDrive typeVersion: 3 \u2014 cred: googleDriveOAuth2Api
894
- n8n-nodes-base.googleCalendar typeVersion: 1.3 \u2014 cred: googleCalendarOAuth2Api
895
-
896
- ### Productivity:
897
- n8n-nodes-base.notion typeVersion: 2.2 \u2014 cred: notionApi
898
- n8n-nodes-base.airtable typeVersion: 2.1 \u2014 cred: airtableTokenApi
899
- n8n-nodes-base.github typeVersion: 1.1 \u2014 cred: githubApi
900
- n8n-nodes-base.jira typeVersion: 1 \u2014 cred: jiraSoftwareCloudApi
901
- n8n-nodes-base.hubspot typeVersion: 2.1 \u2014 cred: hubspotOAuth2Api
902
-
903
- ### Databases:
904
- n8n-nodes-base.postgres typeVersion: 2.5 \u2014 cred: postgres
905
- n8n-nodes-base.mySql typeVersion: 2.4 \u2014 cred: mySql
906
- n8n-nodes-base.redis typeVersion: 1 \u2014 cred: redis
907
- n8n-nodes-base.supabase typeVersion: 1 \u2014 cred: supabaseApi
908
- n8n-nodes-base.awsS3 typeVersion: 2 \u2014 cred: aws
909
-
910
- ### AI \u2014 Root nodes (sit on main data flow, receive ai_* connections as TARGETS):
911
- @n8n/n8n-nodes-langchain.agent typeVersion: 1.9 \u2014 params: promptType, text (if define), options.systemMessage
912
- @n8n/n8n-nodes-langchain.chainLlm typeVersion: 1.5
913
- @n8n/n8n-nodes-langchain.chainRetrievalQa typeVersion: 1.4
914
- @n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi \u2014 standalone node, calls OpenAI directly without sub-nodes
915
- @n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi \u2014 standalone node, calls Anthropic directly without sub-nodes
916
-
917
- ### AI \u2014 Sub-nodes (sources of ai_* connections, wire INTO root nodes above):
918
- @n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
919
- @n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
920
- @n8n/n8n-nodes-langchain.lmChatGoogleGemini typeVersion: 1 \u2014 cred: googlePalmApi \u2014 ai_languageModel
921
- @n8n/n8n-nodes-langchain.memoryBufferWindow typeVersion: 1.3 \u2014 \u2014 ai_memory
922
- @n8n/n8n-nodes-langchain.toolWorkflow typeVersion: 2 \u2014 \u2014 ai_tool
923
- @n8n/n8n-nodes-langchain.toolCode typeVersion: 1.1 \u2014 \u2014 ai_tool
924
- @n8n/n8n-nodes-langchain.toolHttpRequest typeVersion: 1.1 \u2014 \u2014 ai_tool
925
- @n8n/n8n-nodes-langchain.toolCalculator typeVersion: 1 \u2014 \u2014 ai_tool
926
-
927
- ### Resource locator (__rl) format (Google / Slack / Notion modern nodes):
928
- { "__rl": true, "mode": "id", "value": "ACTUAL_ID" }
929
- { "__rl": true, "mode": "name", "value": "#channel-name" }
930
-
931
- ### App node parameter pattern:
932
- { "resource": "message", "operation": "send", ...operation-specific fields }
933
-
934
- ### Schedule Trigger \u2014 daily at 9am example:
935
- { "rule": { "interval": [{ "field": "days", "daysInterval": 1, "triggerAtHour": 9, "triggerAtMinute": 0 }] } }
936
- Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] } }
937
-
938
- ---
856
+ // src/telemetry/types.ts
857
+ var TELEMETRY_SCHEMA_VERSION = 2;
939
858
 
940
- ## PRE-DELIVERY SELF-CHECK (do this before calling the tool):
941
- 1. Every connection source/target name exists in nodes array
942
- 2. No duplicate node names
943
- 3. No duplicate node IDs
944
- 4. No forbidden fields at the workflow root
945
- 5. At least one trigger node present
946
- 6. Every AI Agent has an ai_languageModel sub-node
947
- 7. settings block is complete with executionOrder: "v1"
948
-
949
- ---
950
-
951
- Respond ONLY with a generate_workflow tool call. No prose. No markdown outside the tool call.
952
- If the request is impossible or unclear, set the error field instead of generating a workflow.`;
953
-
954
- // src/utils/thresholds.ts
955
- var DIRECT_THRESHOLD = 0.92;
956
- var REFERENCE_THRESHOLD = 0.72;
957
- function scoreToMode(score) {
958
- if (score >= DIRECT_THRESHOLD) return "direct";
959
- if (score >= REFERENCE_THRESHOLD) return "reference";
960
- return "scratch";
961
- }
962
-
963
- // src/validation/rule-metadata.ts
964
- var VALIDATOR_RULE_IDS = Array.from({ length: 23 }, (_, i) => i + 1);
965
- var RULE_PIPELINE_STAGES = {
966
- 1: "node_generation",
967
- 2: "node_generation",
968
- 3: "node_generation",
969
- 4: "node_generation",
970
- 5: "node_generation",
971
- 6: "node_generation",
972
- 7: "node_generation",
973
- 8: "node_generation",
974
- 9: "connection_wiring",
975
- 10: "connection_wiring",
976
- 11: "connection_wiring",
977
- 12: "workflow_structure",
978
- 13: "node_generation",
979
- 14: "workflow_structure",
980
- 15: "node_generation",
981
- 16: "node_generation",
982
- 17: "credential_injection",
983
- 18: "connection_wiring",
984
- 19: "node_generation",
985
- 20: "connection_wiring",
986
- 21: "workflow_structure",
987
- 22: "workflow_structure",
988
- 23: "node_generation"
989
- };
990
- var RULE_MITIGATIONS = {
991
- 1: "Provide a non-empty workflow name string",
992
- 2: "Include at least one node in the nodes array",
993
- 3: "Every node must have a unique UUID v4 string as its id field",
994
- 4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
995
- 5: "Every node must have a non-empty type string",
996
- 6: "Every node must have a positive integer typeVersion",
997
- 7: "Every node must have a position array of exactly [x, y] numbers",
998
- 8: "Every node must have a non-empty name string",
999
- 9: "connections must be a plain object (use {} if no connections)",
1000
- 10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
1001
- 11: "Every non-trigger node should have at least one incoming connection",
1002
- 12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
1003
- 13: "workflow.settings must be a plain object if present",
1004
- 14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
1005
- 15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1006
- 16: "All node names must be unique within the workflow",
1007
- 17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
1008
- 18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
1009
- 19: "Use known safe typeVersion values for each node type",
1010
- 20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1011
- 21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1012
- 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
1013
- 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync"
1014
- };
1015
-
1016
- // src/generation/prompt-builder.ts
1017
- var CRITICAL_SCORE_THRESHOLD = 0.15;
1018
- var PromptBuilder = class {
1019
- patternsPath;
1020
- constructor(patternsPath) {
1021
- this.patternsPath = patternsPath ?? join(homedir(), ".kairos", "patterns.json");
1022
- }
1023
- build(request, matches, globalFailureRates = [], dynamicCatalog) {
1024
- const mode = this.resolveMode(matches);
1025
- const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
1026
- const userMessage = this.buildUserMessage(request, matches, mode);
1027
- return { system, userMessage, mode, matches };
1028
- }
1029
- buildCorrectionMessage(request, matches, allIssues, attempt) {
1030
- const base = this.buildUserMessage(request, matches, this.resolveMode(matches));
1031
- return `${base}
1032
-
1033
- IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
1034
- ${allIssues.join("\n")}
1035
-
1036
- Fix ALL of the above issues in your new response. Do not repeat any of these mistakes.`;
1037
- }
1038
- resolveMode(matches) {
1039
- if (matches.length === 0) return "scratch";
1040
- const top = matches[0];
1041
- if (!top) return "scratch";
1042
- return scoreToMode(top.score);
1043
- }
1044
- buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog) {
1045
- let basePrompt = SYSTEM_PROMPT_V1;
1046
- if (dynamicCatalog) {
1047
- basePrompt = basePrompt.replace(
1048
- /## NODE CATALOG — exact type strings and safe typeVersions[\s\S]*?(?=## PRE-DELIVERY SELF-CHECK)/,
1049
- dynamicCatalog + "\n\n"
1050
- );
1051
- }
1052
- const blocks = [
1053
- {
1054
- type: "text",
1055
- text: basePrompt,
1056
- cache_control: { type: "ephemeral" }
1057
- }
1058
- ];
1059
- if (mode === "reference" && matches.length > 0) {
1060
- const refText = matches.slice(0, 3).map((m) => {
1061
- const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1062
- return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1063
- Nodes:
1064
- ${nodes}`;
1065
- }).join("\n\n");
1066
- blocks.push({
1067
- type: "text",
1068
- text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1069
-
1070
- ${refText}`
1071
- });
1072
- }
1073
- if (mode === "direct" && matches[0]) {
1074
- const match = matches[0];
1075
- const json = JSON.stringify(match.workflow.workflow, null, 2);
1076
- if (json.length > 3e4) {
1077
- const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1078
- blocks.push({
1079
- type: "text",
1080
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1081
- Nodes:
1082
- ${nodes}`
1083
- });
1084
- } else {
1085
- blocks.push({
1086
- type: "text",
1087
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1088
-
1089
- ${json}`
1090
- });
1091
- }
1092
- }
1093
- if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1094
- const hint = matches[0];
1095
- const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1096
- blocks.push({
1097
- type: "text",
1098
- text: `## Weak Structural Hint
1099
- A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
1100
- });
1101
- }
1102
- const warnings = this.buildFailureWarnings(matches, globalFailureRates);
1103
- if (warnings) {
1104
- blocks.push({ type: "text", text: warnings });
1105
- }
1106
- return blocks;
1107
- }
1108
- loadPatterns() {
1109
- try {
1110
- const raw = readFileSync(this.patternsPath, "utf-8");
1111
- const analysis = JSON.parse(raw);
1112
- const patterns = analysis.topFailureRules ?? [];
1113
- return patterns.filter((p) => typeof p.pipelineStage === "string" && typeof p.state === "string");
1114
- } catch {
1115
- return [];
1116
- }
1117
- }
1118
- getWarnedRules() {
1119
- return this.getActivePatterns().map((p) => p.rule);
1120
- }
1121
- getActivePatterns() {
1122
- const MAX_WARNED = 10;
1123
- const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
1124
- const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
1125
- const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1126
- const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1127
- return [...regressed, ...confirmed, ...drafts].slice(0, MAX_WARNED);
1128
- }
1129
- buildFailureWarnings(matches, globalFailureRates) {
1130
- const richPatterns = this.getActivePatterns();
1131
- if (richPatterns.length > 0) {
1132
- return this.buildStageGroupedWarnings(richPatterns, matches);
1133
- }
1134
- return this.buildLegacyWarnings(matches, globalFailureRates);
1135
- }
1136
- buildStageGroupedWarnings(patterns, matches) {
1137
- const stageLabels = {
1138
- credential_injection: "CREDENTIAL FORMATTING",
1139
- connection_wiring: "CONNECTION WIRING",
1140
- node_generation: "NODE GENERATION",
1141
- workflow_structure: "WORKFLOW STRUCTURE"
859
+ // src/telemetry/collector.ts
860
+ var TelemetryCollector = class {
861
+ dir;
862
+ sessionId;
863
+ dirReady = null;
864
+ constructor(dir) {
865
+ this.dir = dir ?? join(homedir(), ".kairos", "telemetry");
866
+ this.sessionId = generateUUID();
867
+ }
868
+ async emit(eventType, data, runId) {
869
+ const event = {
870
+ schemaVersion: TELEMETRY_SCHEMA_VERSION,
871
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
872
+ sessionId: this.sessionId,
873
+ ...runId ? { runId } : {},
874
+ eventType,
875
+ data
1142
876
  };
1143
- const byStage = /* @__PURE__ */ new Map();
1144
- for (const p of patterns) {
1145
- const list = byStage.get(p.pipelineStage) ?? [];
1146
- list.push(p);
1147
- byStage.set(p.pipelineStage, list);
1148
- }
1149
- const sections = [];
1150
- for (const [stage, stagePatterns] of byStage) {
1151
- const label = stageLabels[stage] ?? stage;
1152
- const byMitigation = /* @__PURE__ */ new Map();
1153
- for (const p of stagePatterns) {
1154
- const key = p.mitigation ?? `rule_${p.rule}`;
1155
- const list = byMitigation.get(key) ?? [];
1156
- list.push(p);
1157
- byMitigation.set(key, list);
1158
- }
1159
- const lines = [];
1160
- for (const group of byMitigation.values()) {
1161
- if (group.length === 1) {
1162
- const p = group[0];
1163
- const urgency = p.regressed ? "CRITICAL REGRESSION: " : (p.compositeScore ?? 0) >= CRITICAL_SCORE_THRESHOLD ? "CRITICAL: " : "";
1164
- const statePrefix = p.state === "confirmed" ? "[CONFIRMED] " : "";
1165
- const trendSuffix = p.trend === "worsening" ? " (GETTING WORSE)" : p.trend === "improving" ? " (improving)" : "";
1166
- const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
1167
- const remedyStr = remedy ? `
1168
- Fix: ${remedy}` : "";
1169
- lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}`);
1170
- } else {
1171
- const ruleNums = group.map((p) => p.rule).join(", ");
1172
- const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
1173
- const hasConfirmed = group.some((p) => p.state === "confirmed");
1174
- const statePrefix = hasConfirmed ? "[CONFIRMED] " : "";
1175
- const remedy = group[0].mitigation;
1176
- const remedyStr = remedy ? `
1177
- Fix: ${remedy}` : "";
1178
- lines.push(`- ${statePrefix}Rules ${ruleNums} (${totalFailures} failures combined): same root cause${remedyStr}`);
1179
- }
1180
- }
1181
- sections.push(`### ${label}
1182
- ${lines.join("\n")}`);
1183
- }
1184
- for (const match of matches) {
1185
- const fps = match.workflow.failurePatterns;
1186
- if (!fps?.length) continue;
1187
- const coveredRules = new Set(patterns.map((p) => p.rule));
1188
- const extra = fps.filter((fp) => !coveredRules.has(fp.rule));
1189
- for (const fp of extra) {
1190
- const remedy = RULE_MITIGATIONS[fp.rule];
1191
- const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1192
- sections.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen in similar workflows)`);
1193
- }
1194
- }
1195
- if (sections.length === 0) return null;
1196
- return `## Known Failure Patterns \u2014 AVOID THESE
1197
-
1198
- Grouped by generation stage. Fix these BEFORE outputting your response:
1199
-
1200
- ${sections.join("\n\n")}`;
1201
- }
1202
- buildLegacyWarnings(matches, globalFailureRates) {
1203
- const lines = [];
1204
- for (const match of matches) {
1205
- const patterns = match.workflow.failurePatterns;
1206
- if (!patterns?.length) continue;
1207
- for (const fp of patterns) {
1208
- const remedy = RULE_MITIGATIONS[fp.rule];
1209
- const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1210
- lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
1211
- }
1212
- }
1213
- const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
1214
- for (const rule of highFreqRules) {
1215
- const remedy = RULE_MITIGATIONS[rule.rule];
1216
- const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1217
- lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
877
+ if (!this.dirReady) {
878
+ this.dirReady = mkdir(this.dir, { recursive: true }).then(() => {
879
+ });
1218
880
  }
1219
- if (lines.length === 0) return null;
1220
- const unique = [...new Set(lines)];
1221
- return `## Known Failure Patterns \u2014 AVOID THESE
1222
-
1223
- Previous builds frequently failed the following validation rules. Ensure your output does NOT repeat these mistakes:
1224
- ${unique.join("\n")}`;
1225
- }
1226
- buildUserMessage(request, _matches, _mode) {
1227
- const namePart = request.name ? `
1228
- Workflow name: "${request.name}"` : "";
1229
- return `Build a workflow that: ${request.description}${namePart}`;
881
+ await this.dirReady;
882
+ const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
883
+ const filepath = join(this.dir, filename);
884
+ await appendFile(filepath, JSON.stringify(event) + "\n", "utf-8");
1230
885
  }
1231
886
  };
1232
887
 
@@ -1334,13 +989,93 @@ var TelemetryReader = class {
1334
989
  };
1335
990
 
1336
991
  // src/telemetry/pattern-analyzer.ts
1337
- import { writeFile, readFile as fsReadFile, appendFile, mkdir, rename } from "fs/promises";
992
+ import { writeFile, readFile as fsReadFile, appendFile as appendFile2, mkdir as mkdir2, rename } from "fs/promises";
1338
993
  import { join as join4 } from "path";
1339
994
  import { homedir as homedir3 } from "os";
995
+
996
+ // src/validation/rule-metadata.ts
997
+ var VALIDATOR_RULE_IDS = Array.from({ length: 26 }, (_, i) => i + 1);
998
+ var RULE_PIPELINE_STAGES = {
999
+ 1: "node_generation",
1000
+ 2: "node_generation",
1001
+ 3: "node_generation",
1002
+ 4: "node_generation",
1003
+ 5: "node_generation",
1004
+ 6: "node_generation",
1005
+ 7: "node_generation",
1006
+ 8: "node_generation",
1007
+ 9: "connection_wiring",
1008
+ 10: "connection_wiring",
1009
+ 11: "connection_wiring",
1010
+ 12: "workflow_structure",
1011
+ 13: "node_generation",
1012
+ 14: "workflow_structure",
1013
+ 15: "node_generation",
1014
+ 16: "node_generation",
1015
+ 17: "credential_injection",
1016
+ 18: "connection_wiring",
1017
+ 19: "node_generation",
1018
+ 20: "connection_wiring",
1019
+ 21: "workflow_structure",
1020
+ 22: "workflow_structure",
1021
+ 23: "node_generation",
1022
+ 24: "expression_syntax",
1023
+ 25: "expression_syntax",
1024
+ 26: "expression_syntax"
1025
+ };
1026
+ var RULE_EXAMPLES = {
1027
+ 17: {
1028
+ bad: '"credentials": { "slackOAuth2Api": "my-token" }',
1029
+ good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
1030
+ },
1031
+ 24: {
1032
+ bad: '$node["Fetch Data"].json.email',
1033
+ good: "$('Fetch Data').item.json.email"
1034
+ },
1035
+ 25: {
1036
+ bad: "$json.items[0].email",
1037
+ good: "$json.email"
1038
+ },
1039
+ 26: {
1040
+ bad: "$('Fetch Data').json.email",
1041
+ good: "$('Fetch Data').first().json.email"
1042
+ }
1043
+ };
1044
+ var RULE_MITIGATIONS = {
1045
+ 1: "Provide a non-empty workflow name string",
1046
+ 2: "Include at least one node in the nodes array",
1047
+ 3: "Every node must have a unique UUID v4 string as its id field",
1048
+ 4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
1049
+ 5: "Every node must have a non-empty type string",
1050
+ 6: "Every node must have a positive integer typeVersion",
1051
+ 7: "Every node must have a position array of exactly [x, y] numbers",
1052
+ 8: "Every node must have a non-empty name string",
1053
+ 9: "connections must be a plain object (use {} if no connections)",
1054
+ 10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
1055
+ 11: "Every non-trigger node should have at least one incoming connection",
1056
+ 12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
1057
+ 13: "workflow.settings must be a plain object if present",
1058
+ 14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
1059
+ 15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1060
+ 16: "All node names must be unique within the workflow",
1061
+ 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',
1062
+ 18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
1063
+ 19: "Use known safe typeVersion values for each node type",
1064
+ 20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1065
+ 21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1066
+ 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
1067
+ 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
1068
+ 24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
1069
+ 25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
1070
+ 26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
1071
+ };
1072
+
1073
+ // src/telemetry/pattern-analyzer.ts
1340
1074
  var PATTERN_SCHEMA_VERSION = 2;
1341
1075
  var PatternAnalyzer = class _PatternAnalyzer {
1342
1076
  telemetryDir;
1343
1077
  outputDir;
1078
+ _cachedEvents = null;
1344
1079
  constructor(telemetryDir) {
1345
1080
  const defaultDir = join4(homedir3(), ".kairos", "telemetry");
1346
1081
  this.telemetryDir = telemetryDir ?? defaultDir;
@@ -1369,19 +1104,23 @@ var PatternAnalyzer = class _PatternAnalyzer {
1369
1104
  }));
1370
1105
  }
1371
1106
  if (fromVersion < 2) {
1372
- migrated = migrated.map((p) => ({
1373
- ...p,
1374
- scoringFactors: {
1375
- ...p.scoringFactors,
1376
- stickinessBoost: p.scoringFactors.stickinessBoost ?? p.scoringFactors["validationBoost"] ?? 0
1377
- }
1378
- }));
1107
+ migrated = migrated.map((p) => {
1108
+ const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
1109
+ return {
1110
+ ...p,
1111
+ scoringFactors: {
1112
+ ...sf,
1113
+ stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
1114
+ }
1115
+ };
1116
+ });
1379
1117
  }
1380
1118
  return migrated;
1381
1119
  }
1382
1120
  async analyze(days = 30) {
1383
1121
  const previousPatterns = await this.loadPreviousPatterns();
1384
1122
  const events = await this.readAllEvents(days);
1123
+ this._cachedEvents = events;
1385
1124
  const starts = events.filter((e) => e.eventType === "build_start");
1386
1125
  const attempts = events.filter((e) => e.eventType === "generation_attempt");
1387
1126
  const passed = attempts.filter(
@@ -1394,13 +1133,18 @@ var PatternAnalyzer = class _PatternAnalyzer {
1394
1133
  const credentialFailures = /* @__PURE__ */ new Map();
1395
1134
  for (const a of failed) {
1396
1135
  const weight = this.recencyWeight(a.fileDate);
1136
+ const buildId = a.runId ?? a.sessionId;
1397
1137
  const data = a.data;
1398
1138
  for (const issue of data.issues ?? []) {
1399
- const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [] };
1139
+ if (issue.severity === "warn") continue;
1140
+ const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
1400
1141
  entry.count++;
1401
- entry.sessions.add(a.sessionId);
1142
+ entry.sessions.add(buildId);
1402
1143
  entry.recencyWeights.push(weight);
1403
1144
  entry.allMessages.push(issue.message);
1145
+ if (data.workflowType) {
1146
+ entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
1147
+ }
1404
1148
  ruleFailures.set(issue.rule, entry);
1405
1149
  if (issue.rule === 17) {
1406
1150
  const credPatterns = [
@@ -1453,9 +1197,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
1453
1197
  }
1454
1198
  const sessions = /* @__PURE__ */ new Map();
1455
1199
  for (const a of attempts) {
1456
- const list = sessions.get(a.sessionId) ?? [];
1200
+ const buildId = a.runId ?? a.sessionId;
1201
+ const list = sessions.get(buildId) ?? [];
1457
1202
  list.push(a);
1458
- sessions.set(a.sessionId, list);
1203
+ sessions.set(buildId, list);
1459
1204
  }
1460
1205
  let firstTryPass = 0;
1461
1206
  let correctionNeeded = 0;
@@ -1502,7 +1247,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1502
1247
  const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
1503
1248
  const stickiness = stickinessCount.get(rule) ?? 0;
1504
1249
  const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
1505
- return {
1250
+ const pattern = {
1506
1251
  rule,
1507
1252
  failureCount: entry.count,
1508
1253
  confidence: Math.round(rawConfidence * 1e3) / 1e3,
@@ -1514,6 +1259,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
1514
1259
  exampleMessages: this.deduplicateMessages(entry.allMessages),
1515
1260
  mitigation: RULE_MITIGATIONS[rule] ?? null
1516
1261
  };
1262
+ if (entry.workflowTypes.size > 0) {
1263
+ pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
1264
+ }
1265
+ return pattern;
1517
1266
  }).sort((a, b) => b.compositeScore - a.compositeScore);
1518
1267
  const activeRules = new Set(activePatterns.map((p) => p.rule));
1519
1268
  for (const p of activePatterns) {
@@ -1570,7 +1319,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1570
1319
  const warned = bcData.warnedRules ?? [];
1571
1320
  if (warned.length === 0) continue;
1572
1321
  const sessionFailedRules = /* @__PURE__ */ new Set();
1573
- const sessionAttempts = sessions.get(bc.sessionId) ?? [];
1322
+ const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
1574
1323
  for (const a of sessionAttempts) {
1575
1324
  const ad = a.data;
1576
1325
  if (ad.validationPassed === false) {
@@ -1637,7 +1386,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1637
1386
  }
1638
1387
  async analyzeAndSave(days = 30) {
1639
1388
  const analysis = await this.analyze(days);
1640
- await mkdir(this.outputDir, { recursive: true });
1389
+ await mkdir2(this.outputDir, { recursive: true });
1641
1390
  const outputPath = join4(this.outputDir, "patterns.json");
1642
1391
  const tmpPath = `${outputPath}.tmp`;
1643
1392
  await writeFile(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
@@ -1652,9 +1401,56 @@ var PatternAnalyzer = class _PatternAnalyzer {
1652
1401
  topRules: analysis.topFailureRules.filter((p) => p.state !== "resolved").slice(0, 5).map((p) => ({ rule: p.rule, compositeScore: p.compositeScore, state: p.state }))
1653
1402
  };
1654
1403
  const historyPath = join4(this.outputDir, "pattern-history.jsonl");
1655
- await appendFile(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
1404
+ await appendFile2(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
1405
+ const sessions = await this.buildSessionSummaries(days);
1406
+ const sessionHistoryPath = join4(this.outputDir, "session-history.json");
1407
+ const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
1408
+ await writeFile(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
1409
+ await rename(sessionHistoryTmp, sessionHistoryPath);
1656
1410
  return analysis;
1657
1411
  }
1412
+ async getSessions(limit = 20) {
1413
+ try {
1414
+ const raw = await fsReadFile(join4(this.outputDir, "session-history.json"), "utf-8");
1415
+ const all = JSON.parse(raw);
1416
+ return all.slice(-limit);
1417
+ } catch {
1418
+ return [];
1419
+ }
1420
+ }
1421
+ async buildSessionSummaries(days = 30) {
1422
+ const events = this._cachedEvents ?? await this.readAllEvents(days);
1423
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
1424
+ const attemptsByBuild = /* @__PURE__ */ new Map();
1425
+ for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
1426
+ const buildId = e.runId ?? e.sessionId;
1427
+ const list = attemptsByBuild.get(buildId) ?? [];
1428
+ list.push(e);
1429
+ attemptsByBuild.set(buildId, list);
1430
+ }
1431
+ const summaries = buildCompletes.map((bc) => {
1432
+ const data = bc.data;
1433
+ const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
1434
+ const failedRules = Array.from(new Set(
1435
+ sessionAttempts.flatMap((a) => {
1436
+ const ad = a.data;
1437
+ if (ad.validationPassed !== false) return [];
1438
+ return (ad.issues ?? []).map((i) => i.rule);
1439
+ })
1440
+ ));
1441
+ return {
1442
+ sessionId: bc.sessionId,
1443
+ date: bc.fileDate,
1444
+ description: data.description ?? "",
1445
+ workflowType: data.workflowType ?? null,
1446
+ attempts: data.totalAttempts ?? 1,
1447
+ success: data.success ?? false,
1448
+ failedRules,
1449
+ workflowName: data.workflowName ?? null
1450
+ };
1451
+ });
1452
+ return summaries.sort((a, b) => a.date.localeCompare(b.date));
1453
+ }
1658
1454
  async getHistory(limit = 20) {
1659
1455
  try {
1660
1456
  const raw = await fsReadFile(join4(this.outputDir, "pattern-history.jsonl"), "utf-8");
@@ -1676,7 +1472,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1676
1472
  alerts.push({
1677
1473
  type: "stale_pattern",
1678
1474
  rule: p.rule,
1679
- message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-23)`
1475
+ message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-26)`
1680
1476
  });
1681
1477
  }
1682
1478
  }
@@ -1979,13 +1775,17 @@ function rerank(candidates, clusters) {
1979
1775
  }
1980
1776
 
1981
1777
  // src/library/file-library.ts
1982
- import { readFile, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "fs/promises";
1778
+ import { readFile, writeFile as writeFile2, rename as rename2, mkdir as mkdir3, stat } from "fs/promises";
1983
1779
  import { join as join5 } from "path";
1984
1780
  import { homedir as homedir4 } from "os";
1985
1781
 
1986
- // src/utils/uuid.ts
1987
- function generateUUID() {
1988
- return crypto.randomUUID();
1782
+ // src/utils/thresholds.ts
1783
+ var DIRECT_THRESHOLD = 0.92;
1784
+ var REFERENCE_THRESHOLD = 0.72;
1785
+ function scoreToMode(score) {
1786
+ if (score >= DIRECT_THRESHOLD) return "direct";
1787
+ if (score >= REFERENCE_THRESHOLD) return "reference";
1788
+ return "scratch";
1989
1789
  }
1990
1790
 
1991
1791
  // src/library/file-library.ts
@@ -2001,14 +1801,28 @@ function buildSearchCorpus(w) {
2001
1801
  return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
2002
1802
  }
2003
1803
  var MAX_LIBRARY_SIZE = 500;
1804
+ function isValidMeta(item) {
1805
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
1806
+ }
1807
+ function isValidOldEntry(item) {
1808
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
1809
+ item.workflow.nodes
1810
+ );
1811
+ }
2004
1812
  var FileLibrary = class {
2005
1813
  dir;
2006
- workflows = [];
1814
+ meta = [];
2007
1815
  initPromise = null;
2008
1816
  writeQueue = Promise.resolve();
2009
1817
  constructor(dir) {
2010
1818
  this.dir = dir ?? join5(homedir4(), ".kairos", "library");
2011
1819
  }
1820
+ get workflowsDir() {
1821
+ return join5(this.dir, "workflows");
1822
+ }
1823
+ workflowFilePath(id) {
1824
+ return join5(this.workflowsDir, `${id}.json`);
1825
+ }
2012
1826
  async initialize() {
2013
1827
  if (!this.initPromise) {
2014
1828
  this.initPromise = this.doInitialize();
@@ -2016,62 +1830,149 @@ var FileLibrary = class {
2016
1830
  return this.initPromise;
2017
1831
  }
2018
1832
  async doInitialize() {
2019
- await mkdir2(this.dir, { recursive: true });
1833
+ await mkdir3(this.dir, { recursive: true });
2020
1834
  const indexPath = join5(this.dir, "index.json");
1835
+ let workflowsDirExists = false;
2021
1836
  try {
2022
- const raw = await readFile(indexPath, "utf-8");
2023
- const parsed = JSON.parse(raw);
2024
- if (!Array.isArray(parsed)) {
2025
- this.workflows = [];
2026
- } else {
2027
- this.workflows = parsed.filter(
2028
- (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)
2029
- );
1837
+ await stat(this.workflowsDir);
1838
+ workflowsDirExists = true;
1839
+ } catch {
1840
+ }
1841
+ if (workflowsDirExists) {
1842
+ try {
1843
+ const raw = await readFile(indexPath, "utf-8");
1844
+ const parsed = JSON.parse(raw);
1845
+ if (Array.isArray(parsed)) {
1846
+ this.meta = parsed.filter(isValidMeta);
1847
+ }
1848
+ } catch {
1849
+ this.meta = [];
2030
1850
  }
1851
+ } else {
1852
+ try {
1853
+ const raw = await readFile(indexPath, "utf-8");
1854
+ const parsed = JSON.parse(raw);
1855
+ if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
1856
+ await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
1857
+ return;
1858
+ }
1859
+ } catch {
1860
+ }
1861
+ this.meta = [];
1862
+ await mkdir3(this.workflowsDir, { recursive: true });
1863
+ }
1864
+ }
1865
+ /**
1866
+ * One-time transparent migration from v0.4.x monolithic index.json.
1867
+ * Splits each stored workflow into a per-file workflow JSON and a lightweight
1868
+ * meta entry. Rewrites index.json in the new format.
1869
+ */
1870
+ async migrateFromMonolithic(oldEntries) {
1871
+ await mkdir3(this.workflowsDir, { recursive: true });
1872
+ const newMeta = [];
1873
+ for (const entry of oldEntries) {
1874
+ const wfPath = this.workflowFilePath(entry.id);
1875
+ const tmpPath = `${wfPath}.tmp`;
1876
+ await writeFile2(tmpPath, JSON.stringify(entry.workflow), "utf-8");
1877
+ await rename2(tmpPath, wfPath);
1878
+ const { workflow, ...metaFields } = entry;
1879
+ newMeta.push({
1880
+ ...metaFields,
1881
+ workflowName: workflow.name,
1882
+ cachedNodeTypes: workflow.nodes.map((n) => n.type)
1883
+ });
1884
+ }
1885
+ this.meta = newMeta;
1886
+ await this.persistNow();
1887
+ }
1888
+ async loadWorkflowFile(id) {
1889
+ try {
1890
+ const raw = await readFile(this.workflowFilePath(id), "utf-8");
1891
+ return JSON.parse(raw);
2031
1892
  } catch {
2032
- this.workflows = [];
1893
+ return null;
2033
1894
  }
2034
1895
  }
1896
+ async writeWorkflowFile(id, workflow) {
1897
+ const wfPath = this.workflowFilePath(id);
1898
+ const tmpPath = `${wfPath}.tmp`;
1899
+ await writeFile2(tmpPath, JSON.stringify(workflow), "utf-8");
1900
+ await rename2(tmpPath, wfPath);
1901
+ }
1902
+ /**
1903
+ * Build a lightweight StoredWorkflow shell from a meta entry for use in
1904
+ * scoring / clustering. Only node.type is populated in each node — no other
1905
+ * node fields are used by hybridScore or clusterWorkflows.
1906
+ */
1907
+ makeSearchShell(m) {
1908
+ return {
1909
+ ...m,
1910
+ workflow: {
1911
+ name: m.workflowName,
1912
+ nodes: m.cachedNodeTypes.map((type) => ({
1913
+ id: "",
1914
+ name: "",
1915
+ type,
1916
+ typeVersion: 1,
1917
+ position: [0, 0],
1918
+ parameters: {}
1919
+ })),
1920
+ connections: {}
1921
+ }
1922
+ };
1923
+ }
2035
1924
  async search(description, options) {
2036
- const searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
2037
- if (searchable.length === 0) return [];
1925
+ const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
1926
+ if (filteredMeta.length === 0) return [];
2038
1927
  const limit = options?.limit ?? 3;
2039
1928
  const queryTokens = tokenize(description);
2040
1929
  if (queryTokens.length === 0) return [];
2041
- const docTokenArrays = searchable.map((w) => tokenize(buildSearchCorpus(w)));
1930
+ const shells = filteredMeta.map((m) => this.makeSearchShell(m));
1931
+ const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
2042
1932
  const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
2043
- const docCount = searchable.length;
1933
+ const docCount = shells.length;
2044
1934
  const idf = /* @__PURE__ */ new Map();
2045
1935
  const allTokens = new Set(queryTokens);
2046
1936
  for (const token of allTokens) {
2047
1937
  const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
2048
1938
  idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
2049
1939
  }
2050
- const scored = hybridScore(queryTokens, description, searchable, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
2051
- const clusters = clusterWorkflows(searchable);
1940
+ const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
1941
+ const clusters = clusterWorkflows(shells);
2052
1942
  const reranked = rerank(scored, clusters).slice(0, limit);
2053
- const results = reranked.map((m) => {
2054
- return { workflow: m.workflow, score: m.score, mode: scoreToMode(m.score) };
2055
- });
2056
- if (results.length > 0) {
2057
- for (const r of results) {
2058
- r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
2059
- }
2060
- this.persist();
2061
- }
2062
- return results;
1943
+ if (reranked.length === 0) return [];
1944
+ for (const r of reranked) {
1945
+ const m = this.meta.find((m2) => m2.id === r.workflow.id);
1946
+ if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
1947
+ }
1948
+ this.persist();
1949
+ const results = await Promise.all(
1950
+ reranked.map(async (r) => {
1951
+ const m = this.meta.find((meta) => meta.id === r.workflow.id);
1952
+ const workflow = await this.loadWorkflowFile(r.workflow.id);
1953
+ if (!workflow) return null;
1954
+ return {
1955
+ workflow: { ...m, workflow },
1956
+ score: r.score,
1957
+ mode: scoreToMode(r.score)
1958
+ };
1959
+ })
1960
+ );
1961
+ return results.filter((r) => r !== null);
2063
1962
  }
2064
1963
  async save(workflow, metadata) {
2065
1964
  const id = generateUUID();
1965
+ await this.writeWorkflowFile(id, workflow);
2066
1966
  const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
2067
- const stored = {
1967
+ const meta = {
2068
1968
  id,
2069
- workflow,
2070
1969
  description: metadata.description,
2071
1970
  tags: metadata.tags ?? [],
2072
1971
  platform: metadata.platform ?? "n8n",
2073
1972
  deployCount: 0,
2074
1973
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1974
+ workflowName: workflow.name,
1975
+ cachedNodeTypes: workflow.nodes.map((n) => n.type),
2075
1976
  ...failurePatterns?.length ? { failurePatterns } : {},
2076
1977
  ...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
2077
1978
  ...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
@@ -2083,31 +1984,35 @@ var FileLibrary = class {
2083
1984
  ...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
2084
1985
  ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
2085
1986
  };
2086
- this.workflows.push(stored);
2087
- if (this.workflows.length > MAX_LIBRARY_SIZE) {
2088
- this.workflows.sort((a, b) => (b.deployCount ?? 1) - (a.deployCount ?? 1));
2089
- this.workflows = this.workflows.slice(0, MAX_LIBRARY_SIZE);
1987
+ this.meta.push(meta);
1988
+ if (this.meta.length > MAX_LIBRARY_SIZE) {
1989
+ this.meta.sort((a, b) => {
1990
+ if (a.id === id) return -1;
1991
+ if (b.id === id) return 1;
1992
+ return (b.deployCount ?? 0) - (a.deployCount ?? 0);
1993
+ });
1994
+ this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
2090
1995
  }
2091
1996
  await this.persist();
2092
1997
  return id;
2093
1998
  }
2094
1999
  async recordDeployment(id) {
2095
- const w = this.workflows.find((w2) => w2.id === id);
2096
- if (w) {
2097
- w.deployCount++;
2098
- w.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
2000
+ const m = this.meta.find((m2) => m2.id === id);
2001
+ if (m) {
2002
+ m.deployCount++;
2003
+ m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
2099
2004
  await this.persist();
2100
2005
  }
2101
2006
  }
2102
2007
  async recordOutcome(id, outcome) {
2103
- const w = this.workflows.find((w2) => w2.id === id);
2104
- if (!w) return;
2008
+ const m = this.meta.find((m2) => m2.id === id);
2009
+ if (!m) return;
2105
2010
  if (outcome.mode === "direct") {
2106
- w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
2011
+ m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
2107
2012
  } else {
2108
- w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
2013
+ m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
2109
2014
  }
2110
- const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
2015
+ const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
2111
2016
  stats.totalUses++;
2112
2017
  stats.totalAttempts += outcome.attempts;
2113
2018
  if (outcome.firstTryPass) stats.firstTryPasses++;
@@ -2115,24 +2020,35 @@ var FileLibrary = class {
2115
2020
  const key = String(rule);
2116
2021
  stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
2117
2022
  }
2118
- w.outcomeStats = stats;
2023
+ m.outcomeStats = stats;
2119
2024
  await this.persist();
2120
2025
  }
2121
2026
  async drain() {
2122
2027
  await this.writeQueue;
2123
2028
  }
2124
2029
  async get(id) {
2125
- return this.workflows.find((w) => w.id === id) ?? null;
2030
+ const m = this.meta.find((m2) => m2.id === id);
2031
+ if (!m) return null;
2032
+ const workflow = await this.loadWorkflowFile(id);
2033
+ if (!workflow) return null;
2034
+ return { ...m, workflow };
2126
2035
  }
2127
2036
  async list(filters) {
2128
- let result = this.workflows;
2037
+ let filtered = this.meta;
2129
2038
  if (filters?.platform) {
2130
- result = result.filter((w) => w.platform === filters.platform);
2039
+ filtered = filtered.filter((m) => m.platform === filters.platform);
2131
2040
  }
2132
2041
  if (filters?.tags && filters.tags.length > 0) {
2133
- result = result.filter((w) => filters.tags.some((t) => w.tags.includes(t)));
2134
- }
2135
- return result;
2042
+ filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
2043
+ }
2044
+ const results = await Promise.all(
2045
+ filtered.map(async (m) => {
2046
+ const workflow = await this.loadWorkflowFile(m.id);
2047
+ if (!workflow) return null;
2048
+ return { ...m, workflow };
2049
+ })
2050
+ );
2051
+ return results.filter((r) => r !== null);
2136
2052
  }
2137
2053
  deduplicateFailurePatterns(patterns) {
2138
2054
  if (!patterns?.length) return void 0;
@@ -2147,11 +2063,36 @@ var FileLibrary = class {
2147
2063
  }
2148
2064
  return [...map.values()];
2149
2065
  }
2066
+ /**
2067
+ * Direct write used only during migration (before writeQueue is needed).
2068
+ */
2069
+ async persistNow() {
2070
+ const indexPath = join5(this.dir, "index.json");
2071
+ const tmpPath = `${indexPath}.tmp`;
2072
+ await writeFile2(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
2073
+ await rename2(tmpPath, indexPath);
2074
+ }
2150
2075
  persist() {
2151
2076
  this.writeQueue = this.writeQueue.then(async () => {
2152
2077
  const indexPath = join5(this.dir, "index.json");
2078
+ let onDisk = [];
2079
+ try {
2080
+ const raw = await readFile(indexPath, "utf-8");
2081
+ const parsed = JSON.parse(raw);
2082
+ if (Array.isArray(parsed)) {
2083
+ onDisk = parsed.filter(isValidMeta);
2084
+ }
2085
+ } catch {
2086
+ }
2087
+ const ourIds = new Set(this.meta.map((m) => m.id));
2088
+ const external = onDisk.filter((m) => !ourIds.has(m.id));
2089
+ let merged = [...this.meta, ...external];
2090
+ if (merged.length > MAX_LIBRARY_SIZE) {
2091
+ merged.sort((a, b) => (b.deployCount ?? 0) - (a.deployCount ?? 0));
2092
+ merged = merged.slice(0, MAX_LIBRARY_SIZE);
2093
+ }
2153
2094
  const tmpPath = `${indexPath}.tmp`;
2154
- await writeFile2(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
2095
+ await writeFile2(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
2155
2096
  await rename2(tmpPath, indexPath);
2156
2097
  });
2157
2098
  return this.writeQueue;
@@ -2169,7 +2110,9 @@ export {
2169
2110
  NodeRegistry,
2170
2111
  N8nValidator,
2171
2112
  scoreToMode,
2172
- PromptBuilder,
2113
+ RULE_EXAMPLES,
2114
+ RULE_MITIGATIONS,
2115
+ TelemetryCollector,
2173
2116
  TelemetryReader,
2174
2117
  PatternAnalyzer,
2175
2118
  nullLogger,
@@ -2180,4 +2123,4 @@ export {
2180
2123
  buildSearchCorpus,
2181
2124
  FileLibrary
2182
2125
  };
2183
- //# sourceMappingURL=chunk-NJ6QZBIC.js.map
2126
+ //# sourceMappingURL=chunk-6IXW3WCC.js.map