@kairos-sdk/core 0.3.2 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,17 +402,31 @@ 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);
408
+ if (Array.isArray(workflow.nodes)) {
409
+ const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
410
+ for (const issue of issues) {
411
+ if (issue.nodeId && !issue.nodeType) {
412
+ const nt = nodeById.get(issue.nodeId);
413
+ if (nt) issue.nodeType = nt;
414
+ }
415
+ }
416
+ }
400
417
  const errors = issues.filter((i) => i.severity === "error");
401
418
  return { valid: errors.length === 0, issues };
402
419
  }
403
- err(issues, rule, message, nodeId) {
420
+ err(issues, rule, message, nodeId, nodeType) {
404
421
  const issue = { rule, severity: "error", message };
405
422
  if (nodeId !== void 0) issue.nodeId = nodeId;
423
+ if (nodeType !== void 0) issue.nodeType = nodeType;
406
424
  issues.push(issue);
407
425
  }
408
- warn(issues, rule, message, nodeId) {
426
+ warn(issues, rule, message, nodeId, nodeType) {
409
427
  const issue = { rule, severity: "warn", message };
410
428
  if (nodeId !== void 0) issue.nodeId = nodeId;
429
+ if (nodeType !== void 0) issue.nodeType = nodeType;
411
430
  issues.push(issue);
412
431
  }
413
432
  isTriggerNode(node) {
@@ -518,10 +537,14 @@ var N8nValidator = class {
518
537
  checkRule11(w, issues) {
519
538
  if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
520
539
  const reachable = /* @__PURE__ */ new Set();
521
- for (const [, outputs] of Object.entries(w.connections)) {
540
+ const aiSubNodeSources = /* @__PURE__ */ new Set();
541
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
522
542
  if (typeof outputs !== "object" || outputs === null) continue;
523
- for (const portGroup of Object.values(outputs)) {
543
+ let hasAiPort = false;
544
+ for (const [portName, portGroup] of Object.entries(outputs)) {
524
545
  if (!Array.isArray(portGroup)) continue;
546
+ const isAiPort = portName.startsWith("ai_");
547
+ if (isAiPort) hasAiPort = true;
525
548
  for (const targets of portGroup) {
526
549
  if (!Array.isArray(targets)) continue;
527
550
  for (const target of targets) {
@@ -530,10 +553,13 @@ var N8nValidator = class {
530
553
  }
531
554
  }
532
555
  }
556
+ if (hasAiPort) aiSubNodeSources.add(sourceName);
533
557
  }
534
558
  for (const node of w.nodes) {
535
559
  if (node.type.includes("stickyNote")) continue;
536
- 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)) {
537
563
  this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
538
564
  }
539
565
  }
@@ -729,6 +755,76 @@ var N8nValidator = class {
729
755
  }
730
756
  }
731
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
+ }
732
828
  // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
733
829
  checkRule21(w, issues) {
734
830
  if (!Array.isArray(w.nodes)) return;
@@ -752,348 +848,93 @@ var N8nValidator = class {
752
848
  }
753
849
  };
754
850
 
755
- // src/generation/prompts/v1.ts
756
- 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.
757
-
758
- ## HARD RULES \u2014 violating any of these causes immediate deployment failure
851
+ // src/telemetry/collector.ts
852
+ import { appendFile, mkdir } from "fs/promises";
853
+ import { join } from "path";
854
+ import { homedir } from "os";
759
855
 
760
- ### Forbidden fields \u2014 NEVER include these in the workflow object:
761
- id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId, activeVersion, pinData, triggerCount, shared, staticData
856
+ // src/telemetry/types.ts
857
+ var TELEMETRY_SCHEMA_VERSION = 2;
762
858
 
763
- ### Required top-level structure:
764
- {
765
- "name": "<descriptive name>",
766
- "nodes": [...],
767
- "connections": {...},
768
- "settings": {
769
- "saveExecutionProgress": true,
770
- "saveManualExecutions": true,
771
- "saveDataErrorExecution": "all",
772
- "saveDataSuccessExecution": "all",
773
- "executionTimeout": 3600,
774
- "timezone": "UTC",
775
- "executionOrder": "v1"
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
876
+ };
877
+ if (!this.dirReady) {
878
+ this.dirReady = mkdir(this.dir, { recursive: true }).then(() => {
879
+ });
880
+ }
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");
776
885
  }
777
- }
778
-
779
- ### Node IDs:
780
- - Every node.id must be a valid UUID v4 (random hex, format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
781
- - Never reuse IDs, never use sequential fake IDs like "node-1"
782
-
783
- ### Credentials:
784
- - Only reference credentials with exact type names (see catalog below)
785
- - If credential ID is unknown, OMIT the credentials block entirely \u2014 never invent credential IDs
786
- - Never put API keys or tokens in parameters when a credential type exists
787
-
788
- ### Node names:
789
- - All node names must be unique within the workflow
790
- - Use descriptive names: "Fetch Open Invoices" not "HTTP Request 2"
791
-
792
- ### Positioning:
793
- - Trigger node: [250, 300]
794
- - Each subsequent step: x + 220 minimum
795
- - Parallel branches: offset y by \xB1150
796
- - AI sub-nodes: place below their root node (y + 200)
797
-
798
- ---
799
-
800
- ## CONNECTION RULES \u2014 the most common source of errors
801
-
802
- ### Standard connections (main data flow):
803
- "NodeA": { "main": [ [ { "node": "NodeB", "type": "main", "index": 0 } ] ] }
804
-
805
- ### AI connections \u2014 CRITICAL: the SUB-NODE is the SOURCE, NOT the agent/chain:
806
- "OpenAI Chat Model": { "ai_languageModel": [ [ { "node": "AI Agent", "type": "ai_languageModel", "index": 0 } ] ] }
807
- "Simple Memory": { "ai_memory": [ [ { "node": "AI Agent", "type": "ai_memory", "index": 0 } ] ] }
808
- "Calculator Tool": { "ai_tool": [ [ { "node": "AI Agent", "type": "ai_tool", "index": 0 } ] ] }
809
-
810
- The AI Agent node does NOT appear in connections as a source for ai_* types.
811
- Every AI Agent must have at least one ai_languageModel sub-node connected.
812
-
813
- ### IF node \u2014 two output ports (0 = true, 1 = false):
814
- "IF Check": { "main": [ [{ "node": "True Path", "type": "main", "index": 0 }], [{ "node": "False Path", "type": "main", "index": 0 }] ] }
815
-
816
- ### SplitInBatches \u2014 two output ports (0 = done/finished, 1 = loop body per batch):
817
- Connect output 0 to the node that runs AFTER all batches complete.
818
- Connect output 1 to the processing chain for each batch. The last node in the chain loops back to SplitInBatches via main input.
819
-
820
- ### Webhook + RespondToWebhook pattern:
821
- When webhook responseMode is "responseNode", you MUST include a respondToWebhook node in the flow.
822
- "Webhook": { "main": [[{ "node": "Process Data", "type": "main", "index": 0 }]] }
823
- "Process Data": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] }
824
-
825
- ### Triggers have no incoming connections.
826
- ### Connection keys are NODE NAMES, never node IDs.
827
-
828
- ### Nested parameters:
829
- Node parameters like conditions, assignments, and rule intervals MUST include all required nested fields. Do not leave nested objects empty or partially filled.
830
-
831
- ---
832
-
833
- ## NODE CATALOG \u2014 exact type strings and safe typeVersions
834
-
835
- ### Triggers (always at least one required):
836
- n8n-nodes-base.manualTrigger typeVersion: 1 \u2014 testing only
837
- n8n-nodes-base.scheduleTrigger typeVersion: 1.2 \u2014 params: rule.interval[{field, ...}]
838
- n8n-nodes-base.webhook typeVersion: 2 \u2014 params: httpMethod, path, responseMode
839
- n8n-nodes-base.formTrigger typeVersion: 2.2
840
- n8n-nodes-base.emailReadImap typeVersion: 2 \u2014 cred: imap
841
- n8n-nodes-base.errorTrigger typeVersion: 1
842
- n8n-nodes-base.executeWorkflowTrigger typeVersion: 1.1
843
- n8n-nodes-base.gmailTrigger typeVersion: 1.2 \u2014 cred: gmailOAuth2
844
- n8n-nodes-base.slackTrigger typeVersion: 1 \u2014 cred: slackApi
845
- n8n-nodes-base.telegramTrigger typeVersion: 1.2 \u2014 cred: telegramApi
846
- n8n-nodes-base.githubTrigger typeVersion: 1 \u2014 cred: githubApi
847
- n8n-nodes-base.airtableTrigger typeVersion: 1 \u2014 cred: airtableTokenApi
848
- n8n-nodes-base.notionTrigger typeVersion: 1 \u2014 cred: notionApi
849
- @n8n/n8n-nodes-langchain.chatTrigger typeVersion: 1.1 \u2014 pairs with AI Agent
850
-
851
- ### Core logic:
852
- n8n-nodes-base.code typeVersion: 2 \u2014 params: mode, jsCode
853
- n8n-nodes-base.httpRequest typeVersion: 4.2 \u2014 params: method, url, [sendBody, jsonBody, sendHeaders, headerParameters]
854
- n8n-nodes-base.set typeVersion: 3.4 \u2014 params: assignments.assignments[{id, name, value, type}]
855
- n8n-nodes-base.if typeVersion: 2.2 \u2014 params: conditions.conditions[{id, leftValue, rightValue, operator}], combinator
856
- n8n-nodes-base.switch typeVersion: 3.2 \u2014 multi-branch routing
857
- n8n-nodes-base.filter typeVersion: 2.2 \u2014 params: conditions (same as IF), 1 output
858
- n8n-nodes-base.merge typeVersion: 3 \u2014 modes: append/combine/chooseBranch
859
- n8n-nodes-base.splitInBatches typeVersion: 3 \u2014 output 0=done, output 1=loop body
860
- n8n-nodes-base.wait typeVersion: 1.1
861
- n8n-nodes-base.executeWorkflow typeVersion: 1.2
862
- n8n-nodes-base.respondToWebhook typeVersion: 1.1 \u2014 required when webhook responseMode is "responseNode"
863
- n8n-nodes-base.noOp typeVersion: 1
864
- n8n-nodes-base.splitOut typeVersion: 1
865
- n8n-nodes-base.aggregate typeVersion: 1
866
- n8n-nodes-base.stickyNote typeVersion: 1 \u2014 never connected, canvas annotation only
867
-
868
- ### Email / messaging:
869
- n8n-nodes-base.emailSend typeVersion: 2.1 \u2014 cred: smtp
870
- n8n-nodes-base.slack typeVersion: 2.2 \u2014 cred: slackOAuth2Api \u2014 params: resource, operation, select, channelId{__rl}, text
871
- n8n-nodes-base.telegram typeVersion: 1.2 \u2014 cred: telegramApi
872
- n8n-nodes-base.discord typeVersion: 2 \u2014 cred: discordWebhookApi
873
-
874
- ### Google:
875
- n8n-nodes-base.gmail typeVersion: 2.1 \u2014 cred: gmailOAuth2 \u2014 params: resource, operation
876
- n8n-nodes-base.googleSheets typeVersion: 4.5 \u2014 cred: googleSheetsOAuth2Api \u2014 params: resource, operation, documentId{__rl}, sheetName{__rl}
877
- n8n-nodes-base.googleDrive typeVersion: 3 \u2014 cred: googleDriveOAuth2Api
878
- n8n-nodes-base.googleCalendar typeVersion: 1.3 \u2014 cred: googleCalendarOAuth2Api
879
-
880
- ### Productivity:
881
- n8n-nodes-base.notion typeVersion: 2.2 \u2014 cred: notionApi
882
- n8n-nodes-base.airtable typeVersion: 2.1 \u2014 cred: airtableTokenApi
883
- n8n-nodes-base.github typeVersion: 1.1 \u2014 cred: githubApi
884
- n8n-nodes-base.jira typeVersion: 1 \u2014 cred: jiraSoftwareCloudApi
885
- n8n-nodes-base.hubspot typeVersion: 2.1 \u2014 cred: hubspotOAuth2Api
886
-
887
- ### Databases:
888
- n8n-nodes-base.postgres typeVersion: 2.5 \u2014 cred: postgres
889
- n8n-nodes-base.mySql typeVersion: 2.4 \u2014 cred: mySql
890
- n8n-nodes-base.redis typeVersion: 1 \u2014 cred: redis
891
- n8n-nodes-base.supabase typeVersion: 1 \u2014 cred: supabaseApi
892
- n8n-nodes-base.awsS3 typeVersion: 2 \u2014 cred: aws
893
-
894
- ### AI \u2014 Root nodes (sit on main data flow, receive ai_* connections as TARGETS):
895
- @n8n/n8n-nodes-langchain.agent typeVersion: 1.9 \u2014 params: promptType, text (if define), options.systemMessage
896
- @n8n/n8n-nodes-langchain.chainLlm typeVersion: 1.5
897
- @n8n/n8n-nodes-langchain.chainRetrievalQa typeVersion: 1.4
898
- @n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi \u2014 standalone node, calls OpenAI directly without sub-nodes
899
- @n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi \u2014 standalone node, calls Anthropic directly without sub-nodes
900
-
901
- ### AI \u2014 Sub-nodes (sources of ai_* connections, wire INTO root nodes above):
902
- @n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
903
- @n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
904
- @n8n/n8n-nodes-langchain.lmChatGoogleGemini typeVersion: 1 \u2014 cred: googlePalmApi \u2014 ai_languageModel
905
- @n8n/n8n-nodes-langchain.memoryBufferWindow typeVersion: 1.3 \u2014 \u2014 ai_memory
906
- @n8n/n8n-nodes-langchain.toolWorkflow typeVersion: 2 \u2014 \u2014 ai_tool
907
- @n8n/n8n-nodes-langchain.toolCode typeVersion: 1.1 \u2014 \u2014 ai_tool
908
- @n8n/n8n-nodes-langchain.toolHttpRequest typeVersion: 1.1 \u2014 \u2014 ai_tool
909
- @n8n/n8n-nodes-langchain.toolCalculator typeVersion: 1 \u2014 \u2014 ai_tool
910
-
911
- ### Resource locator (__rl) format (Google / Slack / Notion modern nodes):
912
- { "__rl": true, "mode": "id", "value": "ACTUAL_ID" }
913
- { "__rl": true, "mode": "name", "value": "#channel-name" }
914
-
915
- ### App node parameter pattern:
916
- { "resource": "message", "operation": "send", ...operation-specific fields }
917
-
918
- ### Schedule Trigger \u2014 daily at 9am example:
919
- { "rule": { "interval": [{ "field": "days", "daysInterval": 1, "triggerAtHour": 9, "triggerAtMinute": 0 }] } }
920
- Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] } }
921
-
922
- ---
923
-
924
- ## PRE-DELIVERY SELF-CHECK (do this before calling the tool):
925
- 1. Every connection source/target name exists in nodes array
926
- 2. No duplicate node names
927
- 3. No duplicate node IDs
928
- 4. No forbidden fields at the workflow root
929
- 5. At least one trigger node present
930
- 6. Every AI Agent has an ai_languageModel sub-node
931
- 7. settings block is complete with executionOrder: "v1"
932
-
933
- ---
934
-
935
- Respond ONLY with a generate_workflow tool call. No prose. No markdown outside the tool call.
936
- If the request is impossible or unclear, set the error field instead of generating a workflow.`;
937
-
938
- // src/utils/thresholds.ts
939
- var DIRECT_THRESHOLD = 0.92;
940
- var REFERENCE_THRESHOLD = 0.72;
941
- function scoreToMode(score) {
942
- if (score >= DIRECT_THRESHOLD) return "direct";
943
- if (score >= REFERENCE_THRESHOLD) return "reference";
944
- return "scratch";
945
- }
946
-
947
- // src/generation/prompt-builder.ts
948
- var RULE_REMEDIES = {
949
- 1: "Provide a non-empty workflow name string",
950
- 2: "Include at least one node in the nodes array",
951
- 3: "Every node must have a unique UUID v4 string as its id field",
952
- 4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
953
- 5: "Every node must have a non-empty type string",
954
- 6: "Every node must have a positive integer typeVersion",
955
- 7: "Every node must have a position array of exactly [x, y] numbers",
956
- 8: "Every node must have a non-empty name string",
957
- 9: "connections must be a plain object (use {} if no connections)",
958
- 10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
959
- 12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
960
- 14: "Include at least one trigger node (e.g. webhook, scheduleTrigger, manualTrigger)",
961
- 15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
962
- 16: "All node names must be unique within the workflow",
963
- 17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
964
- 18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
965
- 19: "Use known safe typeVersion values for each node type",
966
- 20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
967
- 21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
968
- 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
969
886
  };
970
- var PromptBuilder = class {
971
- build(request, matches, globalFailureRates = [], dynamicCatalog) {
972
- const mode = this.resolveMode(matches);
973
- const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
974
- const userMessage = this.buildUserMessage(request, matches, mode);
975
- return { system, userMessage, mode, matches };
976
- }
977
- buildCorrectionMessage(request, matches, allIssues, attempt) {
978
- const base = this.buildUserMessage(request, matches, this.resolveMode(matches));
979
- return `${base}
980
887
 
