@kairos-sdk/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -46,8 +46,15 @@ __export(index_exports, {
46
46
  ProviderError: () => ProviderError,
47
47
  ResponseParseError: () => ResponseParseError,
48
48
  TelemetryCollector: () => TelemetryCollector,
49
+ TelemetryReader: () => TelemetryReader,
50
+ TemplateSyncer: () => TemplateSyncer,
49
51
  ValidationError: () => ValidationError,
50
- nullLogger: () => nullLogger
52
+ buildSearchCorpus: () => buildSearchCorpus,
53
+ clusterWorkflows: () => clusterWorkflows,
54
+ hybridScore: () => hybridScore,
55
+ nullLogger: () => nullLogger,
56
+ rerank: () => rerank,
57
+ tokenize: () => tokenize
51
58
  });
52
59
  module.exports = __toCommonJS(index_exports);
53
60
 
@@ -71,6 +78,8 @@ var NullLibrary = class {
71
78
  }
72
79
  async recordDeployment(_id) {
73
80
  }
81
+ async recordOutcome(_id, _outcome) {
82
+ }
74
83
  async get(_id) {
75
84
  return null;
76
85
  }
@@ -85,6 +94,9 @@ var KairosError = class extends Error {
85
94
  super(message);
86
95
  this.cause = cause;
87
96
  this.name = "KairosError";
97
+ if (Error.captureStackTrace) {
98
+ Error.captureStackTrace(this, this.constructor);
99
+ }
88
100
  }
89
101
  cause;
90
102
  };
@@ -107,8 +119,34 @@ var ProviderError = class extends KairosError {
107
119
  }
108
120
  };
109
121
 
122
+ // src/utils/retry.ts
123
+ async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
124
+ let lastError;
125
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
126
+ if (attempt > 0) {
127
+ const jitter = Math.random() * delayMs * 0.5;
128
+ await new Promise((resolve) => setTimeout(resolve, delayMs * 2 ** (attempt - 1) + jitter));
129
+ }
130
+ try {
131
+ return await fn();
132
+ } catch (err) {
133
+ lastError = err;
134
+ if (shouldRetry && !shouldRetry(err)) throw err;
135
+ }
136
+ }
137
+ throw lastError;
138
+ }
139
+ function fetchWithTimeout(url, init, timeoutMs) {
140
+ const controller = new AbortController();
141
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
142
+ return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
143
+ }
144
+
110
145
  // src/providers/n8n/api-client.ts
111
146
  var EXECUTION_LIMIT_CAP = 100;
147
+ var REQUEST_TIMEOUT_MS = 3e4;
148
+ var RETRY_ATTEMPTS = 3;
149
+ var RETRY_DELAY_MS = 1e3;
112
150
  var N8nApiClient = class {
113
151
  constructor(baseUrl, apiKey, logger) {
114
152
  this.baseUrl = baseUrl;
@@ -121,9 +159,21 @@ var N8nApiClient = class {
121
159
  async request(method, path, body) {
122
160
  const url = `${this.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
123
161
  this.logger.debug(`n8n ${method} ${path}`);
162
+ const isSafe = method === "GET";
163
+ if (!isSafe) {
164
+ return this.singleRequest(url, method, path, body);
165
+ }
166
+ return withRetry(
167
+ () => this.singleRequest(url, method, path, body),
168
+ RETRY_ATTEMPTS,
169
+ RETRY_DELAY_MS,
170
+ (err) => err instanceof ProviderError || err instanceof ApiError && err.statusCode === 429
171
+ );
172
+ }
173
+ async singleRequest(url, method, path, body) {
124
174
  let response;
125
175
  try {
126
- response = await fetch(url, {
176
+ response = await fetchWithTimeout(url, {
127
177
  method,
128
178
  headers: {
129
179
  "X-N8N-API-KEY": this.apiKey,
@@ -131,7 +181,7 @@ var N8nApiClient = class {
131
181
  Accept: "application/json"
132
182
  },
133
183
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
134
- });
184
+ }, REQUEST_TIMEOUT_MS);
135
185
  } catch (err) {
136
186
  throw new ProviderError(`Network error calling n8n API: ${path}`, err);
137
187
  }
@@ -165,15 +215,24 @@ var N8nApiClient = class {
165
215
  return this.request("GET", `/workflows/${id}`);
166
216
  }
167
217
  async listWorkflows() {
168
- const response = await this.request("GET", "/workflows?limit=250");
169
- return response.data.map((w) => ({
170
- id: w.id,
171
- name: w.name,
172
- active: w.active,
173
- createdAt: w.createdAt,
174
- updatedAt: w.updatedAt,
175
- ...w.tags !== void 0 ? { tags: w.tags } : {}
176
- }));
218
+ const all = [];
219
+ let path = "/workflows?limit=250";
220
+ for (; ; ) {
221
+ const response = await this.request("GET", path);
222
+ for (const w of response.data) {
223
+ all.push({
224
+ id: w.id,
225
+ name: w.name,
226
+ active: w.active,
227
+ createdAt: w.createdAt,
228
+ updatedAt: w.updatedAt,
229
+ ...w.tags !== void 0 ? { tags: w.tags } : {}
230
+ });
231
+ }
232
+ if (!response.nextCursor) break;
233
+ path = `/workflows?limit=250&cursor=${response.nextCursor}`;
234
+ }
235
+ return all;
177
236
  }
178
237
  async deleteWorkflow(id) {
179
238
  await this.request("DELETE", `/workflows/${id}`);
@@ -200,8 +259,17 @@ var N8nApiClient = class {
200
259
  return { ...this.mapExecution(response), data: response.data, workflowData: response.workflowData };
201
260
  }
202
261
  async listTags() {
203
- const response = await this.request("GET", "/tags");
204
- return response.data.map((t) => ({ id: t.id, name: t.name }));
262
+ const all = [];
263
+ let path = "/tags?limit=250";
264
+ for (; ; ) {
265
+ const response = await this.request("GET", path);
266
+ for (const t of response.data) {
267
+ all.push({ id: t.id, name: t.name });
268
+ }
269
+ if (!response.nextCursor) break;
270
+ path = `/tags?limit=250&cursor=${response.nextCursor}`;
271
+ }
272
+ return all;
205
273
  }
206
274
  async createTag(name) {
207
275
  const response = await this.request("POST", "/tags", { name });
@@ -240,7 +308,8 @@ var FORBIDDEN_ON_CREATE = [
240
308
  "active",
241
309
  "pinData",
242
310
  "triggerCount",
243
- "shared"
311
+ "shared",
312
+ "staticData"
244
313
  ];
245
314
  var FORBIDDEN_ON_UPDATE = FORBIDDEN_ON_CREATE.filter((f) => f !== "id");
246
315
 
@@ -399,6 +468,8 @@ var DEFAULT_REGISTRY = [
399
468
  { type: "@n8n/n8n-nodes-langchain.agent", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9], requiredParams: [] },
400
469
  { type: "@n8n/n8n-nodes-langchain.chainLlm", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5], requiredParams: [] },
401
470
  { type: "@n8n/n8n-nodes-langchain.chainRetrievalQa", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4], requiredParams: [] },
471
+ { type: "@n8n/n8n-nodes-langchain.openAi", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8], requiredParams: [], credentialType: "openAiApi" },
472
+ { type: "@n8n/n8n-nodes-langchain.anthropic", safeTypeVersions: [1], requiredParams: [], credentialType: "anthropicApi" },
402
473
  { type: "@n8n/n8n-nodes-langchain.informationExtractor", safeTypeVersions: [1], requiredParams: [] },
403
474
  { type: "@n8n/n8n-nodes-langchain.textClassifier", safeTypeVersions: [1], requiredParams: [] },
404
475
  // AI / LangChain sub-nodes (models)
@@ -409,7 +480,8 @@ var DEFAULT_REGISTRY = [
409
480
  { type: "@n8n/n8n-nodes-langchain.memoryBufferWindow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
410
481
  { type: "@n8n/n8n-nodes-langchain.toolWorkflow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
411
482
  { type: "@n8n/n8n-nodes-langchain.toolCode", safeTypeVersions: [1, 1.1], requiredParams: [] },
412
- { type: "@n8n/n8n-nodes-langchain.toolHttpRequest", safeTypeVersions: [1, 1.1], requiredParams: [] }
483
+ { type: "@n8n/n8n-nodes-langchain.toolHttpRequest", safeTypeVersions: [1, 1.1], requiredParams: [] },
484
+ { type: "@n8n/n8n-nodes-langchain.toolCalculator", safeTypeVersions: [1], requiredParams: [] }
413
485
  ];
414
486
  var NodeRegistry = class {
415
487
  byType;
@@ -422,11 +494,17 @@ var NodeRegistry = class {
422
494
  isTrigger(type) {
423
495
  return this.byType.get(type)?.isTrigger === true;
424
496
  }
497
+ isKnown(type) {
498
+ return this.byType.has(type);
499
+ }
425
500
  isVersionSafe(type, version) {
426
501
  const def = this.byType.get(type);
427
502
  if (!def) return true;
428
503
  return def.safeTypeVersions.includes(version);
429
504
  }
505
+ getRequiredParams(type) {
506
+ return this.byType.get(type)?.requiredParams ?? [];
507
+ }
430
508
  };
431
509
 
432
510
  // src/validation/validator.ts
@@ -442,7 +520,7 @@ var AI_CONNECTION_TYPES = [
442
520
  "ai_vectorStore"
443
521
  ];
444
522
  var TRIGGER_TYPE_PATTERNS = [/trigger/i, /Trigger$/];
445
- var NODE_TYPE_PATTERN = /^(@[a-z0-9-]+\/[a-z0-9-]+\.|n8n-nodes-[a-z0-9-]+\.)[a-zA-Z][a-zA-Z0-9]+$/;
523
+ var NODE_TYPE_PATTERN = /^(@[a-z0-9-]+\/[a-z0-9-]+\.|n8n-nodes-[a-z0-9-]+\.)[a-zA-Z][a-zA-Z0-9-]+$/;
446
524
  var N8nValidator = class {
447
525
  registry;
448
526
  constructor(registry = new NodeRegistry(DEFAULT_REGISTRY)) {
@@ -469,6 +547,10 @@ var N8nValidator = class {
469
547
  this.checkRule17(workflow, issues);
470
548
  this.checkRule18(workflow, issues);
471
549
  this.checkRule19(workflow, issues);
550
+ this.checkRule20(workflow, issues);
551
+ this.checkRule21(workflow, issues);
552
+ this.checkRule22(workflow, issues);
553
+ this.checkRule23(workflow, issues);
472
554
  const errors = issues.filter((i) => i.severity === "error");
473
555
  return { valid: errors.length === 0, issues };
474
556
  }
@@ -604,6 +686,7 @@ var N8nValidator = class {
604
686
  }
605
687
  }
606
688
  for (const node of w.nodes) {
689
+ if (node.type.includes("stickyNote")) continue;
607
690
  if (!this.isTriggerNode(node) && !reachable.has(node.name)) {
608
691
  this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
609
692
  }
@@ -673,7 +756,7 @@ var N8nValidator = class {
673
756
  }
674
757
  }
675
758
  }
676
- // Rule 18 (WARN): AI connections originate from sub-nodes, not the agent/chain root
759
+ // Rule 18 (ERROR): AI connections must originate from sub-nodes, not the agent/chain root
677
760
  checkRule18(w, issues) {
678
761
  if (typeof w.connections !== "object" || w.connections === null) return;
679
762
  const agentTypes = /* @__PURE__ */ new Set([
@@ -691,7 +774,7 @@ var N8nValidator = class {
691
774
  if (typeof outputs !== "object" || outputs === null) continue;
692
775
  for (const connType of AI_CONNECTION_TYPES) {
693
776
  if (connType in outputs) {
694
- this.warn(
777
+ this.err(
695
778
  issues,
696
779
  18,
697
780
  `Node "${sourceName}" uses AI connection type "${connType}" as a SOURCE \u2014 AI sub-nodes should be the source, not the agent/chain root`,
@@ -716,6 +799,111 @@ var N8nValidator = class {
716
799
  }
717
800
  }
718
801
  }
802
+ // Rule 20 (WARN): cycle detection — no node should be reachable from itself
803
+ // Exempts splitInBatches loops which are an intentional n8n pattern
804
+ checkRule20(w, issues) {
805
+ if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
806
+ const splitBatchNodes = new Set(
807
+ w.nodes.filter((n) => n.type.includes("splitInBatches")).map((n) => n.name)
808
+ );
809
+ const adj = /* @__PURE__ */ new Map();
810
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
811
+ if (typeof outputs !== "object" || outputs === null) continue;
812
+ const targets = [];
813
+ for (const portGroup of Object.values(outputs)) {
814
+ if (!Array.isArray(portGroup)) continue;
815
+ for (const conns of portGroup) {
816
+ if (!Array.isArray(conns)) continue;
817
+ for (const conn of conns) {
818
+ const t = conn;
819
+ if (typeof t?.node === "string") {
820
+ if (splitBatchNodes.has(t.node)) continue;
821
+ targets.push(t.node);
822
+ }
823
+ }
824
+ }
825
+ }
826
+ adj.set(sourceName, targets);
827
+ }
828
+ const WHITE = 0, GRAY = 1, BLACK = 2;
829
+ const color = /* @__PURE__ */ new Map();
830
+ for (const node of w.nodes) color.set(node.name, WHITE);
831
+ const dfs = (name) => {
832
+ color.set(name, GRAY);
833
+ for (const neighbor of adj.get(name) ?? []) {
834
+ const c = color.get(neighbor);
835
+ if (c === GRAY) return true;
836
+ if (c === WHITE && dfs(neighbor)) return true;
837
+ }
838
+ color.set(name, BLACK);
839
+ return false;
840
+ };
841
+ for (const node of w.nodes) {
842
+ if (color.get(node.name) === WHITE && dfs(node.name)) {
843
+ this.warn(issues, 20, "Workflow contains a connection cycle \u2014 this may cause infinite loops");
844
+ return;
845
+ }
846
+ }
847
+ }
848
+ // Rule 22 (WARN): check requiredParams from registry
849
+ checkRule22(w, issues) {
850
+ if (!Array.isArray(w.nodes)) return;
851
+ for (const node of w.nodes) {
852
+ if (typeof node.type !== "string") continue;
853
+ const required = this.registry.getRequiredParams(node.type);
854
+ if (required.length === 0) continue;
855
+ const params = node.parameters ?? {};
856
+ for (const param of required) {
857
+ const value = params[param];
858
+ if (value === void 0 || value === null || value === "") {
859
+ this.warn(
860
+ issues,
861
+ 22,
862
+ `Node "${node.name}" (${node.type}) is missing required parameter "${param}"`,
863
+ node.id
864
+ );
865
+ }
866
+ }
867
+ }
868
+ }
869
+ // Rule 23 (WARN): unknown node types not in registry
870
+ checkRule23(w, issues) {
871
+ if (!Array.isArray(w.nodes)) return;
872
+ for (const node of w.nodes) {
873
+ if (typeof node.type !== "string") continue;
874
+ if (node.type.includes("stickyNote")) continue;
875
+ if (!NODE_TYPE_PATTERN.test(node.type)) continue;
876
+ if (!this.registry.isKnown(node.type)) {
877
+ this.warn(
878
+ issues,
879
+ 23,
880
+ `Node "${node.name}" uses unknown type "${node.type}" \u2014 it may not exist in n8n`,
881
+ node.id
882
+ );
883
+ }
884
+ }
885
+ }
886
+ // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
887
+ checkRule21(w, issues) {
888
+ if (!Array.isArray(w.nodes)) return;
889
+ const webhooksNeedingResponse = w.nodes.filter((n) => {
890
+ if (!n.type.includes("webhook")) return false;
891
+ const params = n.parameters;
892
+ return params?.responseMode === "responseNode";
893
+ });
894
+ if (webhooksNeedingResponse.length === 0) return;
895
+ const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
896
+ if (!hasRespondNode) {
897
+ for (const wh of webhooksNeedingResponse) {
898
+ this.warn(
899
+ issues,
900
+ 21,
901
+ `Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
902
+ wh.id
903
+ );
904
+ }
905
+ }
906
+ }
719
907
  };