981
- IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
982
- ${allIssues.join("\n")}
983
-
984
- Fix ALL of the above issues in your new response. Do not repeat any of these mistakes.`;
985
- }
986
- resolveMode(matches) {
987
- if (matches.length === 0) return "scratch";
988
- const top = matches[0];
989
- if (!top) return "scratch";
990
- return scoreToMode(top.score);
991
- }
992
- buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog) {
993
- let basePrompt = SYSTEM_PROMPT_V1;
994
- if (dynamicCatalog) {
995
- basePrompt = basePrompt.replace(
996
- /## NODE CATALOG — exact type strings and safe typeVersions[\s\S]*?(?=## PRE-DELIVERY SELF-CHECK)/,
997
- dynamicCatalog + "\n\n"
998
- );
999
- }
1000
- const blocks = [
1001
- {
1002
- type: "text",
1003
- text: basePrompt,
1004
- cache_control: { type: "ephemeral" }
1005
- }
1006
- ];
1007
- if (mode === "reference" && matches.length > 0) {
1008
- const refText = matches.slice(0, 3).map((m) => {
1009
- const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1010
- return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1011
- Nodes:
1012
- ${nodes}`;
1013
- }).join("\n\n");
1014
- blocks.push({
1015
- type: "text",
1016
- text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1017
-
1018
- ${refText}`
1019
- });
1020
- }
1021
- if (mode === "direct" && matches[0]) {
1022
- const match = matches[0];
1023
- const json = JSON.stringify(match.workflow.workflow, null, 2);
1024
- if (json.length > 3e4) {
1025
- const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1026
- blocks.push({
1027
- type: "text",
1028
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1029
- Nodes:
1030
- ${nodes}`
1031
- });
1032
- } else {
1033
- blocks.push({
1034
- type: "text",
1035
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
888
+ // src/telemetry/reader.ts
889
+ import { homedir as homedir2 } from "os";
890
+ import { join as join3 } from "path";
1036
891
 
1037
- ${json}`
1038
- });
1039
- }
1040
- }
1041
- if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1042
- const hint = matches[0];
1043
- const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1044
- blocks.push({
1045
- type: "text",
1046
- text: `## Weak Structural Hint
1047
- A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
892
+ // src/telemetry/event-reader.ts
893
+ import { readdir } from "fs/promises";
894
+ import { createReadStream } from "fs";
895
+ import { join as join2 } from "path";
896
+ import { createInterface } from "readline";
897
+ async function readTelemetryEvents(dir, days) {
898
+ let files;
899
+ try {
900
+ files = await readdir(dir);
901
+ } catch {
902
+ return [];
903
+ }
904
+ const cutoff = /* @__PURE__ */ new Date();
905
+ cutoff.setDate(cutoff.getDate() - days);
906
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
907
+ const todayStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
908
+ const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
909
+ const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr && f <= `${todayStr}.jsonl`).sort();
910
+ const events = [];
911
+ for (const file of recentFiles) {
912
+ const fileDate = file.replace(".jsonl", "");
913
+ try {
914
+ const rl = createInterface({
915
+ input: createReadStream(join2(dir, file), "utf-8"),
916
+ crlfDelay: Infinity
1048
917
  });
1049
- }
1050
- const warnings = this.buildFailureWarnings(matches, globalFailureRates);
1051
- if (warnings) {
1052
- blocks.push({ type: "text", text: warnings });
1053
- }
1054
- return blocks;
1055
- }
1056
- buildFailureWarnings(matches, globalFailureRates) {
1057
- const lines = [];
1058
- for (const match of matches) {
1059
- const patterns = match.workflow.failurePatterns;
1060
- if (!patterns?.length) continue;
1061
- for (const fp of patterns) {
1062
- const remedy = RULE_REMEDIES[fp.rule];
1063
- const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1064
- lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
918
+ for await (const line of rl) {
919
+ if (!line.trim()) continue;
920
+ try {
921
+ events.push({ ...JSON.parse(line), fileDate });
922
+ } catch {
923
+ }
1065
924
  }
925
+ } catch {
1066
926
  }
1067
- const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
1068
- for (const rule of highFreqRules) {
1069
- const remedy = RULE_REMEDIES[rule.rule];
1070
- const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1071
- lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
1072
- }
1073
- if (lines.length === 0) return null;
1074
- const unique = [...new Set(lines)];
1075
- return `## Known Failure Patterns \u2014 AVOID THESE
1076
-
1077
- Previous builds frequently failed the following validation rules. Ensure your output does NOT repeat these mistakes:
1078
- ${unique.join("\n")}`;
1079
927
  }
1080
- buildUserMessage(request, _matches, _mode) {
1081
- const namePart = request.name ? `
1082
- Workflow name: "${request.name}"` : "";
1083
- return `Build a workflow that: ${request.description}${namePart}`;
1084
- }
1085
- };
928
+ return events;
929
+ }
1086
930
 
1087
931
  // src/telemetry/reader.ts
1088
- import { readFile, readdir } from "fs/promises";
1089
- import { join } from "path";
1090
- import { homedir } from "os";
1091
932
  var TelemetryReader = class {
1092
933
  dir;
1093
934
  cache = null;
1094
935
  cacheTime = 0;
1095
936
  constructor(dir) {
1096
- this.dir = dir ?? join(homedir(), ".kairos", "telemetry");
937
+ this.dir = dir ?? join3(homedir2(), ".kairos", "telemetry");
1097
938
  }
1098
939
  async getFailureRates(days = 30) {
1099
940
  const now = Date.now();
@@ -1102,9 +943,10 @@ var TelemetryReader = class {
1102
943
  }
1103
944
  const events = await this.readRecentEvents(days);
1104
945
  const buildSessions = new Set(
1105
- events.filter((e) => e.eventType === "build_complete" && !e.data.dryRun).map((e) => e.sessionId)
946
+ events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
1106
947
  );
1107
- if (buildSessions.size === 0) return [];
948
+ const MIN_BUILDS_FOR_RATES = 3;
949
+ if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
1108
950
  const ruleSessions = /* @__PURE__ */ new Map();
1109
951
  for (const event of events) {
1110
952
  if (event.eventType !== "generation_attempt") continue;
@@ -1142,32 +984,566 @@ var TelemetryReader = class {
1142
984
  return rates;
1143
985
  }
1144
986
  async readRecentEvents(days) {
1145
- let files;
987
+ return readTelemetryEvents(this.dir, days);
988
+ }
989
+ };
990
+
991
+ // src/telemetry/pattern-analyzer.ts
992
+ import { writeFile, readFile as fsReadFile, appendFile as appendFile2, mkdir as mkdir2, rename } from "fs/promises";
993
+ import { join as join4 } from "path";
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
1074
+ var PATTERN_SCHEMA_VERSION = 2;
1075
+ var PatternAnalyzer = class _PatternAnalyzer {
1076
+ telemetryDir;
1077
+ outputDir;
1078
+ _cachedEvents = null;
1079
+ constructor(telemetryDir) {
1080
+ const defaultDir = join4(homedir3(), ".kairos", "telemetry");
1081
+ this.telemetryDir = telemetryDir ?? defaultDir;
1082
+ this.outputDir = telemetryDir ? join4(telemetryDir, "..") : join4(homedir3(), ".kairos");
1083
+ }
1084
+ async loadPreviousPatterns() {
1146
1085
  try {
1147
- files = await readdir(this.dir);
1086
+ const raw = await fsReadFile(join4(this.outputDir, "patterns.json"), "utf-8");
1087
+ const prev = JSON.parse(raw);
1088
+ const version = prev.schemaVersion ?? 0;
1089
+ const patterns = prev.topFailureRules ?? [];
1090
+ if (version === PATTERN_SCHEMA_VERSION) return patterns;
1091
+ return this.migratePatterns(patterns, version);
1148
1092
  } catch {
1149
1093
  return [];
1150
1094
  }
1151
- const cutoff = /* @__PURE__ */ new Date();
1152
- cutoff.setDate(cutoff.getDate() - days);
1153
- const cutoffStr = cutoff.toISOString().slice(0, 10);
1154
- const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1155
- const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr).sort();
1156
- const events = [];
1157
- for (const file of recentFiles) {
1158
- try {
1159
- const content = await readFile(join(this.dir, file), "utf-8");
1160
- for (const line of content.split("\n")) {
1161
- if (!line.trim()) continue;
1162
- try {
1163
- events.push(JSON.parse(line));
1164
- } catch {
1095
+ }
1096
+ migratePatterns(patterns, fromVersion) {
1097
+ let migrated = patterns;
1098
+ if (fromVersion < 1) {
1099
+ migrated = migrated.map((p) => ({
1100
+ ...p,
1101
+ compositeScore: p.compositeScore ?? 0,
1102
+ scoringFactors: p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
1103
+ pipelineStage: p.pipelineStage ?? "node_generation"
1104
+ }));
1105
+ }
1106
+ if (fromVersion < 2) {
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
1165
1114
  }
1115
+ };
1116
+ });
1117
+ }
1118
+ return migrated;
1119
+ }
1120
+ async analyze(days = 30) {
1121
+ const previousPatterns = await this.loadPreviousPatterns();
1122
+ const events = await this.readAllEvents(days);
1123
+ this._cachedEvents = events;
1124
+ const starts = events.filter((e) => e.eventType === "build_start");
1125
+ const attempts = events.filter((e) => e.eventType === "generation_attempt");
1126
+ const passed = attempts.filter(
1127
+ (a) => a.data.validationPassed === true
1128
+ );
1129
+ const failed = attempts.filter(
1130
+ (a) => a.data.validationPassed === false
1131
+ );
1132
+ const ruleFailures = /* @__PURE__ */ new Map();
1133
+ const credentialFailures = /* @__PURE__ */ new Map();
1134
+ for (const a of failed) {
1135
+ const weight = this.recencyWeight(a.fileDate);
1136
+ const buildId = a.runId ?? a.sessionId;
1137
+ const data = a.data;
1138
+ for (const issue of data.issues ?? []) {
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() };
1141
+ entry.count++;
1142
+ entry.sessions.add(buildId);
1143
+ entry.recencyWeights.push(weight);
1144
+ entry.allMessages.push(issue.message);
1145
+ if (data.workflowType) {
1146
+ entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
1147
+ }
1148
+ ruleFailures.set(issue.rule, entry);
1149
+ if (issue.rule === 17) {
1150
+ const credPatterns = [
1151
+ /credential\s+"([^"]+)"/,
1152
+ /credentialType[:\s]+"?([^"'\s]+)"?/,
1153
+ /missing\s+credential\s+(?:for\s+)?["']?([^"'\s]+)/i
1154
+ ];
1155
+ let credType = "unknown";
1156
+ for (const re of credPatterns) {
1157
+ const m = issue.message.match(re);
1158
+ if (m?.[1]) {
1159
+ credType = m[1];
1160
+ break;
1161
+ }
1162
+ }
1163
+ credentialFailures.set(credType, (credentialFailures.get(credType) ?? 0) + 1);
1164
+ }
1165
+ }
1166
+ }
1167
+ const failedByDate = /* @__PURE__ */ new Map();
1168
+ for (const a of failed) {
1169
+ failedByDate.set(a.fileDate, (failedByDate.get(a.fileDate) ?? 0) + 1);
1170
+ }
1171
+ const sortedFailDates = [...failedByDate.entries()].sort((a, b) => a[0].localeCompare(b[0]));
1172
+ const hasTrendData = sortedFailDates.length >= 3;
1173
+ let midDate = "";
1174
+ if (hasTrendData) {
1175
+ const halfTotal = failed.length / 2;
1176
+ let cumulative = 0;
1177
+ for (const [date, count] of sortedFailDates) {
1178
+ cumulative += count;
1179
+ if (cumulative >= halfTotal) {
1180
+ midDate = date;
1181
+ break;
1166
1182
  }
1167
- } catch {
1168
1183
  }
1169
1184
  }
1170
- return events;
1185
+ const ruleTrends = /* @__PURE__ */ new Map();
1186
+ if (hasTrendData) {
1187
+ for (const a of failed) {
1188
+ const data = a.data;
1189
+ const isNewer = a.fileDate > midDate;
1190
+ for (const issue of data.issues ?? []) {
1191
+ const entry = ruleTrends.get(issue.rule) ?? { older: 0, newer: 0 };
1192
+ if (isNewer) entry.newer++;
1193
+ else entry.older++;
1194
+ ruleTrends.set(issue.rule, entry);
1195
+ }
1196
+ }
1197
+ }
1198
+ const sessions = /* @__PURE__ */ new Map();
1199
+ for (const a of attempts) {
1200
+ const buildId = a.runId ?? a.sessionId;
1201
+ const list = sessions.get(buildId) ?? [];
1202
+ list.push(a);
1203
+ sessions.set(buildId, list);
1204
+ }
1205
+ let firstTryPass = 0;
1206
+ let correctionNeeded = 0;
1207
+ let singleAttemptFail = 0;
1208
+ for (const sessionAttempts of sessions.values()) {
1209
+ const lastAttempt = sessionAttempts[sessionAttempts.length - 1];
1210
+ const lastPassed = lastAttempt.data.validationPassed === true;
1211
+ if (sessionAttempts.length === 1 && lastPassed) {
1212
+ firstTryPass++;
1213
+ } else if (sessionAttempts.length > 1 && lastPassed) {
1214
+ correctionNeeded++;
1215
+ } else {
1216
+ singleAttemptFail++;
1217
+ }
1218
+ }
1219
+ const durations = attempts.map((a) => a.data.durationMs).filter((d) => typeof d === "number" && d > 0);
1220
+ const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
1221
+ const totalInput = attempts.reduce((s, a) => s + (a.data.tokensInput ?? 0), 0);
1222
+ const totalOutput = attempts.reduce((s, a) => s + (a.data.tokensOutput ?? 0), 0);
1223
+ const totalSessions = Math.max(sessions.size, 1);
1224
+ const stickinessCount = /* @__PURE__ */ new Map();
1225
+ for (const sessionAttempts of sessions.values()) {
1226
+ if (sessionAttempts.length < 2) continue;
1227
+ for (let i = 0; i < sessionAttempts.length - 1; i++) {
1228
+ const curr = sessionAttempts[i].data;
1229
+ const next = sessionAttempts[i + 1].data;
1230
+ if (curr.validationPassed !== false || next.validationPassed !== false) continue;
1231
+ const currRules = new Set((curr.issues ?? []).map((iss) => iss.rule));
1232
+ const nextRules = new Set((next.issues ?? []).map((iss) => iss.rule));
1233
+ for (const rule of currRules) {
1234
+ if (nextRules.has(rule)) {
1235
+ stickinessCount.set(rule, (stickinessCount.get(rule) ?? 0) + 1);
1236
+ }
1237
+ }
1238
+ }
1239
+ }
1240
+ const CONFIRMED_THRESHOLD = 3;
1241
+ const BUILDS_SINCE_LAST_FAILURE_THRESHOLD = 5;
1242
+ const RESOLVED_TTL_DAYS = 90;
1243
+ const activePatterns = [...ruleFailures.entries()].map(([rule, entry]) => {
1244
+ const t = ruleTrends.get(rule) ?? { older: 0, newer: 0 };
1245
+ const rawConfidence = Math.min(entry.sessions.size / totalSessions, 1);
1246
+ const state = entry.count >= CONFIRMED_THRESHOLD ? "confirmed" : "draft";
1247
+ const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
1248
+ const stickiness = stickinessCount.get(rule) ?? 0;
1249
+ const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
1250
+ const pattern = {
1251
+ rule,
1252
+ failureCount: entry.count,
1253
+ confidence: Math.round(rawConfidence * 1e3) / 1e3,
1254
+ compositeScore,
1255
+ scoringFactors: factors,
1256
+ state,
1257
+ trend: this.classifyTrend(t.older, t.newer),
1258
+ pipelineStage: RULE_PIPELINE_STAGES[rule] ?? "node_generation",
1259
+ exampleMessages: this.deduplicateMessages(entry.allMessages),
1260
+ mitigation: RULE_MITIGATIONS[rule] ?? null
1261
+ };
1262
+ if (entry.workflowTypes.size > 0) {
1263
+ pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
1264
+ }
1265
+ return pattern;
1266
+ }).sort((a, b) => b.compositeScore - a.compositeScore);
1267
+ const activeRules = new Set(activePatterns.map((p) => p.rule));
1268
+ for (const p of activePatterns) {
1269
+ const prev = previousPatterns.find((pp) => pp.rule === p.rule);
1270
+ if (prev?.state === "resolved") {
1271
+ p.trend = "worsening";
1272
+ p.regressed = true;
1273
+ }
1274
+ }
1275
+ const ruleLastFailureDate = /* @__PURE__ */ new Map();
1276
+ for (const a of failed) {
1277
+ const data = a.data;
1278
+ for (const issue of data.issues ?? []) {
1279
+ const existing = ruleLastFailureDate.get(issue.rule);
1280
+ if (!existing || a.fileDate > existing) {
1281
+ ruleLastFailureDate.set(issue.rule, a.fileDate);
1282
+ }
1283
+ }
1284
+ }
1285
+ const newlyResolved = previousPatterns.filter((p) => {
1286
+ if (p.state !== "confirmed" || activeRules.has(p.rule)) return false;
1287
+ const lastFailDate = ruleLastFailureDate.get(p.rule) ?? "";
1288
+ const buildsSince = starts.filter((s) => s.fileDate > lastFailDate).length;
1289
+ return buildsSince >= BUILDS_SINCE_LAST_FAILURE_THRESHOLD;
1290
+ }).map((p) => ({
1291
+ ...p,
1292
+ state: "resolved",
1293
+ trend: "improving",
1294
+ pipelineStage: p.pipelineStage ?? RULE_PIPELINE_STAGES[p.rule] ?? "node_generation",
1295
+ confidence: 0,
1296
+ compositeScore: 0,
1297
+ scoringFactors: { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
1298
+ failureCount: 0,
1299
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
1300
+ }));
1301
+ const ttlCutoff = /* @__PURE__ */ new Date();
1302
+ ttlCutoff.setDate(ttlCutoff.getDate() - RESOLVED_TTL_DAYS);
1303
+ const ttlCutoffStr = ttlCutoff.toISOString();
1304
+ const carriedResolved = previousPatterns.filter((p) => p.state === "resolved" && !activeRules.has(p.rule) && (!p.resolvedAt || p.resolvedAt >= ttlCutoffStr)).map((p) => ({ ...p }));
1305
+ const newlyResolvedRules = new Set(newlyResolved.map((p) => p.rule));
1306
+ const pendingResolution = previousPatterns.filter((p) => p.state === "confirmed" && !activeRules.has(p.rule) && !newlyResolvedRules.has(p.rule)).map((p) => ({ ...p }));
1307
+ const deduped = [
1308
+ ...newlyResolved,
1309
+ ...carriedResolved.filter((p) => !newlyResolvedRules.has(p.rule)),
1310
+ ...pendingResolution
1311
+ ];
1312
+ const patterns = [...activePatterns, ...deduped];
1313
+ const credTypes = [...credentialFailures.entries()].sort((a, b) => b[1] - a[1]).map(([type, count]) => ({ type, count }));
1314
+ const drift = this.detectDrift(patterns);
1315
+ const warnEffMap = /* @__PURE__ */ new Map();
1316
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
1317
+ for (const bc of buildCompletes) {
1318
+ const bcData = bc.data;
1319
+ const warned = bcData.warnedRules ?? [];
1320
+ if (warned.length === 0) continue;
1321
+ const sessionFailedRules = /* @__PURE__ */ new Set();
1322
+ const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
1323
+ for (const a of sessionAttempts) {
1324
+ const ad = a.data;
1325
+ if (ad.validationPassed === false) {
1326
+ for (const issue of ad.issues ?? []) {
1327
+ sessionFailedRules.add(issue.rule);
1328
+ }
1329
+ }
1330
+ }
1331
+ for (const rule of warned) {
1332
+ const entry = warnEffMap.get(rule) ?? { warned: 0, passed: 0, failed: 0 };
1333
+ entry.warned++;
1334
+ if (sessionFailedRules.has(rule)) entry.failed++;
1335
+ else entry.passed++;
1336
+ warnEffMap.set(rule, entry);
1337
+ }
1338
+ }
1339
+ const warningEffectiveness = [...warnEffMap.entries()].map(([rule, e]) => ({
1340
+ rule,
1341
+ timesWarned: e.warned,
1342
+ timesWarnedAndPassed: e.passed,
1343
+ timesWarnedAndFailed: e.failed,
1344
+ effectivenessRate: e.warned > 0 ? Math.round(e.passed / e.warned * 1e3) / 1e3 : 0
1345
+ })).sort((a, b) => b.timesWarned - a.timesWarned);
1346
+ const coOccurrenceMap = /* @__PURE__ */ new Map();
1347
+ for (const a of failed) {
1348
+ const data = a.data;
1349
+ const rules = [...new Set((data.issues ?? []).map((i) => i.rule))].sort((x, y) => x - y);
1350
+ for (let i = 0; i < rules.length; i++) {
1351
+ for (let j = i + 1; j < rules.length; j++) {
1352
+ const key = `${rules[i]},${rules[j]}`;
1353
+ coOccurrenceMap.set(key, (coOccurrenceMap.get(key) ?? 0) + 1);
1354
+ }
1355
+ }
1356
+ }
1357
+ const ruleCoOccurrence = [...coOccurrenceMap.entries()].filter(([, count]) => count >= 3).map(([key, count]) => {
1358
+ const [a, b] = key.split(",").map(Number);
1359
+ return { rules: [a, b], count };
1360
+ }).sort((a, b) => b.count - a.count);
1361
+ const attemptDistribution = {};
1362
+ for (const sessionAttempts of sessions.values()) {
1363
+ const depth = sessionAttempts.length;
1364
+ attemptDistribution[depth] = (attemptDistribution[depth] ?? 0) + 1;
1365
+ }
1366
+ return {
1367
+ schemaVersion: PATTERN_SCHEMA_VERSION,
1368
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1369
+ summary: {
1370
+ totalBuilds: starts.length,
1371
+ totalAttempts: attempts.length,
1372
+ firstTryPassRate: Math.round(firstTryPass / totalSessions * 1e3) / 1e3,
1373
+ correctionRate: Math.round(correctionNeeded / totalSessions * 1e3) / 1e3,
1374
+ singleAttemptFailRate: Math.round(singleAttemptFail / totalSessions * 1e3) / 1e3,
1375
+ avgDurationMs: Math.round(avgDuration),
1376
+ totalTokensInput: totalInput,
1377
+ totalTokensOutput: totalOutput,
1378
+ attemptDistribution
1379
+ },
1380
+ topFailureRules: patterns,
1381
+ failingCredentialTypes: credTypes,
1382
+ drift,
1383
+ warningEffectiveness,
1384
+ ruleCoOccurrence
1385
+ };
1386
+ }
1387
+ async analyzeAndSave(days = 30) {
1388
+ const analysis = await this.analyze(days);
1389
+ await mkdir2(this.outputDir, { recursive: true });
1390
+ const outputPath = join4(this.outputDir, "patterns.json");
1391
+ const tmpPath = `${outputPath}.tmp`;
1392
+ await writeFile(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
1393
+ await rename(tmpPath, outputPath);
1394
+ const historySummary = {
1395
+ timestamp: analysis.generatedAt,
1396
+ totalBuilds: analysis.summary.totalBuilds,
1397
+ firstTryPassRate: analysis.summary.firstTryPassRate,
1398
+ correctionRate: analysis.summary.correctionRate,
1399
+ singleAttemptFailRate: analysis.summary.singleAttemptFailRate,
1400
+ activePatternCount: analysis.topFailureRules.filter((p) => p.state !== "resolved").length,
1401
+ topRules: analysis.topFailureRules.filter((p) => p.state !== "resolved").slice(0, 5).map((p) => ({ rule: p.rule, compositeScore: p.compositeScore, state: p.state }))
1402
+ };
1403
+ const historyPath = join4(this.outputDir, "pattern-history.jsonl");
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);
1410
+ return analysis;
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
+ }
1454
+ async getHistory(limit = 20) {
1455
+ try {
1456
+ const raw = await fsReadFile(join4(this.outputDir, "pattern-history.jsonl"), "utf-8");
1457
+ return raw.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)).slice(-limit);
1458
+ } catch {
1459
+ return [];
1460
+ }
1461
+ }
1462
+ static fromEnv() {
1463
+ const dir = process.env["KAIROS_TELEMETRY"];
1464
+ return dir && dir !== "true" && dir !== "false" ? new _PatternAnalyzer(dir) : new _PatternAnalyzer();
1465
+ }
1466
+ detectDrift(patterns) {
1467
+ const VALIDATOR_RULES = VALIDATOR_RULE_IDS;
1468
+ const validatorRuleSet = new Set(VALIDATOR_RULES);
1469
+ const alerts = [];
1470
+ for (const p of patterns) {
1471
+ if (p.state !== "resolved" && !validatorRuleSet.has(p.rule)) {
1472
+ alerts.push({
1473
+ type: "stale_pattern",
1474
+ rule: p.rule,
1475
+ message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-26)`
1476
+ });
1477
+ }
1478
+ }
1479
+ for (const rule of VALIDATOR_RULES) {
1480
+ if (!(rule in RULE_MITIGATIONS)) {
1481
+ alerts.push({
1482
+ type: "missing_mitigation",
1483
+ rule,
1484
+ message: `Rule ${rule} has no mitigation text \u2014 if it fails, the system can't advise the LLM how to fix it`
1485
+ });
1486
+ }
1487
+ if (!(rule in RULE_PIPELINE_STAGES)) {
1488
+ alerts.push({
1489
+ type: "missing_stage_mapping",
1490
+ rule,
1491
+ message: `Rule ${rule} has no pipeline stage mapping \u2014 failures won't be grouped correctly`
1492
+ });
1493
+ }
1494
+ }
1495
+ const coveredRules = VALIDATOR_RULES.filter((r) => r in RULE_MITIGATIONS && r in RULE_PIPELINE_STAGES).length;
1496
+ return {
1497
+ healthy: alerts.length === 0,
1498
+ alerts,
1499
+ coveredRules,
1500
+ totalRules: VALIDATOR_RULES.length
1501
+ };
1502
+ }
1503
+ computeCompositeScore(rawConfidence, sampleSize, state, avgRecency, stickiness) {
1504
+ const stateWeights = { draft: 0.3, confirmed: 0.8, resolved: 0.1 };
1505
+ const stateWeight = stateWeights[state];
1506
+ const impact = (1 - Math.exp(-sampleSize / 5)) * stateWeight;
1507
+ const stickinessBoost = Math.min(0.15, stickiness * 0.05);
1508
+ const compositeScore = Math.min(Math.round(rawConfidence * impact * avgRecency * (1 + stickinessBoost) * 1e3) / 1e3, 1);
1509
+ return {
1510
+ compositeScore,
1511
+ factors: {
1512
+ rawConfidence: Math.round(rawConfidence * 1e3) / 1e3,
1513
+ impact: Math.round(impact * 1e3) / 1e3,
1514
+ recency: Math.round(avgRecency * 1e3) / 1e3,
1515
+ stickinessBoost: Math.round(stickinessBoost * 1e3) / 1e3
1516
+ }
1517
+ };
1518
+ }
1519
+ classifyTrend(older, newer) {
1520
+ const total = older + newer;
1521
+ if (total === 0) return "stable";
1522
+ if (older === 0) return "new";
1523
+ const newerRatio = newer / total;
1524
+ if (newerRatio >= 0.65) return "worsening";
1525
+ if (newerRatio <= 0.35) return "improving";
1526
+ return "stable";
1527
+ }
1528
+ deduplicateMessages(messages, maxCount = 3) {
1529
+ const normalize = (msg) => msg.replace(/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/gi, "...").replace(/\bnode\s+"[^"]+"/g, 'node "..."').replace(/\s+/g, " ").trim();
1530
+ const seen = /* @__PURE__ */ new Set();
1531
+ const unique = [];
1532
+ for (const msg of messages) {
1533
+ const key = normalize(msg);
1534
+ if (!seen.has(key) && unique.length < maxCount) {
1535
+ seen.add(key);
1536
+ unique.push(msg);
1537
+ }
1538
+ }
1539
+ return unique;
1540
+ }
1541
+ recencyWeight(fileDate, halfLifeDays = 30) {
1542
+ const daysAgo = Math.max(0, (Date.now() - (/* @__PURE__ */ new Date(fileDate + "T12:00:00Z")).getTime()) / (1e3 * 60 * 60 * 24));
1543
+ return Math.max(0.1, Math.exp(-Math.LN2 * daysAgo / halfLifeDays));
1544
+ }
1545
+ async readAllEvents(days) {
1546
+ return readTelemetryEvents(this.telemetryDir, days);
1171
1547
  }