720
908
 
721
909
  // src/errors/generation-error.ts
@@ -763,7 +951,7 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
763
951
  "saveDataErrorExecution": "all",
764
952
  "saveDataSuccessExecution": "all",
765
953
  "executionTimeout": 3600,
766
- "timezone": "America/New_York",
954
+ "timezone": "UTC",
767
955
  "executionOrder": "v1"
768
956
  }
769
957
  }
@@ -805,9 +993,21 @@ Every AI Agent must have at least one ai_languageModel sub-node connected.
805
993
  ### IF node \u2014 two output ports (0 = true, 1 = false):
806
994
  "IF Check": { "main": [ [{ "node": "True Path", "type": "main", "index": 0 }], [{ "node": "False Path", "type": "main", "index": 0 }] ] }
807
995
 
996
+ ### SplitInBatches \u2014 two output ports (0 = done/finished, 1 = loop body per batch):
997
+ Connect output 0 to the node that runs AFTER all batches complete.
998
+ Connect output 1 to the processing chain for each batch. The last node in the chain loops back to SplitInBatches via main input.
999
+
1000
+ ### Webhook + RespondToWebhook pattern:
1001
+ When webhook responseMode is "responseNode", you MUST include a respondToWebhook node in the flow.
1002
+ "Webhook": { "main": [[{ "node": "Process Data", "type": "main", "index": 0 }]] }
1003
+ "Process Data": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] }
1004
+
808
1005
  ### Triggers have no incoming connections.
809
1006
  ### Connection keys are NODE NAMES, never node IDs.
810
1007
 
1008
+ ### Nested parameters:
1009
+ Node parameters like conditions, assignments, and rule intervals MUST include all required nested fields. Do not leave nested objects empty or partially filled.
1010
+
811
1011
  ---
812
1012
 
813
1013
  ## NODE CATALOG \u2014 exact type strings and safe typeVersions
@@ -871,16 +1071,16 @@ n8n-nodes-base.redis typeVersion: 1 \u2014 cred: redis
871
1071
  n8n-nodes-base.supabase typeVersion: 1 \u2014 cred: supabaseApi
872
1072
  n8n-nodes-base.awsS3 typeVersion: 2 \u2014 cred: aws
873
1073
 
874
- ### AI \u2014 Root nodes (sit on main data flow):
1074
+ ### AI \u2014 Root nodes (sit on main data flow, receive ai_* connections as TARGETS):
875
1075
  @n8n/n8n-nodes-langchain.agent typeVersion: 1.9 \u2014 params: promptType, text (if define), options.systemMessage
876
1076
  @n8n/n8n-nodes-langchain.chainLlm typeVersion: 1.5
877
1077
  @n8n/n8n-nodes-langchain.chainRetrievalQa typeVersion: 1.4
878
- @n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi
879
- @n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi
1078
+ @n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi \u2014 standalone node, calls OpenAI directly without sub-nodes
1079
+ @n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi \u2014 standalone node, calls Anthropic directly without sub-nodes
880
1080
 
881
- ### AI \u2014 Sub-nodes (sources of ai_* connections):
882
- @n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel
883
- @n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel
1081
+ ### AI \u2014 Sub-nodes (sources of ai_* connections, wire INTO root nodes above):
1082
+ @n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
1083
+ @n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
884
1084
  @n8n/n8n-nodes-langchain.lmChatGoogleGemini typeVersion: 1 \u2014 cred: googlePalmApi \u2014 ai_languageModel