1172
1548
  };
1173
1549
 
@@ -1399,13 +1775,17 @@ function rerank(candidates, clusters) {
1399
1775
  }
1400
1776
 
1401
1777
  // src/library/file-library.ts
1402
- import { readFile as readFile2, writeFile, rename, mkdir } from "fs/promises";
1403
- import { join as join2 } from "path";
1404
- import { homedir as homedir2 } from "os";
1778
+ import { readFile, writeFile as writeFile2, rename as rename2, mkdir as mkdir3, stat } from "fs/promises";
1779
+ import { join as join5 } from "path";
1780
+ import { homedir as homedir4 } from "os";
1405
1781
 
1406
- // src/utils/uuid.ts
1407
- function generateUUID() {
1408
- 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";
1409
1789
  }
1410
1790
 
1411
1791
  // src/library/file-library.ts
@@ -1421,13 +1801,27 @@ function buildSearchCorpus(w) {
1421
1801
  return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
1422
1802
  }
1423
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
+ }
1424
1812
  var FileLibrary = class {
1425
1813
  dir;
1426
- workflows = [];
1814
+ meta = [];
1427
1815
  initPromise = null;
1428
1816
  writeQueue = Promise.resolve();
1429
1817
  constructor(dir) {
1430
- this.dir = dir ?? join2(homedir2(), ".kairos", "library");
1818
+ this.dir = dir ?? join5(homedir4(), ".kairos", "library");
1819
+ }
1820
+ get workflowsDir() {
1821
+ return join5(this.dir, "workflows");
1822
+ }
1823
+ workflowFilePath(id) {
1824
+ return join5(this.workflowsDir, `${id}.json`);
1431
1825
  }
1432
1826
  async initialize() {
1433
1827
  if (!this.initPromise) {
@@ -1436,62 +1830,149 @@ var FileLibrary = class {
1436
1830
  return this.initPromise;
1437
1831
  }
1438
1832
  async doInitialize() {
1439
- await mkdir(this.dir, { recursive: true });
1440
- const indexPath = join2(this.dir, "index.json");
1833
+ await mkdir3(this.dir, { recursive: true });
1834
+ const indexPath = join5(this.dir, "index.json");
1835
+ let workflowsDirExists = false;
1441
1836
  try {
1442
- const raw = await readFile2(indexPath, "utf-8");
1443
- const parsed = JSON.parse(raw);
1444
- if (!Array.isArray(parsed)) {
1445
- this.workflows = [];
1446
- } else {
1447
- this.workflows = parsed.filter(
1448
- (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)
1449
- );
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 = [];
1450
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);
1451
1892
  } catch {
1452
- this.workflows = [];
1893
+ return null;
1453
1894
  }
1454
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
+ }
1455
1924
  async search(description, options) {
1456
- const searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
1457
- if (searchable.length === 0) return [];
1925
+ const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
1926
+ if (filteredMeta.length === 0) return [];
1458
1927
  const limit = options?.limit ?? 3;
1459
1928
  const queryTokens = tokenize(description);
1460
1929
  if (queryTokens.length === 0) return [];
1461
- 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)));
1462
1932
  const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
1463
- const docCount = searchable.length;
1933
+ const docCount = shells.length;
1464
1934
  const idf = /* @__PURE__ */ new Map();
1465
1935
  const allTokens = new Set(queryTokens);
1466
1936
  for (const token of allTokens) {
1467
1937
  const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
1468
1938
  idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
1469
1939
  }
1470
- const scored = hybridScore(queryTokens, description, searchable, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
1471
- 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);
1472
1942
  const reranked = rerank(scored, clusters).slice(0, limit);
1473
- const results = reranked.map((m) => {
1474
- return { workflow: m.workflow, score: m.score, mode: scoreToMode(m.score) };
1475
- });
1476
- if (results.length > 0) {
1477
- for (const r of results) {
1478
- r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
1479
- }
1480
- this.persist();
1481
- }
1482
- 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);
1483
1962
  }