885
1085
  @n8n/n8n-nodes-langchain.memoryBufferWindow typeVersion: 1.3 \u2014 \u2014 ai_memory
886
1086
  @n8n/n8n-nodes-langchain.toolWorkflow typeVersion: 2 \u2014 \u2014 ai_tool
@@ -915,11 +1115,42 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
915
1115
  Respond ONLY with a generate_workflow tool call. No prose. No markdown outside the tool call.
916
1116
  If the request is impossible or unclear, set the error field instead of generating a workflow.`;
917
1117
 
1118
+ // src/utils/thresholds.ts
1119
+ var DIRECT_THRESHOLD = 0.92;
1120
+ var REFERENCE_THRESHOLD = 0.72;
1121
+ function scoreToMode(score) {
1122
+ if (score >= DIRECT_THRESHOLD) return "direct";
1123
+ if (score >= REFERENCE_THRESHOLD) return "reference";
1124
+ return "scratch";
1125
+ }
1126
+
918
1127
  // src/generation/prompt-builder.ts
1128
+ var RULE_REMEDIES = {
1129
+ 1: "Provide a non-empty workflow name string",
1130
+ 2: "Include at least one node in the nodes array",
1131
+ 3: "Every node must have a unique UUID v4 string as its id field",
1132
+ 4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
1133
+ 5: "Every node must have a non-empty type string",
1134
+ 6: "Every node must have a positive integer typeVersion",
1135
+ 7: "Every node must have a position array of exactly [x, y] numbers",
1136
+ 8: "Every node must have a non-empty name string",
1137
+ 9: "connections must be a plain object (use {} if no connections)",
1138
+ 10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
1139
+ 12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
1140
+ 14: "Include at least one trigger node (e.g. webhook, scheduleTrigger, manualTrigger)",
1141
+ 15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1142
+ 16: "All node names must be unique within the workflow",
1143
+ 17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
1144
+ 18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
1145
+ 19: "Use known safe typeVersion values for each node type",
1146
+ 20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1147
+ 21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1148
+ 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
1149
+ };
919
1150
  var PromptBuilder = class {
920
- build(request, matches) {
1151
+ build(request, matches, globalFailureRates = []) {
921
1152
  const mode = this.resolveMode(matches);
922
- const system = this.buildSystem(matches, mode);
1153
+ const system = this.buildSystem(matches, mode, globalFailureRates);
923
1154
  const userMessage = this.buildUserMessage(request, matches, mode);
924
1155
  return { system, userMessage, mode, matches };
925
1156
  }
@@ -936,11 +1167,9 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
936
1167
  if (matches.length === 0) return "scratch";
937
1168
  const top = matches[0];
938
1169
  if (!top) return "scratch";
939
- if (top.score >= 0.92) return "direct";
940
- if (top.score >= 0.72) return "reference";
941
- return "scratch";
1170
+ return scoreToMode(top.score);
942
1171
  }
943
- buildSystem(matches, mode) {
1172
+ buildSystem(matches, mode, globalFailureRates = []) {
944
1173
  const blocks = [
945
1174
  {
946
1175
  type: "text",
@@ -964,15 +1193,63 @@ ${refText}`
964
1193
  }
965
1194
  if (mode === "direct" && matches[0]) {
966
1195
  const match = matches[0];
1196
+ const json = JSON.stringify(match.workflow.workflow, null, 2);
1197
+ if (json.length > 3e4) {
1198
+ const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1199
+ blocks.push({
1200
+ type: "text",
1201
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1202
+ Nodes:
1203
+ ${nodes}`
1204
+ });
1205
+ } else {
1206
+ blocks.push({
1207
+ type: "text",
1208
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1209
+
1210
+ ${json}`
1211
+ });
1212
+ }
1213
+ }
1214
+ if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1215
+ const hint = matches[0];
1216
+ const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
967
1217
  blocks.push({
968
1218
  type: "text",
969
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
970
-
971
- ${JSON.stringify(match.workflow.workflow, null, 2)}`
1219
+ text: `## Weak Structural Hint
1220
+ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
972
1221
  });
973
1222
  }
1223
+ const warnings = this.buildFailureWarnings(matches, globalFailureRates);
1224
+ if (warnings) {
1225
+ blocks.push({ type: "text", text: warnings });
1226
+ }
974
1227
  return blocks;
975
1228
  }
1229
+ buildFailureWarnings(matches, globalFailureRates) {
1230
+ const lines = [];
1231
+ for (const match of matches) {
1232
+ const patterns = match.workflow.failurePatterns;
1233
+ if (!patterns?.length) continue;
1234
+ for (const fp of patterns) {
1235
+ const remedy = RULE_REMEDIES[fp.rule];
1236
+ const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1237
+ lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
1238
+ }
1239
+ }
1240
+ const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
1241
+ for (const rule of highFreqRules) {
1242
+ const remedy = RULE_REMEDIES[rule.rule];
1243
+ const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1244
+ lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
1245
+ }
1246
+ if (lines.length === 0) return null;
1247
+ const unique = [...new Set(lines)];
1248
+ return `## Known Failure Patterns \u2014 AVOID THESE
1249
+
1250
+ Previous builds frequently failed the following validation rules. Ensure your output does NOT repeat these mistakes:
1251
+ ${unique.join("\n")}`;
1252
+ }
976
1253
  buildUserMessage(request, _matches, _mode) {
977
1254
  const namePart = request.name ? `
978
1255
  Workflow name: "${request.name}"` : "";
@@ -1019,7 +1296,7 @@ var GENERATE_WORKFLOW_TOOL = {
1019
1296
  description: "Set this if the request cannot be fulfilled \u2014 explain why"
1020
1297
  }
1021
1298
  },
1022
- required: ["workflow"]
1299
+ required: []
1023
1300
  }
1024
1301
  };
1025
1302
  var WorkflowDesigner = class {
@@ -1035,24 +1312,24 @@ var WorkflowDesigner = class {
1035
1312
  logger;
1036
1313
  validator;
1037
1314
  promptBuilder;
1038
- async design(request, matches) {
1039
- const allIssues = [];
1315
+ async design(request, matches, globalFailureRates = []) {
1040
1316
  const attemptMetadata = [];
1317
+ let lastErrors = [];
1041
1318
  let attempts = 0;
1319
+ const built = this.promptBuilder.build(request, matches, globalFailureRates);
1042
1320
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
1043
1321
  attempts = attempt;
1044
1322
  const temperature = attempt === MAX_ATTEMPTS ? FINAL_TEMPERATURE : BASE_TEMPERATURE;
1045
- const built = this.promptBuilder.build(request, matches);
1046
1323
  let userMessage;
1047
1324
  if (attempt === 1) {
1048
1325
  userMessage = built.userMessage;
1049
1326
  this.logger.debug("WorkflowDesigner: attempt 1", { description: request.description });
1050
1327
  } else {
1051
- const issueLines = allIssues.map(
1328
+ const issueLines = lastErrors.map(
1052
1329
  (i) => `- [Rule ${i.rule}] ${i.message}${i.nodeId ? ` (node: ${i.nodeId})` : ""}`
1053
1330
  );
1054
1331
  userMessage = this.promptBuilder.buildCorrectionMessage(request, matches, issueLines, attempt - 1);
1055
- this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: allIssues.length });
1332
+ this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: lastErrors.length });
1056
1333
  }
1057
1334
  const start = Date.now();
1058
1335
  const message = await this.callClaude(built.system, userMessage, temperature);
@@ -1070,35 +1347,43 @@ var WorkflowDesigner = class {
1070
1347
  tokensInput: message.usage.input_tokens,
1071
1348
  tokensOutput: message.usage.output_tokens,
1072
1349
  validationPassed: validation.valid,
1073
- issues: errors
1350
+ issues: validation.issues
1074
1351
  });
1075
1352
  if (validation.valid) {
1076
1353
  return { workflow: parsed.workflow, credentialsNeeded: parsed.credentialsNeeded, attempts, attemptMetadata };
1077
1354
  }
1078
- allIssues.push(...errors);
1355
+ lastErrors = errors;
1079
1356
  this.logger.warn(`WorkflowDesigner: validation failed on attempt ${attempt}`, {
1080
- newErrors: errors.length,
1081
- totalErrors: allIssues.length
1357
+ errorCount: errors.length
1082
1358
  });
1083
1359
  }
1360
+ const finalIssues = attemptMetadata.at(-1)?.issues ?? lastErrors;
1084
1361
  throw new ValidationError(
1085
- `Workflow failed validation after ${MAX_ATTEMPTS} attempts (${allIssues.length} total errors)`,
1086
- allIssues
1362
+ `Workflow failed validation after ${MAX_ATTEMPTS} attempts`,
1363
+ finalIssues
1087
1364
  );
1088
1365
  }
1089
1366
  async callClaude(system, userMessage, temperature) {
1367
+ const controller = new AbortController();
1368
+ const timer = setTimeout(() => controller.abort(), 12e4);
1090
1369
  try {
1091
- return await this.anthropic.messages.create({
1092
- model: this.model,
1093
- max_tokens: 8192,
1094
- temperature,
1095
- system,
1096
- messages: [{ role: "user", content: userMessage }],
1097
- tools: [GENERATE_WORKFLOW_TOOL],
1098
- tool_choice: { type: "tool", name: "generate_workflow" }
1099
- });
1370
+ return await this.anthropic.messages.create(
1371
+ {
1372
+ model: this.model,
1373
+ max_tokens: 8192,
1374
+ temperature,
1375
+ system: system.map((b) => ({ type: b.type, text: b.text, ...b.cache_control ? { cache_control: b.cache_control } : {} })),
1376
+ messages: [{ role: "user", content: userMessage }],
1377
+ tools: [GENERATE_WORKFLOW_TOOL],
1378
+ tool_choice: { type: "tool", name: "generate_workflow" }
1379
+ },
1380
+ { signal: controller.signal }
1381
+ );
1100
1382
  } catch (err) {
1101
- throw new GenerationError("Anthropic API call failed", err);
1383
+ const detail = err instanceof Error ? err.message : String(err);
1384
+ throw new GenerationError(`Anthropic API call failed: ${detail}`, err);
1385
+ } finally {
1386
+ clearTimeout(timer);
1102
1387
  }
1103
1388
  }
1104
1389
  extractToolUse(message) {
@@ -1131,27 +1416,125 @@ var WorkflowDesigner = class {
1131
1416
  var import_promises = require("fs/promises");
1132
1417
  var import_node_path = require("path");
1133
1418
  var import_node_os = require("os");
1419
+
1420
+ // src/telemetry/types.ts
1421
+ var TELEMETRY_SCHEMA_VERSION = 2;
1422
+
1423
+ // src/telemetry/collector.ts
1134
1424
  var TelemetryCollector = class {
1135
1425
  dir;
1136
1426
  sessionId;
1427
+ dirReady = null;
1137
1428
  constructor(dir) {
1138
1429
  this.dir = dir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "telemetry");
1139
1430
  this.sessionId = generateUUID();
1140
1431
  }
1141
1432
  async emit(eventType, data) {
1142
1433
  const event = {
1434
+ schemaVersion: TELEMETRY_SCHEMA_VERSION,
1143
1435
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1144
1436
  sessionId: this.sessionId,
1145
1437
  eventType,
1146
1438
  data
1147
1439
  };
1148
- await (0, import_promises.mkdir)(this.dir, { recursive: true });
1440
+ if (!this.dirReady) {
1441
+ this.dirReady = (0, import_promises.mkdir)(this.dir, { recursive: true }).then(() => {
1442
+ });
1443
+ }
1444
+ await this.dirReady;
1149
1445
  const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
1150
1446
  const filepath = (0, import_node_path.join)(this.dir, filename);
1151
1447
  await (0, import_promises.appendFile)(filepath, JSON.stringify(event) + "\n", "utf-8");
1152
1448
  }
1153
1449
  };
1154
1450
 
1451
+ // src/telemetry/reader.ts
1452
+ var import_promises2 = require("fs/promises");
1453
+ var import_node_path2 = require("path");
1454
+ var import_node_os2 = require("os");
1455
+ var TelemetryReader = class {
1456
+ dir;
1457
+ cache = null;
1458
+ cacheTime = 0;
1459
+ constructor(dir) {
1460
+ this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
1461
+ }
1462
+ async getFailureRates(days = 30) {
1463
+ const now = Date.now();
1464
+ if (this.cache && now - this.cacheTime < 5 * 60 * 1e3) {
1465
+ return this.cache;
1466
+ }
1467
+ const events = await this.readRecentEvents(days);
1468
+ const buildSessions = new Set(
1469
+ events.filter((e) => e.eventType === "build_complete" && !e.data.dryRun).map((e) => e.sessionId)
1470
+ );
1471
+ if (buildSessions.size === 0) return [];
1472
+ const ruleSessions = /* @__PURE__ */ new Map();
1473
+ for (const event of events) {
1474
+ if (event.eventType !== "generation_attempt") continue;
1475
+ if (!buildSessions.has(event.sessionId)) continue;
1476
+ const data = event.data;
1477
+ if (data.validationPassed || !data.issues) continue;
1478
+ for (const issue of data.issues) {
1479
+ const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
1480
+ entry.sessions.add(event.sessionId);
1481
+ entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
1482
+ ruleSessions.set(issue.rule, entry);
1483
+ }
1484
+ }
1485
+ const rates = [];
1486
+ for (const [rule, entry] of ruleSessions) {
1487
+ let topMessage = "";
1488
+ let topCount = 0;
1489
+ for (const [msg, count] of entry.messages) {
1490
+ if (count > topCount) {
1491
+ topMessage = msg;
1492
+ topCount = count;
1493
+ }
1494
+ }
1495
+ rates.push({
1496
+ rule,
1497
+ failureCount: entry.sessions.size,
1498
+ totalBuilds: buildSessions.size,
1499
+ rate: entry.sessions.size / buildSessions.size,
1500
+ commonMessage: topMessage
1501
+ });
1502
+ }
1503
+ rates.sort((a, b) => b.rate - a.rate);
1504
+ this.cache = rates;
1505
+ this.cacheTime = now;
1506
+ return rates;
1507
+ }
1508
+ async readRecentEvents(days) {
1509
+ let files;
1510
+ try {
1511
+ files = await (0, import_promises2.readdir)(this.dir);
1512
+ } catch {
1513
+ return [];
1514
+ }
1515
+ const cutoff = /* @__PURE__ */ new Date();
1516
+ cutoff.setDate(cutoff.getDate() - days);
1517
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
1518
+ const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1519
+ const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr).sort();
1520
+ const events = [];
1521
+ for (const file of recentFiles) {
1522
+ try {
1523
+ const content = await (0, import_promises2.readFile)((0, import_node_path2.join)(this.dir, file), "utf-8");
1524
+ for (const line of content.split("\n")) {
1525
+ if (!line.trim()) continue;
1526
+ try {
1527
+ events.push(JSON.parse(line));
1528
+ } catch {
1529
+ }
1530
+ }
1531
+ } catch {
1532
+ }
1533
+ }
1534
+ return events;
1535
+ }
1536
+ };
1537
+
1155
1538
  // src/utils/logger.ts