1484
1963
  async save(workflow, metadata) {
1485
1964
  const id = generateUUID();
1965
+ await this.writeWorkflowFile(id, workflow);
1486
1966
  const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
1487
- const stored = {
1967
+ const meta = {
1488
1968
  id,
1489
- workflow,
1490
1969
  description: metadata.description,
1491
1970
  tags: metadata.tags ?? [],
1492
1971
  platform: metadata.platform ?? "n8n",
1493
1972
  deployCount: 0,
1494
1973
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1974
+ workflowName: workflow.name,
1975
+ cachedNodeTypes: workflow.nodes.map((n) => n.type),
1495
1976
  ...failurePatterns?.length ? { failurePatterns } : {},
1496
1977
  ...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
1497
1978
  ...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
@@ -1503,31 +1984,35 @@ var FileLibrary = class {
1503
1984
  ...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
1504
1985
  ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
1505
1986
  };
1506
- this.workflows.push(stored);
1507
- if (this.workflows.length > MAX_LIBRARY_SIZE) {
1508
- this.workflows.sort((a, b) => (b.deployCount ?? 1) - (a.deployCount ?? 1));
1509
- 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);
1510
1995
  }
1511
1996
  await this.persist();
1512
1997
  return id;
1513
1998
  }
1514
1999
  async recordDeployment(id) {
1515
- const w = this.workflows.find((w2) => w2.id === id);
1516
- if (w) {
1517
- w.deployCount++;
1518
- 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();
1519
2004
  await this.persist();
1520
2005
  }
1521
2006
  }
1522
2007
  async recordOutcome(id, outcome) {
1523
- const w = this.workflows.find((w2) => w2.id === id);
1524
- if (!w) return;
2008
+ const m = this.meta.find((m2) => m2.id === id);
2009
+ if (!m) return;
1525
2010
  if (outcome.mode === "direct") {
1526
- w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
2011
+ m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
1527
2012
  } else {
1528
- w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
2013
+ m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
1529
2014
  }
1530
- const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
2015
+ const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
1531
2016
  stats.totalUses++;
1532
2017
  stats.totalAttempts += outcome.attempts;
1533
2018
  if (outcome.firstTryPass) stats.firstTryPasses++;
@@ -1535,24 +2020,35 @@ var FileLibrary = class {
1535
2020
  const key = String(rule);
1536
2021
  stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
1537
2022
  }
1538
- w.outcomeStats = stats;
2023
+ m.outcomeStats = stats;
1539
2024
  await this.persist();
1540
2025
  }
1541
2026
  async drain() {
1542
2027
  await this.writeQueue;
1543
2028
  }
1544
2029
  async get(id) {
1545
- 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 };
1546
2035
  }