1156
1539
  var nullLogger = {
1157
1540
  debug() {
@@ -1173,27 +1556,53 @@ var Kairos = class {
1173
1556
  library;
1174
1557
  logger;
1175
1558
  telemetry;
1559
+ telemetryReader;
1176
1560
  model;
1561
+ saveQueue = Promise.resolve(null);
1177
1562
  constructor(options) {
1178
1563
  const logger = options.logger ?? nullLogger;
1179
1564
  this.model = options.model ?? DEFAULT_MODEL;
1565
+ if (options.n8nBaseUrl && options.n8nApiKey) {
1566
+ try {
1567
+ new URL(options.n8nBaseUrl);
1568
+ } catch {
1569
+ throw new GuardError(`Invalid n8nBaseUrl: "${options.n8nBaseUrl}" \u2014 must be a valid URL`);
1570
+ }
1571
+ const apiClient = new N8nApiClient(options.n8nBaseUrl, options.n8nApiKey, logger);
1572
+ const stripper = new N8nFieldStripper();
1573
+ this.provider = new N8nProvider(apiClient, stripper);
1574
+ } else {
1575
+ this.provider = null;
1576
+ }
1180
1577
  const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
1181
- const apiClient = new N8nApiClient(options.n8nBaseUrl, options.n8nApiKey, logger);
1182
- const stripper = new N8nFieldStripper();
1183
- this.provider = new N8nProvider(apiClient, stripper);
1184
1578
  this.designer = new WorkflowDesigner(anthropic, this.model, logger);
1185
1579
  this.validator = new N8nValidator();
1186
1580
  this.library = options.library ?? new NullLibrary();
1187
1581
  this.logger = logger;
1188
1582
  if (options.telemetry === true) {
1189
1583
  this.telemetry = new TelemetryCollector();
1584
+ this.telemetryReader = new TelemetryReader();
1190
1585
  } else if (typeof options.telemetry === "string") {
1191
1586
  this.telemetry = new TelemetryCollector(options.telemetry);
1587
+ this.telemetryReader = new TelemetryReader(options.telemetry);
1192
1588
  } else {
1193
1589
  this.telemetry = null;
1590
+ this.telemetryReader = null;
1591
+ }
1592
+ }
1593
+ requireProvider() {
1594
+ if (!this.provider) {
1595
+ throw new GuardError("n8nBaseUrl and n8nApiKey are required for this operation \u2014 set them in the Kairos constructor, or use { dryRun: true } for generation-only mode");
1596
+ }
1597
+ return this.provider;
1598
+ }
1599
+ validateDescription(description) {
1600
+ if (!description || description.trim().length === 0) {
1601
+ throw new GuardError("Description is required and must be non-empty");
1194
1602
  }
1195
1603
  }
1196
1604
  async build(description, options) {
1605
+ this.validateDescription(description);
1197
1606
  this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
1198
1607
  const buildStart = Date.now();
1199
1608
  await this.telemetry?.emit("build_start", {
@@ -1203,33 +1612,28 @@ var Kairos = class {
1203
1612
  });
1204
1613
  await this.library.initialize();
1205
1614
  const matches = await this.library.search(description);
1615
+ if (matches.length > 0) {
1616
+ const top = matches[0];
1617
+ this.logger.info(`Library: ${matches.length} match(es), top="${top.workflow.description.slice(0, 50)}" score=${top.score.toFixed(2)} mode=${top.mode}`);
1618
+ } else {
1619
+ this.logger.info("Library: no matches (scratch mode)");
1620
+ }
1621
+ const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
1622
+ if (globalFailureRates.length > 0) {
1623
+ const highFreq = globalFailureRates.filter((r) => r.rate >= 0.15);
1624
+ if (highFreq.length > 0) {
1625
+ this.logger.info(`Telemetry: ${highFreq.length} high-frequency failure rule(s) will be warned about`);
1626
+ }
1627
+ }
1206
1628
  const designResult = await this.designer.design(
1207
1629
  { description, ...options?.name ? { name: options.name } : {} },
1208
- matches
1630
+ matches,
1631
+ globalFailureRates
1209
1632
  );
1210
- for (const meta of designResult.attemptMetadata) {
1211
- await this.telemetry?.emit("generation_attempt", {
1212
- description,
1213
- attempt: meta.attempt,
1214
- temperature: meta.temperature,
1215
- durationMs: meta.durationMs,
1216
- tokensInput: meta.tokensInput,
1217
- tokensOutput: meta.tokensOutput,
1218
- validationPassed: meta.validationPassed,
1219
- issueCount: meta.issues.length,
1220
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message }))
1221
- });
1222
- }
1633
+ await this.emitAttemptTelemetry(description, designResult);
1223
1634
  const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
1635
+ this.saveToLibrary(workflow, description, designResult, matches);
1224
1636
  if (options?.dryRun) {
1225
- const result2 = {
1226
- workflowId: null,
1227
- name: workflow.name,
1228
- credentialsNeeded: designResult.credentialsNeeded,
1229
- activationRequired: true,
1230
- generationAttempts: designResult.attempts,
1231
- dryRun: true
1232
- };
1233
1637
  const totalTokensInput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
1234
1638
  const totalTokensOutput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
1235
1639
  await this.telemetry?.emit("build_complete", {
@@ -1244,23 +1648,64 @@ var Kairos = class {
1244
1648
  dryRun: true,
1245
1649
  credentialsNeeded: designResult.credentialsNeeded.length
1246
1650
  });
1247
- return result2;
1651
+ return {
1652
+ workflowId: null,
1653
+ name: workflow.name,
1654
+ workflow,
1655
+ credentialsNeeded: designResult.credentialsNeeded,
1656
+ activationRequired: true,
1657
+ generationAttempts: designResult.attempts,
1658
+ dryRun: true
1659
+ };
1248
1660
  }
1249
- const deployed = await this.provider.deploy(workflow);
1250
- this.library.save(workflow, { description }).catch((err) => {
1251
- this.logger.warn("Failed to save workflow to library (non-fatal)", { err: String(err) });
1252
- });
1661
+ const provider = this.requireProvider();
1662
+ const deployed = await provider.deploy(workflow);
1663
+ this.recordDeploy();
1253
1664
  if (options?.activate) {
1254
- await this.provider.activate(deployed.workflowId);
1665
+ await provider.activate(deployed.workflowId);
1255
1666
  }
1256
- const result = {
1667
+ const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
1668
+ const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
1669
+ await this.telemetry?.emit("build_complete", {
1670
+ description,
1671
+ success: true,
1672
+ totalAttempts: designResult.attempts,
1673
+ totalDurationMs: Date.now() - buildStart,
1674
+ totalTokensInput,
1675
+ totalTokensOutput,
1676
+ workflowName: deployed.name,
1677
+ workflowId: deployed.workflowId,
1678
+ dryRun: false,
1679
+ credentialsNeeded: designResult.credentialsNeeded.length
1680
+ });
1681
+ return {
1257
1682
  workflowId: deployed.workflowId,
1258
1683
  name: deployed.name,
1684
+ workflow,
1259
1685
  credentialsNeeded: designResult.credentialsNeeded,
1260
1686
  activationRequired: !options?.activate,
1261
1687
  generationAttempts: designResult.attempts,
1262
1688
  dryRun: false
1263
1689
  };
1690
+ }
1691
+ async replace(id, description) {
1692
+ this.validateDescription(description);
1693
+ this.logger.info("Kairos.update", { id, description });
1694
+ const buildStart = Date.now();
1695
+ await this.telemetry?.emit("build_start", {
1696
+ description,
1697
+ model: this.model,
1698
+ dryRun: false
1699
+ });
1700
+ await this.library.initialize();
1701
+ const matches = await this.library.search(description);
1702
+ const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
1703
+ const designResult = await this.designer.design({ description }, matches, globalFailureRates);
1704
+ await this.emitAttemptTelemetry(description, designResult);
1705
+ const provider = this.requireProvider();
1706
+ const deployed = await provider.update(id, designResult.workflow);
1707
+ this.saveToLibrary(designResult.workflow, description, designResult, matches);
1708
+ this.recordDeploy();
1264
1709
  const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
1265
1710
  const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
1266
1711
  await this.telemetry?.emit("build_complete", {
@@ -1275,134 +1720,449 @@ var Kairos = class {
1275
1720
  dryRun: false,
1276
1721
  credentialsNeeded: designResult.credentialsNeeded.length
1277
1722
  });
1278
- return result;
1279
- }
1280
- async update(id, description) {
1281
- this.logger.info("Kairos.update", { id, description });
1282
- const matches = await this.library.search(description);
1283
- const designResult = await this.designer.design({ description }, matches);
1284
- const deployed = await this.provider.update(id, designResult.workflow);
1285
1723
  return {
1286
1724
  workflowId: deployed.workflowId,
1287
1725
  name: deployed.name,
1726
+ workflow: designResult.workflow,
1288
1727
  credentialsNeeded: designResult.credentialsNeeded,
1289
1728
  activationRequired: true,
1290
1729
  generationAttempts: designResult.attempts,
1291
1730
  dryRun: false
1292
1731
  };
1293
1732
  }
1733
+ async drain() {
1734
+ await this.saveQueue.catch(() => {
1735
+ });
1736
+ }
1737
+ async emitAttemptTelemetry(description, designResult) {
1738
+ for (const meta of designResult.attemptMetadata) {
1739
+ await this.telemetry?.emit("generation_attempt", {
1740
+ description,
1741
+ attempt: meta.attempt,
1742
+ temperature: meta.temperature,
1743
+ durationMs: meta.durationMs,
1744
+ tokensInput: meta.tokensInput,
1745
+ tokensOutput: meta.tokensOutput,
1746
+ validationPassed: meta.validationPassed,
1747
+ issueCount: meta.issues.length,
1748
+ issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message }))
1749
+ });
1750
+ }
1751
+ }
1752
+ recordDeploy() {
1753
+ this.saveQueue = this.saveQueue.then(async (savedId) => {
1754
+ if (savedId) {
1755
+ await this.library.recordDeployment(savedId);
1756
+ }
1757
+ return savedId;
1758
+ }).catch((err) => {
1759
+ this.logger.warn("Failed to record deployment (non-fatal)", { err: String(err) });
1760
+ return null;
1761
+ });
1762
+ }
1763
+ saveToLibrary(workflow, description, designResult, matches) {
1764
+ const failedAttempts = designResult.attemptMetadata.filter((m) => !m.validationPassed);
1765
+ const failurePatterns = failedAttempts.flatMap(
1766
+ (m) => m.issues.map((i) => ({ rule: i.rule, message: i.message }))
1767
+ );
1768
+ const topMatch = matches[0];
1769
+ const generationMode = topMatch ? scoreToMode(topMatch.score) : "scratch";
1770
+ const autoTags = Array.from(new Set(
1771
+ workflow.nodes.flatMap((n) => {
1772
+ const bare = n.type.split(".").pop() ?? "";
1773
+ const tags = [bare];
1774
+ if (n.type.includes("Trigger") || n.type.includes("trigger")) tags.push(`trigger:${bare}`);
1775
+ if (n.type.includes("langchain")) tags.push("ai");
1776
+ return tags;
1777
+ })
1778
+ ));
1779
+ const metadata = {
1780
+ description,
1781
+ generationMode,
1782
+ generationAttempts: designResult.attempts
1783
+ };
1784
+ if (autoTags.length > 0) metadata.tags = autoTags;
1785
+ if (failurePatterns.length > 0) metadata.failurePatterns = failurePatterns;
1786
+ if (matches.length > 0) metadata.sourceWorkflowIds = matches.map((m) => m.workflow.id);
1787
+ if (topMatch) metadata.topMatchScore = topMatch.score;
1788
+ if (designResult.credentialsNeeded.length > 0) metadata.credentialsNeeded = designResult.credentialsNeeded;
1789
+ const firstTryPass = designResult.attemptMetadata.length > 0 && designResult.attemptMetadata[0].validationPassed;
1790
+ const failedRules = Array.from(new Set(
1791
+ designResult.attemptMetadata.filter((m) => !m.validationPassed).flatMap((m) => m.issues.map((i) => i.rule))
1792
+ ));
1793
+ this.saveQueue = this.saveQueue.then(async () => {
1794
+ const savedId = await this.library.save(workflow, metadata);
1795
+ for (const match of matches) {
1796
+ if (match.mode === "direct" || match.mode === "reference") {
1797
+ await this.library.recordOutcome(match.workflow.id, {
1798
+ attempts: designResult.attempts,
1799
+ firstTryPass,
1800
+ failedRules,
1801
+ mode: match.mode
1802
+ });
1803
+ }
1804
+ }
1805
+ return savedId;
1806
+ }).catch((err) => {
1807
+ this.logger.warn("Failed to save workflow to library (non-fatal)", { err: String(err) });
1808
+ return null;
1809
+ });
1810
+ }
1294
1811
  async get(id) {
1295
- return this.provider.get(id);
1812
+ return this.requireProvider().get(id);
1296
1813
  }
1297
1814
  async list() {
1298
- return this.provider.list();
1815
+ return this.requireProvider().list();
1299
1816
  }
1300
1817
  async activate(id) {
1301
- await this.provider.activate(id);
1818
+ await this.requireProvider().activate(id);
1302
1819
  }
1303
1820
  async deactivate(id) {
1304
- await this.provider.deactivate(id);
1821
+ await this.requireProvider().deactivate(id);
1305
1822
  }
1306
1823
  async delete(id, options) {
1307
- await this.provider.delete(id, options);
1824
+ await this.requireProvider().delete(id, options);
1308
1825
  }
1309
1826
  async executions(workflowId, filter) {
1310
- return this.provider.executions(workflowId, filter);
1827
+ return this.requireProvider().executions(workflowId, filter);
1311
1828
  }
1312
1829
  async execution(id) {
1313
- return this.provider.execution(id);
1830
+ return this.requireProvider().execution(id);
1314
1831
  }
1315
1832
  async listTags() {
1316
- return this.provider.listTags();
1833
+ return this.requireProvider().listTags();
1317
1834
  }
1318
1835
  async createTag(name) {
1319
- return this.provider.createTag(name);
1836
+ return this.requireProvider().createTag(name);
1320
1837
  }
1321
1838
  async tag(workflowId, tagIds) {
1322
- await this.provider.tag(workflowId, tagIds);
1839
+ await this.requireProvider().tag(workflowId, tagIds);
1323
1840
  }
1324
1841
  async untag(workflowId, tagIds) {
1325
- await this.provider.untag(workflowId, tagIds);
1842
+ await this.requireProvider().untag(workflowId, tagIds);
1326
1843
  }
1327
1844
  };