1547
2036
  async list(filters) {
1548
- let result = this.workflows;
2037
+ let filtered = this.meta;
1549
2038
  if (filters?.platform) {
1550
- result = result.filter((w) => w.platform === filters.platform);
2039
+ filtered = filtered.filter((m) => m.platform === filters.platform);
1551
2040
  }
1552
2041
  if (filters?.tags && filters.tags.length > 0) {
1553
- result = result.filter((w) => filters.tags.some((t) => w.tags.includes(t)));
1554
- }
1555
- 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);
1556
2052
  }
1557
2053
  deduplicateFailurePatterns(patterns) {
1558
2054
  if (!patterns?.length) return void 0;
@@ -1567,12 +2063,37 @@ var FileLibrary = class {
1567
2063
  }
1568
2064
  return [...map.values()];
1569
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
+ }
1570
2075
  persist() {
1571
2076
  this.writeQueue = this.writeQueue.then(async () => {
1572
- const indexPath = join2(this.dir, "index.json");
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
+ }
1573
2094
  const tmpPath = `${indexPath}.tmp`;
1574
- await writeFile(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
1575
- await rename(tmpPath, indexPath);
2095
+ await writeFile2(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
2096
+ await rename2(tmpPath, indexPath);
1576
2097
  });
1577
2098
  return this.writeQueue;
1578
2099
  }
@@ -1589,8 +2110,11 @@ export {
1589
2110
  NodeRegistry,
1590
2111
  N8nValidator,
1591
2112
  scoreToMode,
1592
- PromptBuilder,
2113
+ RULE_EXAMPLES,
2114
+ RULE_MITIGATIONS,
2115
+ TelemetryCollector,
1593
2116
  TelemetryReader,
2117
+ PatternAnalyzer,
1594
2118
  nullLogger,
1595
2119
  hybridScore,
1596
2120
  clusterWorkflows,
@@ -1599,4 +2123,4 @@ export {
1599
2123
  buildSearchCorpus,
1600
2124
  FileLibrary
1601
2125
  };
1602
- //# sourceMappingURL=chunk-RYGYNOR6.js.map
2126
+ //# sourceMappingURL=chunk-6IXW3WCC.js.map