1328
1845
 
1329
1846
  // src/library/file-library.ts
1330
- var import_promises2 = require("fs/promises");
1331
- var import_node_path2 = require("path");
1332
- var import_node_os2 = require("os");
1847
+ var import_promises3 = require("fs/promises");
1848
+ var import_node_path3 = require("path");
1849
+ var import_node_os3 = require("os");
1850
+
1851
+ // src/library/scorer.ts
1852
+ var WEIGHTS = {
1853
+ tfidf: 0.35,
1854
+ nodeFingerprint: 0.3,
1855
+ outcome: 0.2,
1856
+ deploy: 0.15
1857
+ };
1858
+ var NODE_KEYWORDS = {
1859
+ slack: ["slack", "slackApi"],
1860
+ email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
1861
+ webhook: ["webhook", "webhookTrigger"],
1862
+ schedule: ["scheduleTrigger", "cron"],
1863
+ http: ["httpRequest"],
1864
+ sheets: ["googleSheets"],
1865
+ github: ["github", "githubTrigger"],
1866
+ telegram: ["telegram", "telegramTrigger"],
1867
+ ai: ["agent", "openAi", "lmChatOpenAi", "lmChatAnthropic", "chainLlm", "chainSummarization"],
1868
+ memory: ["memoryBufferWindow", "memoryXata", "memoryPostgres"],
1869
+ vector: ["vectorStoreInMemory", "vectorStorePinecone", "vectorStoreQdrant"],
1870
+ database: ["postgres", "mySql", "redis", "mongoDb"],
1871
+ airtable: ["airtable"],
1872
+ notion: ["notion"],
1873
+ s3: ["awsS3"],
1874
+ code: ["code"],
1875
+ merge: ["merge"],
1876
+ switch: ["switch"],
1877
+ if: ["if"],
1878
+ wait: ["wait"],
1879
+ rss: ["rssFeedRead", "rssFeedReadTrigger"],
1880
+ form: ["formTrigger"],
1881
+ set: ["set"],
1882
+ split: ["splitInBatches"],
1883
+ filter: ["filter"],
1884
+ telegram_trigger: ["telegramTrigger"],
1885
+ stripe: ["stripe"]
1886
+ };
1887
+ function extractQueryFingerprint(description) {
1888
+ const lower = description.toLowerCase();
1889
+ const matches = /* @__PURE__ */ new Set();
1890
+ for (const [keyword, nodeTypes] of Object.entries(NODE_KEYWORDS)) {
1891
+ if (lower.includes(keyword)) {
1892
+ for (const nt of nodeTypes) matches.add(nt);
1893
+ }
1894
+ }
1895
+ if (/\bevery\b|\bdaily\b|\bhourly\b|\bweekly\b|\bmonthly\b|\bcron\b|\bschedule\b|\bat \d/.test(lower)) {
1896
+ matches.add("scheduleTrigger");
1897
+ }
1898
+ if (/\bwebhook\b|\breceive\b.*\bpost\b|\bpost\b.*\brequest\b/.test(lower)) {
1899
+ matches.add("webhook");
1900
+ }
1901
+ if (/\bchat\b|\bchatbot\b|\bconversation\b/.test(lower)) {
1902
+ matches.add("chatTrigger");
1903
+ }
1904
+ if (/\bai\b|\bllm\b|\bgpt\b|\bclaude\b|\bagent\b|\bsummariz/.test(lower)) {
1905
+ matches.add("agent");
1906
+ }
1907
+ return matches;
1908
+ }
1909
+ function extractWorkflowFingerprint(w) {
1910
+ const fp = /* @__PURE__ */ new Set();
1911
+ for (const node of w.workflow.nodes) {
1912
+ const bare = node.type.split(".").pop() ?? "";
1913
+ fp.add(bare);
1914
+ }
1915
+ return fp;
1916
+ }
1917
+ function jaccardSimilarity(a, b) {
1918
+ if (a.size === 0 && b.size === 0) return 0;
1919
+ let intersection = 0;
1920
+ for (const item of a) {
1921
+ if (b.has(item)) intersection++;
1922
+ }
1923
+ const union = a.size + b.size - intersection;
1924
+ return union > 0 ? intersection / union : 0;
1925
+ }
1926
+ function outcomeScore(w) {
1927
+ const stats = w.outcomeStats;
1928
+ if (!stats || stats.totalUses === 0) return 0.5;
1929
+ const passRate = stats.firstTryPasses / stats.totalUses;
1930
+ const avgAttempts = stats.totalAttempts / stats.totalUses;
1931
+ const attemptPenalty = Math.max(0, 1 - (avgAttempts - 1) * 0.3);
1932
+ return passRate * 0.6 + attemptPenalty * 0.4;
1933
+ }
1934
+ function deployScore(w) {
1935
+ return 1 + Math.log(w.deployCount + 1) * 0.1;
1936
+ }
1937
+ function hybridScore(queryTokens, queryDescription, workflows, docTokenArrays, idf) {
1938
+ const queryFp = extractQueryFingerprint(queryDescription);
1939
+ const ceiling = queryTokens.reduce((sum, qt) => sum + (idf.get(qt) ?? 0), 0) || 1;
1940
+ return workflows.map((w, i) => {
1941
+ const docTokens = docTokenArrays[i];
1942
+ let tfidfRaw = 0;
1943
+ const docFreq = /* @__PURE__ */ new Map();
1944
+ for (const t of docTokens) {
1945
+ docFreq.set(t, (docFreq.get(t) ?? 0) + 1);
1946
+ }
1947
+ for (const qt of queryTokens) {
1948
+ const tf = docTokens.length > 0 ? (docFreq.get(qt) ?? 0) / docTokens.length : 0;
1949
+ const idfVal = idf.get(qt) ?? 0;
1950
+ tfidfRaw += tf * idfVal;
1951
+ }
1952
+ const tfidf = Math.min(tfidfRaw / ceiling, 1);
1953
+ const workflowFp = extractWorkflowFingerprint(w);
1954
+ const nodeFingerprint = queryFp.size > 0 ? jaccardSimilarity(queryFp, workflowFp) : 0;
1955
+ const outcome = outcomeScore(w);
1956
+ const deploy = Math.min(deployScore(w), 1.5) / 1.5;
1957
+ const score = Math.min(
1958
+ WEIGHTS.tfidf * tfidf + WEIGHTS.nodeFingerprint * nodeFingerprint + WEIGHTS.outcome * outcome + WEIGHTS.deploy * deploy,
1959
+ 1
1960
+ );
1961
+ return {
1962
+ workflow: w,
1963
+ score,
1964
+ signals: { tfidf, nodeFingerprint, outcome, deploy }
1965
+ };
1966
+ });
1967
+ }
1968
+
1969
+ // src/library/cluster.ts
1970
+ function getFingerprint(w) {
1971
+ return w.workflow.nodes.map((n) => n.type.split(".").pop() ?? "").sort();
1972
+ }
1973
+ function fingerprintKey(fp) {
1974
+ return fp.join("|");
1975
+ }
1976
+ function describePattern(fp) {
1977
+ const triggers = fp.filter((n) => /trigger/i.test(n));
1978
+ const outputs = fp.filter((n) => /slack|gmail|email|telegram|sheets|airtable|notion/i.test(n));
1979
+ const ai = fp.filter((n) => /agent|openai|anthropic|chain|memory/i.test(n));
1980
+ const core = fp.filter((n) => /httpRequest|code|merge|switch|if|set|filter/i.test(n));
1981
+ const parts = [];
1982
+ if (triggers.length > 0) parts.push(triggers[0]);
1983
+ if (ai.length > 0) parts.push("AI");
1984
+ if (core.length > 0) parts.push(core.slice(0, 2).join("+"));
1985
+ if (outputs.length > 0) parts.push(outputs[0]);
1986
+ return parts.length > 0 ? parts.join(" \u2192 ") : fp.slice(0, 3).join(" \u2192 ");
1987
+ }
1988
+ function clusterWorkflows(workflows) {
1989
+ const groups = /* @__PURE__ */ new Map();
1990
+ for (const w of workflows) {
1991
+ const fp = getFingerprint(w);
1992
+ const key = fingerprintKey(fp);
1993
+ const existing = groups.get(key);
1994
+ if (existing) {
1995
+ existing.push(w);
1996
+ } else {
1997
+ groups.set(key, [w]);
1998
+ }
1999
+ }
2000
+ const clusters = [];
2001
+ for (const [, members] of groups) {
2002
+ if (members.length === 0) continue;
2003
+ const fp = getFingerprint(members[0]);
2004
+ const withStats = members.filter((m) => m.outcomeStats && m.outcomeStats.totalUses > 0);
2005
+ let avgFirstTryPassRate = 0;
2006
+ let avgAttempts = 0;
2007
+ if (withStats.length > 0) {
2008
+ avgFirstTryPassRate = withStats.reduce((sum, m) => {
2009
+ const s = m.outcomeStats;
2010
+ return sum + s.firstTryPasses / s.totalUses;
2011
+ }, 0) / withStats.length;
2012
+ avgAttempts = withStats.reduce((sum, m) => {
2013
+ const s = m.outcomeStats;
2014
+ return sum + s.totalAttempts / s.totalUses;
2015
+ }, 0) / withStats.length;
2016
+ }
2017
+ const ruleCounts = /* @__PURE__ */ new Map();
2018
+ let totalFailureInstances = 0;
2019
+ for (const m of withStats) {
2020
+ const rules = m.outcomeStats.failedRules;
2021
+ for (const [rule, count] of Object.entries(rules)) {
2022
+ const r = parseInt(rule, 10);
2023
+ ruleCounts.set(r, (ruleCounts.get(r) ?? 0) + count);
2024
+ totalFailureInstances += count;
2025
+ }
2026
+ }
2027
+ const commonFailedRules = [...ruleCounts.entries()].map(([rule, count]) => ({
2028
+ rule,
2029
+ frequency: totalFailureInstances > 0 ? count / totalFailureInstances : 0
2030
+ })).filter((r) => r.frequency >= 0.1).sort((a, b) => b.frequency - a.frequency);
2031
+ clusters.push({
2032
+ pattern: describePattern(fp),
2033
+ fingerprint: fp,
2034
+ members,
2035
+ avgFirstTryPassRate,
2036
+ avgAttempts,
2037
+ commonFailedRules
2038
+ });
2039
+ }
2040
+ return clusters.sort((a, b) => b.members.length - a.members.length);
2041
+ }
2042
+ function rerank(candidates, clusters) {
2043
+ const clusterMap = /* @__PURE__ */ new Map();
2044
+ for (const cluster of clusters) {
2045
+ for (const member of cluster.members) {
2046
+ clusterMap.set(member.id, cluster);
2047
+ }
2048
+ }
2049
+ return candidates.map((c) => {
2050
+ const cluster = clusterMap.get(c.workflow.id);
2051
+ let boost = 0;
2052
+ if (cluster && cluster.avgFirstTryPassRate > 0) {
2053
+ boost = (cluster.avgFirstTryPassRate - 0.5) * 0.1;
2054
+ }
2055
+ if (cluster && cluster.commonFailedRules.length > 0) {
2056
+ boost -= cluster.commonFailedRules.length * 0.02;
2057
+ }
2058
+ return {
2059
+ workflow: c.workflow,
2060
+ score: Math.max(0, Math.min(1, c.score + boost)),
2061
+ ...cluster ? { clusterPattern: cluster.pattern } : {}
2062
+ };
2063
+ }).sort((a, b) => b.score - a.score);
2064
+ }
2065
+
2066
+ // src/library/file-library.ts
1333
2067
  function tokenize(text) {
1334
2068
  return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2);
1335
2069
  }
1336
- function computeTfIdf(queryTokens, docTokens, idf) {
1337
- if (docTokens.length === 0) return 0;
1338
- let score = 0;
1339
- const docFreq = /* @__PURE__ */ new Map();
1340
- for (const t of docTokens) {
1341
- docFreq.set(t, (docFreq.get(t) ?? 0) + 1);
1342
- }
1343
- for (const qt of queryTokens) {
1344
- const tf = (docFreq.get(qt) ?? 0) / docTokens.length;
1345
- const idfVal = idf.get(qt) ?? 0;
1346
- score += tf * idfVal;
1347
- }
1348
- return score;
2070
+ function buildSearchCorpus(w) {
2071
+ const nodeTokens = w.workflow.nodes.map((n) => {
2072
+ const bare = n.type.split(".").pop() ?? "";
2073
+ const spaced = bare.replace(/([A-Z])/g, " $1").trim().toLowerCase();
2074
+ return `${bare} ${spaced}`;
2075
+ });
2076
+ return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
1349
2077
  }
2078
+ var MAX_LIBRARY_SIZE = 500;
1350
2079
  var FileLibrary = class {
1351
2080
  dir;
1352
2081
  workflows = [];
1353
- initialized = false;
2082
+ initPromise = null;
2083
+ writeQueue = Promise.resolve();
1354
2084
  constructor(dir) {
1355
- this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "library");
2085
+ this.dir = dir ?? (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".kairos", "library");
1356
2086
  }
1357
2087
  async initialize() {
1358
- if (this.initialized) return;
1359
- await (0, import_promises2.mkdir)(this.dir, { recursive: true });
1360
- const indexPath = (0, import_node_path2.join)(this.dir, "index.json");
2088
+ if (!this.initPromise) {
2089
+ this.initPromise = this.doInitialize();
2090
+ }
2091
+ return this.initPromise;
2092
+ }
2093
+ async doInitialize() {
2094
+ await (0, import_promises3.mkdir)(this.dir, { recursive: true });
2095
+ const indexPath = (0, import_node_path3.join)(this.dir, "index.json");
1361
2096
  try {
1362
- const raw = await (0, import_promises2.readFile)(indexPath, "utf-8");
1363
- this.workflows = JSON.parse(raw);
2097
+ const raw = await (0, import_promises3.readFile)(indexPath, "utf-8");
2098
+ const parsed = JSON.parse(raw);
2099
+ if (!Array.isArray(parsed)) {
2100
+ this.workflows = [];
2101
+ } else {
2102
+ this.workflows = parsed.filter(
2103
+ (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)
2104
+ );
2105
+ }
1364
2106
  } catch {
1365
2107
  this.workflows = [];
1366
2108
  }
1367
- this.initialized = true;
1368
2109
  }
1369
2110
  async search(description, options) {
1370
- if (this.workflows.length === 0) return [];
2111
+ const searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
2112
+ if (searchable.length === 0) return [];
1371
2113
  const limit = options?.limit ?? 3;
1372
2114
  const queryTokens = tokenize(description);
1373
2115
  if (queryTokens.length === 0) return [];
1374
- const docTokenSets = this.workflows.map(
1375
- (w) => tokenize(`${w.description} ${w.workflow.name} ${w.tags.join(" ")}`)
1376
- );
1377
- const docCount = this.workflows.length;
2116
+ const docTokenArrays = searchable.map((w) => tokenize(buildSearchCorpus(w)));
2117
+ const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
2118
+ const docCount = searchable.length;
1378
2119
  const idf = /* @__PURE__ */ new Map();
1379
2120
  const allTokens = new Set(queryTokens);
1380
2121
  for (const token of allTokens) {
1381
- const docsWithToken = docTokenSets.filter((d) => d.includes(token)).length;
2122
+ const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
1382
2123
  idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
1383
2124
  }
1384
- const scored = this.workflows.map((w, i) => ({
1385
- workflow: w,
1386
- score: computeTfIdf(queryTokens, docTokenSets[i], idf)
1387
- })).filter((m) => m.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
1388
- return scored.map((m) => ({
1389
- workflow: m.workflow,
1390
- score: m.score,
1391
- mode: m.score > 0.5 ? "direct" : "reference"
1392
- }));
2125
+ const scored = hybridScore(queryTokens, description, searchable, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
2126
+ const clusters = clusterWorkflows(searchable);
2127
+ const reranked = rerank(scored, clusters).slice(0, limit);
2128
+ const results = reranked.map((m) => {
2129
+ return { workflow: m.workflow, score: m.score, mode: scoreToMode(m.score) };
2130
+ });
2131
+ if (results.length > 0) {
2132
+ for (const r of results) {
2133
+ r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
2134
+ }
2135
+ this.persist();
2136
+ }
2137
+ return results;
1393
2138
  }
1394
2139
  async save(workflow, metadata) {
1395
2140
  const id = generateUUID();
2141
+ const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
1396
2142
  const stored = {
1397
2143
  id,
1398
2144
  workflow,
1399
2145
  description: metadata.description,
1400
2146
  tags: metadata.tags ?? [],
1401
2147
  platform: metadata.platform ?? "n8n",
1402
- deployCount: 1,
1403
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
2148
+ deployCount: 0,
2149
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2150
+ ...failurePatterns?.length ? { failurePatterns } : {},
2151
+ ...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
2152
+ ...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
2153
+ ...metadata.topMatchScore != null ? { topMatchScore: metadata.topMatchScore } : {},
2154
+ ...metadata.generationAttempts != null ? { generationAttempts: metadata.generationAttempts } : {},
2155
+ ...metadata.credentialsNeeded?.length ? { credentialsNeeded: metadata.credentialsNeeded } : {},
2156
+ ...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
2157
+ ...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
2158
+ ...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
2159
+ ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
1404
2160
  };
1405
2161
  this.workflows.push(stored);
2162
+ if (this.workflows.length > MAX_LIBRARY_SIZE) {
2163
+ this.workflows.sort((a, b) => (b.deployCount ?? 1) - (a.deployCount ?? 1));
2164
+ this.workflows = this.workflows.slice(0, MAX_LIBRARY_SIZE);
2165
+ }
1406
2166
  await this.persist();
1407
2167
  return id;
1408
2168
  }
@@ -1414,6 +2174,28 @@ var FileLibrary = class {
1414
2174
  await this.persist();
1415
2175
  }
1416
2176
  }
2177
+ async recordOutcome(id, outcome) {
2178
+ const w = this.workflows.find((w2) => w2.id === id);
2179
+ if (!w) return;
2180
+ if (outcome.mode === "direct") {
2181
+ w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
2182
+ } else {
2183
+ w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
2184
+ }
2185
+ const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
2186
+ stats.totalUses++;
2187
+ stats.totalAttempts += outcome.attempts;
2188
+ if (outcome.firstTryPass) stats.firstTryPasses++;
2189
+ for (const rule of outcome.failedRules) {
2190
+ const key = String(rule);
2191
+ stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
2192
+ }
2193
+ w.outcomeStats = stats;
2194
+ await this.persist();
2195
+ }
2196
+ async drain() {
2197
+ await this.writeQueue;
2198
+ }
1417
2199
  async get(id) {
1418
2200
  return this.workflows.find((w) => w.id === id) ?? null;
1419
2201
  }
@@ -1427,9 +2209,202 @@ var FileLibrary = class {
1427
2209
  }
1428
2210
  return result;
1429
2211
  }
1430
- async persist() {
1431
- const indexPath = (0, import_node_path2.join)(this.dir, "index.json");
1432
- await (0, import_promises2.writeFile)(indexPath, JSON.stringify(this.workflows, null, 2), "utf-8");
2212
+ deduplicateFailurePatterns(patterns) {
2213
+ if (!patterns?.length) return void 0;
2214
+ const map = /* @__PURE__ */ new Map();
2215
+ for (const fp of patterns) {
2216
+ const existing = map.get(fp.rule);
2217
+ if (existing) {
2218
+ existing.occurrences++;
2219
+ } else {
2220
+ map.set(fp.rule, { rule: fp.rule, message: fp.message, occurrences: 1 });
2221
+ }
2222
+ }
2223
+ return [...map.values()];
2224
+ }
2225
+ persist() {
2226
+ this.writeQueue = this.writeQueue.then(async () => {
2227
+ const indexPath = (0, import_node_path3.join)(this.dir, "index.json");
2228
+ const tmpPath = `${indexPath}.tmp`;
2229
+ await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
2230
+ await (0, import_promises3.rename)(tmpPath, indexPath);
2231
+ });
2232
+ return this.writeQueue;
2233
+ }
2234
+ };
2235
+
2236
+ // src/templates/safety.ts
2237
+ var BLOCKED_NODE_TYPES = /* @__PURE__ */ new Set([
2238
+ "n8n-nodes-base.code",
2239
+ "n8n-nodes-base.executeCommand",
2240
+ "n8n-nodes-base.ssh"
2241
+ ]);
2242
+ var REVIEW_NODE_TYPES = /* @__PURE__ */ new Set([
2243
+ "n8n-nodes-base.httpRequest"
2244
+ ]);
2245
+ var SECRET_PATTERNS = [
2246
+ /sk-[a-zA-Z0-9]{20,}/,
2247
+ /ghp_[a-zA-Z0-9]{36}/,
2248
+ /xoxb-[0-9]+-[0-9]+-[a-zA-Z0-9]+/,
2249
+ /AIza[a-zA-Z0-9_-]{35}/,
2250
+ /AKIA[A-Z0-9]{16}/
2251
+ ];
2252
+ function assessTemplateSafety(workflow) {
2253
+ const reasons = [];
2254
+ let worst = "safe";
2255
+ const escalate = (level, reason) => {
2256
+ reasons.push(reason);
2257
+ if (level === "blocked") worst = "blocked";
2258
+ else if (level === "review" && worst === "safe") worst = "review";
2259
+ };
2260
+ for (const node of workflow.nodes) {
2261
+ if (BLOCKED_NODE_TYPES.has(node.type)) {
2262
+ escalate("blocked", `Contains ${node.type} node "${node.name}"`);
2263
+ }
2264
+ if (REVIEW_NODE_TYPES.has(node.type)) {
2265
+ escalate("review", `Contains ${node.type} node "${node.name}"`);
2266
+ }
2267
+ const paramStr = JSON.stringify(node.parameters);
2268
+ for (const pattern of SECRET_PATTERNS) {
2269
+ if (pattern.test(paramStr)) {
2270
+ escalate("blocked", `Node "${node.name}" parameters contain a hardcoded secret`);
2271
+ break;
2272
+ }
2273
+ }
2274
+ }
2275
+ return { trustLevel: worst, reasons };
2276
+ }
2277
+
2278
+ // src/templates/syncer.ts
2279
+ var N8N_TEMPLATE_API = "https://api.n8n.io/api/templates";
2280
+ var PAGE_SIZE = 50;
2281
+ var DELAY_BETWEEN_FETCHES_MS = 200;
2282
+ var DEFAULT_SETTINGS = {
2283
+ executionOrder: "v1",
2284
+ saveManualExecutions: true,
2285
+ timezone: "UTC"
2286
+ };
2287
+ var TemplateSyncer = class {
2288
+ constructor(library, logger) {
2289
+ this.library = library;
2290
+ this.validator = new N8nValidator();
2291
+ this.logger = logger;
2292
+ }
2293
+ library;
2294
+ validator;
2295
+ logger;
2296
+ async sync(options) {
2297
+ const maxTemplates = options?.maxTemplates ?? 500;
2298
+ await this.library.initialize();
2299
+ const existing = await this.library.list();
2300
+ const existingSourceIds = new Set(
2301
+ existing.filter((w) => w.sourceKind === "n8n-template" && w.sourceId).map((w) => w.sourceId)
2302
+ );
2303
+ const progress = {
2304
+ total: 0,
2305
+ processed: 0,
2306
+ saved: 0,
2307
+ skippedPaid: 0,
2308
+ skippedDuplicate: 0,
2309
+ blocked: 0,
2310
+ reviewed: 0
2311
+ };
2312
+ const templateIds = await this.fetchTemplateIds(maxTemplates, progress);
2313
+ for (const id of templateIds) {
2314
+ if (existingSourceIds.has(String(id))) {
2315
+ progress.skippedDuplicate++;
2316
+ progress.processed++;
2317
+ options?.onProgress?.(progress);
2318
+ continue;
2319
+ }
2320
+ try {
2321
+ await this.processTemplate(id, progress);
2322
+ } catch (err) {
2323
+ this.logger.warn(`Failed to process template ${id}`, { err: String(err) });
2324
+ }
2325
+ progress.processed++;
2326
+ options?.onProgress?.(progress);
2327
+ await new Promise((resolve) => setTimeout(resolve, DELAY_BETWEEN_FETCHES_MS));
2328
+ }
2329
+ return progress;
2330
+ }
2331
+ async fetchTemplateIds(max, progress) {
2332
+ const ids = [];
2333
+ let page = 1;
2334
+ while (ids.length < max) {
2335
+ const url = `${N8N_TEMPLATE_API}/search?page=${page}&rows=${PAGE_SIZE}`;
2336
+ const response = await fetch(url);
2337
+ if (!response.ok) break;
2338
+ const data = await response.json();
2339
+ progress.total = Math.min(data.totalWorkflows, max);
2340
+ for (const template of data.workflows) {
2341
+ if (ids.length >= max) break;
2342
+ if (template.price && template.price > 0) {
2343
+ progress.skippedPaid++;
2344
+ continue;
2345
+ }
2346
+ ids.push(template.id);
2347
+ }
2348
+ if (data.workflows.length < PAGE_SIZE) break;
2349
+ page++;
2350
+ await new Promise((resolve) => setTimeout(resolve, DELAY_BETWEEN_FETCHES_MS));
2351
+ }
2352
+ return ids;
2353
+ }
2354
+ async processTemplate(id, progress) {
2355
+ const url = `${N8N_TEMPLATE_API}/workflows/${id}`;
2356
+ const response = await fetch(url);
2357
+ if (!response.ok) return;
2358
+ const data = await response.json();
2359
+ const templateMeta = data.workflow;
2360
+ const rawWorkflow = templateMeta.workflow;
2361
+ if (!rawWorkflow?.nodes?.length) return;
2362
+ const workflow = {
2363
+ name: templateMeta.name,
2364
+ nodes: rawWorkflow.nodes.filter((n) => n.type && n.name),
2365
+ connections: rawWorkflow.connections,
2366
+ settings: rawWorkflow.settings ? { executionOrder: "v1", ...rawWorkflow.settings } : { ...DEFAULT_SETTINGS }
2367
+ };
2368
+ const validation = this.validator.validate(workflow);
2369
+ const validationErrors = validation.issues.filter((i) => i.severity === "error");
2370
+ if (validationErrors.length > 0) {
2371
+ progress.blocked++;
2372
+ this.logger.debug(`Template ${id} blocked: ${validationErrors.length} validation errors`);
2373
+ return;
2374
+ }
2375
+ const safety = assessTemplateSafety(workflow);
2376
+ if (safety.trustLevel === "blocked") {
2377
+ progress.blocked++;
2378
+ this.logger.debug(`Template ${id} blocked: ${safety.reasons.join(", ")}`);
2379
+ return;
2380
+ }
2381
+ if (safety.trustLevel === "review") {
2382
+ progress.reviewed++;
2383
+ }
2384
+ const description = this.cleanDescription(templateMeta.description);
2385
+ const autoTags = Array.from(new Set(
2386
+ workflow.nodes.flatMap((n) => {
2387
+ const bare = n.type.split(".").pop() ?? "";
2388
+ const tags = [bare];
2389
+ if (n.type.includes("Trigger") || n.type.includes("trigger")) tags.push(`trigger:${bare}`);
2390
+ if (n.type.includes("langchain")) tags.push("ai");
2391
+ return tags;
2392
+ })
2393
+ ));
2394
+ const metadata = {
2395
+ description,
2396
+ tags: autoTags,
2397
+ sourceKind: "n8n-template",
2398
+ sourceId: String(id),
2399
+ sourceUrl: `https://n8n.io/workflows/${id}`,
2400
+ trustLevel: safety.trustLevel
2401
+ };
2402
+ await this.library.save(workflow, metadata);
2403
+ progress.saved++;
2404
+ this.logger.debug(`Template ${id} saved: "${templateMeta.name}" (${safety.trustLevel})`);
2405
+ }
2406
+ cleanDescription(raw) {
2407
+ return raw.replace(/#{1,6}\s*/g, "").replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\n{3,}/g, "\n\n").trim().slice(0, 500);
1433
2408
  }
1434
2409
  };
1435
2410
  // Annotate the CommonJS export names for ESM import in node:
@@ -1450,7 +2425,14 @@ var FileLibrary = class {
1450
2425
  ProviderError,
1451
2426
  ResponseParseError,
1452
2427
  TelemetryCollector,
2428
+ TelemetryReader,
2429
+ TemplateSyncer,
1453
2430
  ValidationError,
1454
- nullLogger
2431
+ buildSearchCorpus,
2432
+ clusterWorkflows,
2433
+ hybridScore,
2434
+ nullLogger,
2435
+ rerank,
2436
+ tokenize
1455
2437
  });
1456
2438
  //# sourceMappingURL=index.cjs.map