@kairos-sdk/core 0.1.1 → 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.
@@ -15,6 +15,8 @@ var NullLibrary = class {
15
15
  }
16
16
  async recordDeployment(_id) {
17
17
  }
18
+ async recordOutcome(_id, _outcome) {
19
+ }
18
20
  async get(_id) {
19
21
  return null;
20
22
  }
@@ -29,6 +31,9 @@ var KairosError = class extends Error {
29
31
  super(message);
30
32
  this.cause = cause;
31
33
  this.name = "KairosError";
34
+ if (Error.captureStackTrace) {
35
+ Error.captureStackTrace(this, this.constructor);
36
+ }
32
37
  }
33
38
  cause;
34
39
  };
@@ -51,8 +56,34 @@ var ProviderError = class extends KairosError {
51
56
  }
52
57
  };
53
58
 
59
+ // src/utils/retry.ts
60
+ async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
61
+ let lastError;
62
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
63
+ if (attempt > 0) {
64
+ const jitter = Math.random() * delayMs * 0.5;
65
+ await new Promise((resolve) => setTimeout(resolve, delayMs * 2 ** (attempt - 1) + jitter));
66
+ }
67
+ try {
68
+ return await fn();
69
+ } catch (err) {
70
+ lastError = err;
71
+ if (shouldRetry && !shouldRetry(err)) throw err;
72
+ }
73
+ }
74
+ throw lastError;
75
+ }
76
+ function fetchWithTimeout(url, init, timeoutMs) {
77
+ const controller = new AbortController();
78
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
79
+ return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
80
+ }
81
+
54
82
  // src/providers/n8n/api-client.ts
55
83
  var EXECUTION_LIMIT_CAP = 100;
84
+ var REQUEST_TIMEOUT_MS = 3e4;
85
+ var RETRY_ATTEMPTS = 3;
86
+ var RETRY_DELAY_MS = 1e3;
56
87
  var N8nApiClient = class {
57
88
  constructor(baseUrl, apiKey, logger) {
58
89
  this.baseUrl = baseUrl;
@@ -65,9 +96,21 @@ var N8nApiClient = class {
65
96
  async request(method, path, body) {
66
97
  const url = `${this.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
67
98
  this.logger.debug(`n8n ${method} ${path}`);
99
+ const isSafe = method === "GET";
100
+ if (!isSafe) {
101
+ return this.singleRequest(url, method, path, body);
102
+ }
103
+ return withRetry(
104
+ () => this.singleRequest(url, method, path, body),
105
+ RETRY_ATTEMPTS,
106
+ RETRY_DELAY_MS,
107
+ (err) => err instanceof ProviderError || err instanceof ApiError && err.statusCode === 429
108
+ );
109
+ }
110
+ async singleRequest(url, method, path, body) {
68
111
  let response;
69
112
  try {
70
- response = await fetch(url, {
113
+ response = await fetchWithTimeout(url, {
71
114
  method,
72
115
  headers: {
73
116
  "X-N8N-API-KEY": this.apiKey,
@@ -75,7 +118,7 @@ var N8nApiClient = class {
75
118
  Accept: "application/json"
76
119
  },
77
120
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
78
- });
121
+ }, REQUEST_TIMEOUT_MS);
79
122
  } catch (err) {
80
123
  throw new ProviderError(`Network error calling n8n API: ${path}`, err);
81
124
  }
@@ -109,15 +152,24 @@ var N8nApiClient = class {
109
152
  return this.request("GET", `/workflows/${id}`);
110
153
  }
111
154
  async listWorkflows() {
112
- const response = await this.request("GET", "/workflows?limit=250");
113
- return response.data.map((w) => ({
114
- id: w.id,
115
- name: w.name,
116
- active: w.active,
117
- createdAt: w.createdAt,
118
- updatedAt: w.updatedAt,
119
- ...w.tags !== void 0 ? { tags: w.tags } : {}
120
- }));
155
+ const all = [];
156
+ let path = "/workflows?limit=250";
157
+ for (; ; ) {
158
+ const response = await this.request("GET", path);
159
+ for (const w of response.data) {
160
+ all.push({
161
+ id: w.id,
162
+ name: w.name,
163
+ active: w.active,
164
+ createdAt: w.createdAt,
165
+ updatedAt: w.updatedAt,
166
+ ...w.tags !== void 0 ? { tags: w.tags } : {}
167
+ });
168
+ }
169
+ if (!response.nextCursor) break;
170
+ path = `/workflows?limit=250&cursor=${response.nextCursor}`;
171
+ }
172
+ return all;
121
173
  }
122
174
  async deleteWorkflow(id) {
123
175
  await this.request("DELETE", `/workflows/${id}`);
@@ -144,8 +196,17 @@ var N8nApiClient = class {
144
196
  return { ...this.mapExecution(response), data: response.data, workflowData: response.workflowData };
145
197
  }
146
198
  async listTags() {
147
- const response = await this.request("GET", "/tags");
148
- return response.data.map((t) => ({ id: t.id, name: t.name }));
199
+ const all = [];
200
+ let path = "/tags?limit=250";
201
+ for (; ; ) {
202
+ const response = await this.request("GET", path);
203
+ for (const t of response.data) {
204
+ all.push({ id: t.id, name: t.name });
205
+ }
206
+ if (!response.nextCursor) break;
207
+ path = `/tags?limit=250&cursor=${response.nextCursor}`;
208
+ }
209
+ return all;
149
210
  }
150
211
  async createTag(name) {
151
212
  const response = await this.request("POST", "/tags", { name });
@@ -184,7 +245,8 @@ var FORBIDDEN_ON_CREATE = [
184
245
  "active",
185
246
  "pinData",
186
247
  "triggerCount",
187
- "shared"
248
+ "shared",
249
+ "staticData"
188
250
  ];
189
251
  var FORBIDDEN_ON_UPDATE = FORBIDDEN_ON_CREATE.filter((f) => f !== "id");
190
252
 
@@ -343,6 +405,8 @@ var DEFAULT_REGISTRY = [
343
405
  { 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: [] },
344
406
  { type: "@n8n/n8n-nodes-langchain.chainLlm", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5], requiredParams: [] },
345
407
  { type: "@n8n/n8n-nodes-langchain.chainRetrievalQa", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4], requiredParams: [] },
408
+ { 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" },
409
+ { type: "@n8n/n8n-nodes-langchain.anthropic", safeTypeVersions: [1], requiredParams: [], credentialType: "anthropicApi" },
346
410
  { type: "@n8n/n8n-nodes-langchain.informationExtractor", safeTypeVersions: [1], requiredParams: [] },
347
411
  { type: "@n8n/n8n-nodes-langchain.textClassifier", safeTypeVersions: [1], requiredParams: [] },
348
412
  // AI / LangChain sub-nodes (models)
@@ -353,7 +417,8 @@ var DEFAULT_REGISTRY = [
353
417
  { type: "@n8n/n8n-nodes-langchain.memoryBufferWindow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
354
418
  { type: "@n8n/n8n-nodes-langchain.toolWorkflow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
355
419
  { type: "@n8n/n8n-nodes-langchain.toolCode", safeTypeVersions: [1, 1.1], requiredParams: [] },
356
- { type: "@n8n/n8n-nodes-langchain.toolHttpRequest", safeTypeVersions: [1, 1.1], requiredParams: [] }
420
+ { type: "@n8n/n8n-nodes-langchain.toolHttpRequest", safeTypeVersions: [1, 1.1], requiredParams: [] },
421
+ { type: "@n8n/n8n-nodes-langchain.toolCalculator", safeTypeVersions: [1], requiredParams: [] }
357
422
  ];
358
423
  var NodeRegistry = class {
359
424
  byType;
@@ -366,11 +431,17 @@ var NodeRegistry = class {
366
431
  isTrigger(type) {
367
432
  return this.byType.get(type)?.isTrigger === true;
368
433
  }
434
+ isKnown(type) {
435
+ return this.byType.has(type);
436
+ }
369
437
  isVersionSafe(type, version) {
370
438
  const def = this.byType.get(type);
371
439
  if (!def) return true;
372
440
  return def.safeTypeVersions.includes(version);
373
441
  }
442
+ getRequiredParams(type) {
443
+ return this.byType.get(type)?.requiredParams ?? [];
444
+ }
374
445
  };
375
446
 
376
447
  // src/validation/validator.ts
@@ -386,7 +457,7 @@ var AI_CONNECTION_TYPES = [
386
457
  "ai_vectorStore"
387
458
  ];
388
459
  var TRIGGER_TYPE_PATTERNS = [/trigger/i, /Trigger$/];
389
- var NODE_TYPE_PATTERN = /^(@[a-z0-9-]+\/[a-z0-9-]+\.|n8n-nodes-[a-z0-9-]+\.)[a-zA-Z][a-zA-Z0-9]+$/;
460
+ var NODE_TYPE_PATTERN = /^(@[a-z0-9-]+\/[a-z0-9-]+\.|n8n-nodes-[a-z0-9-]+\.)[a-zA-Z][a-zA-Z0-9-]+$/;
390
461
  var N8nValidator = class {
391
462
  registry;
392
463
  constructor(registry = new NodeRegistry(DEFAULT_REGISTRY)) {
@@ -413,6 +484,10 @@ var N8nValidator = class {
413
484
  this.checkRule17(workflow, issues);
414
485
  this.checkRule18(workflow, issues);
415
486
  this.checkRule19(workflow, issues);
487
+ this.checkRule20(workflow, issues);
488
+ this.checkRule21(workflow, issues);
489
+ this.checkRule22(workflow, issues);
490
+ this.checkRule23(workflow, issues);
416
491
  const errors = issues.filter((i) => i.severity === "error");
417
492
  return { valid: errors.length === 0, issues };
418
493
  }
@@ -548,6 +623,7 @@ var N8nValidator = class {
548
623
  }
549
624
  }
550
625
  for (const node of w.nodes) {
626
+ if (node.type.includes("stickyNote")) continue;
551
627
  if (!this.isTriggerNode(node) && !reachable.has(node.name)) {
552
628
  this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
553
629
  }
@@ -617,7 +693,7 @@ var N8nValidator = class {
617
693
  }
618
694
  }
619
695
  }
620
- // Rule 18 (WARN): AI connections originate from sub-nodes, not the agent/chain root
696
+ // Rule 18 (ERROR): AI connections must originate from sub-nodes, not the agent/chain root
621
697
  checkRule18(w, issues) {
622
698
  if (typeof w.connections !== "object" || w.connections === null) return;
623
699
  const agentTypes = /* @__PURE__ */ new Set([
@@ -635,7 +711,7 @@ var N8nValidator = class {
635
711
  if (typeof outputs !== "object" || outputs === null) continue;
636
712
  for (const connType of AI_CONNECTION_TYPES) {
637
713
  if (connType in outputs) {
638
- this.warn(
714
+ this.err(
639
715
  issues,
640
716
  18,
641
717
  `Node "${sourceName}" uses AI connection type "${connType}" as a SOURCE \u2014 AI sub-nodes should be the source, not the agent/chain root`,
@@ -660,6 +736,111 @@ var N8nValidator = class {
660
736
  }
661
737
  }
662
738
  }
739
+ // Rule 20 (WARN): cycle detection — no node should be reachable from itself
740
+ // Exempts splitInBatches loops which are an intentional n8n pattern
741
+ checkRule20(w, issues) {
742
+ if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
743
+ const splitBatchNodes = new Set(
744
+ w.nodes.filter((n) => n.type.includes("splitInBatches")).map((n) => n.name)
745
+ );
746
+ const adj = /* @__PURE__ */ new Map();
747
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
748
+ if (typeof outputs !== "object" || outputs === null) continue;
749
+ const targets = [];
750
+ for (const portGroup of Object.values(outputs)) {
751
+ if (!Array.isArray(portGroup)) continue;
752
+ for (const conns of portGroup) {
753
+ if (!Array.isArray(conns)) continue;
754
+ for (const conn of conns) {
755
+ const t = conn;
756
+ if (typeof t?.node === "string") {
757
+ if (splitBatchNodes.has(t.node)) continue;
758
+ targets.push(t.node);
759
+ }
760
+ }
761
+ }
762
+ }
763
+ adj.set(sourceName, targets);
764
+ }
765
+ const WHITE = 0, GRAY = 1, BLACK = 2;
766
+ const color = /* @__PURE__ */ new Map();
767
+ for (const node of w.nodes) color.set(node.name, WHITE);
768
+ const dfs = (name) => {
769
+ color.set(name, GRAY);
770
+ for (const neighbor of adj.get(name) ?? []) {
771
+ const c = color.get(neighbor);
772
+ if (c === GRAY) return true;
773
+ if (c === WHITE && dfs(neighbor)) return true;
774
+ }
775
+ color.set(name, BLACK);
776
+ return false;
777
+ };
778
+ for (const node of w.nodes) {
779
+ if (color.get(node.name) === WHITE && dfs(node.name)) {
780
+ this.warn(issues, 20, "Workflow contains a connection cycle \u2014 this may cause infinite loops");
781
+ return;
782
+ }
783
+ }
784
+ }
785
+ // Rule 22 (WARN): check requiredParams from registry
786
+ checkRule22(w, issues) {
787
+ if (!Array.isArray(w.nodes)) return;
788
+ for (const node of w.nodes) {
789
+ if (typeof node.type !== "string") continue;
790
+ const required = this.registry.getRequiredParams(node.type);
791
+ if (required.length === 0) continue;
792
+ const params = node.parameters ?? {};
793
+ for (const param of required) {
794
+ const value = params[param];
795
+ if (value === void 0 || value === null || value === "") {
796
+ this.warn(
797
+ issues,
798
+ 22,
799
+ `Node "${node.name}" (${node.type}) is missing required parameter "${param}"`,
800
+ node.id
801
+ );
802
+ }
803
+ }
804
+ }
805
+ }
806
+ // Rule 23 (WARN): unknown node types not in registry
807
+ checkRule23(w, issues) {
808
+ if (!Array.isArray(w.nodes)) return;
809
+ for (const node of w.nodes) {
810
+ if (typeof node.type !== "string") continue;
811
+ if (node.type.includes("stickyNote")) continue;
812
+ if (!NODE_TYPE_PATTERN.test(node.type)) continue;
813
+ if (!this.registry.isKnown(node.type)) {
814
+ this.warn(
815
+ issues,
816
+ 23,
817
+ `Node "${node.name}" uses unknown type "${node.type}" \u2014 it may not exist in n8n`,
818
+ node.id
819
+ );
820
+ }
821
+ }
822
+ }
823
+ // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
824
+ checkRule21(w, issues) {
825
+ if (!Array.isArray(w.nodes)) return;
826
+ const webhooksNeedingResponse = w.nodes.filter((n) => {
827
+ if (!n.type.includes("webhook")) return false;
828
+ const params = n.parameters;
829
+ return params?.responseMode === "responseNode";
830
+ });
831
+ if (webhooksNeedingResponse.length === 0) return;
832
+ const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
833
+ if (!hasRespondNode) {
834
+ for (const wh of webhooksNeedingResponse) {
835
+ this.warn(
836
+ issues,
837
+ 21,
838
+ `Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
839
+ wh.id
840
+ );
841
+ }
842
+ }
843
+ }
663
844
  };
664
845
 
665
846
  // src/errors/generation-error.ts
@@ -692,27 +873,125 @@ var ValidationError = class extends KairosError {
692
873
  import { appendFile, mkdir } from "fs/promises";
693
874
  import { join } from "path";
694
875
  import { homedir } from "os";
876
+
877
+ // src/telemetry/types.ts
878
+ var TELEMETRY_SCHEMA_VERSION = 2;
879
+
880
+ // src/telemetry/collector.ts
695
881
  var TelemetryCollector = class {
696
882
  dir;
697
883
  sessionId;
884
+ dirReady = null;
698
885
  constructor(dir) {
699
886
  this.dir = dir ?? join(homedir(), ".kairos", "telemetry");
700
887
  this.sessionId = generateUUID();
701
888
  }
702
889
  async emit(eventType, data) {
703
890
  const event = {
891
+ schemaVersion: TELEMETRY_SCHEMA_VERSION,
704
892
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
705
893
  sessionId: this.sessionId,
706
894
  eventType,
707
895
  data
708
896
  };
709
- await mkdir(this.dir, { recursive: true });
897
+ if (!this.dirReady) {
898
+ this.dirReady = mkdir(this.dir, { recursive: true }).then(() => {
899
+ });
900
+ }
901
+ await this.dirReady;
710
902
  const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
711
903
  const filepath = join(this.dir, filename);
712
904
  await appendFile(filepath, JSON.stringify(event) + "\n", "utf-8");
713
905
  }
714
906
  };
715
907
 
908
+ // src/telemetry/reader.ts
909
+ import { readFile, readdir } from "fs/promises";
910
+ import { join as join2 } from "path";
911
+ import { homedir as homedir2 } from "os";
912
+ var TelemetryReader = class {
913
+ dir;
914
+ cache = null;
915
+ cacheTime = 0;
916
+ constructor(dir) {
917
+ this.dir = dir ?? join2(homedir2(), ".kairos", "telemetry");
918
+ }
919
+ async getFailureRates(days = 30) {
920
+ const now = Date.now();
921
+ if (this.cache && now - this.cacheTime < 5 * 60 * 1e3) {
922
+ return this.cache;
923
+ }
924
+ const events = await this.readRecentEvents(days);
925
+ const buildSessions = new Set(
926
+ events.filter((e) => e.eventType === "build_complete" && !e.data.dryRun).map((e) => e.sessionId)
927
+ );
928
+ if (buildSessions.size === 0) return [];
929
+ const ruleSessions = /* @__PURE__ */ new Map();
930
+ for (const event of events) {
931
+ if (event.eventType !== "generation_attempt") continue;
932
+ if (!buildSessions.has(event.sessionId)) continue;
933
+ const data = event.data;
934
+ if (data.validationPassed || !data.issues) continue;
935
+ for (const issue of data.issues) {
936
+ const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
937
+ entry.sessions.add(event.sessionId);
938
+ entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
939
+ ruleSessions.set(issue.rule, entry);
940
+ }
941
+ }
942
+ const rates = [];
943
+ for (const [rule, entry] of ruleSessions) {
944
+ let topMessage = "";
945
+ let topCount = 0;
946
+ for (const [msg, count] of entry.messages) {
947
+ if (count > topCount) {
948
+ topMessage = msg;
949
+ topCount = count;
950
+ }
951
+ }
952
+ rates.push({
953
+ rule,
954
+ failureCount: entry.sessions.size,
955
+ totalBuilds: buildSessions.size,
956
+ rate: entry.sessions.size / buildSessions.size,
957
+ commonMessage: topMessage
958
+ });
959
+ }
960
+ rates.sort((a, b) => b.rate - a.rate);
961
+ this.cache = rates;
962
+ this.cacheTime = now;
963
+ return rates;
964
+ }
965
+ async readRecentEvents(days) {
966
+ let files;
967
+ try {
968
+ files = await readdir(this.dir);
969
+ } catch {
970
+ return [];
971
+ }
972
+ const cutoff = /* @__PURE__ */ new Date();
973
+ cutoff.setDate(cutoff.getDate() - days);
974
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
975
+ const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
976
+ const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr).sort();
977
+ const events = [];
978
+ for (const file of recentFiles) {
979
+ try {
980
+ const content = await readFile(join2(this.dir, file), "utf-8");
981
+ for (const line of content.split("\n")) {
982
+ if (!line.trim()) continue;
983
+ try {
984
+ events.push(JSON.parse(line));
985
+ } catch {
986
+ }
987
+ }
988
+ } catch {
989
+ }
990
+ }
991
+ return events;
992
+ }
993
+ };
994
+
716
995
  // src/utils/logger.ts
717
996
  var nullLogger = {
718
997
  debug() {
@@ -747,7 +1026,7 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
747
1026
  "saveDataErrorExecution": "all",
748
1027
  "saveDataSuccessExecution": "all",
749
1028
  "executionTimeout": 3600,
750
- "timezone": "America/New_York",
1029
+ "timezone": "UTC",
751
1030
  "executionOrder": "v1"
752
1031
  }
753
1032
  }
@@ -789,9 +1068,21 @@ Every AI Agent must have at least one ai_languageModel sub-node connected.
789
1068
  ### IF node \u2014 two output ports (0 = true, 1 = false):
790
1069
  "IF Check": { "main": [ [{ "node": "True Path", "type": "main", "index": 0 }], [{ "node": "False Path", "type": "main", "index": 0 }] ] }
791
1070
 
1071
+ ### SplitInBatches \u2014 two output ports (0 = done/finished, 1 = loop body per batch):
1072
+ Connect output 0 to the node that runs AFTER all batches complete.
1073
+ Connect output 1 to the processing chain for each batch. The last node in the chain loops back to SplitInBatches via main input.
1074
+
1075
+ ### Webhook + RespondToWebhook pattern:
1076
+ When webhook responseMode is "responseNode", you MUST include a respondToWebhook node in the flow.
1077
+ "Webhook": { "main": [[{ "node": "Process Data", "type": "main", "index": 0 }]] }
1078
+ "Process Data": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] }
1079
+
792
1080
  ### Triggers have no incoming connections.
793
1081
  ### Connection keys are NODE NAMES, never node IDs.
794
1082
 
1083
+ ### Nested parameters:
1084
+ Node parameters like conditions, assignments, and rule intervals MUST include all required nested fields. Do not leave nested objects empty or partially filled.
1085
+
795
1086
  ---
796
1087
 
797
1088
  ## NODE CATALOG \u2014 exact type strings and safe typeVersions
@@ -855,16 +1146,16 @@ n8n-nodes-base.redis typeVersion: 1 \u2014 cred: redis
855
1146
  n8n-nodes-base.supabase typeVersion: 1 \u2014 cred: supabaseApi
856
1147
  n8n-nodes-base.awsS3 typeVersion: 2 \u2014 cred: aws
857
1148
 
858
- ### AI \u2014 Root nodes (sit on main data flow):
1149
+ ### AI \u2014 Root nodes (sit on main data flow, receive ai_* connections as TARGETS):
859
1150
  @n8n/n8n-nodes-langchain.agent typeVersion: 1.9 \u2014 params: promptType, text (if define), options.systemMessage
860
1151
  @n8n/n8n-nodes-langchain.chainLlm typeVersion: 1.5
861
1152
  @n8n/n8n-nodes-langchain.chainRetrievalQa typeVersion: 1.4
862
- @n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi
863
- @n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi
1153
+ @n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi \u2014 standalone node, calls OpenAI directly without sub-nodes
1154
+ @n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi \u2014 standalone node, calls Anthropic directly without sub-nodes
864
1155
 
865
- ### AI \u2014 Sub-nodes (sources of ai_* connections):
866
- @n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel
867
- @n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel
1156
+ ### AI \u2014 Sub-nodes (sources of ai_* connections, wire INTO root nodes above):
1157
+ @n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
1158
+ @n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
868
1159
  @n8n/n8n-nodes-langchain.lmChatGoogleGemini typeVersion: 1 \u2014 cred: googlePalmApi \u2014 ai_languageModel
869
1160
  @n8n/n8n-nodes-langchain.memoryBufferWindow typeVersion: 1.3 \u2014 \u2014 ai_memory
870
1161
  @n8n/n8n-nodes-langchain.toolWorkflow typeVersion: 2 \u2014 \u2014 ai_tool
@@ -899,11 +1190,42 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
899
1190
  Respond ONLY with a generate_workflow tool call. No prose. No markdown outside the tool call.
900
1191
  If the request is impossible or unclear, set the error field instead of generating a workflow.`;
901
1192
 
1193
+ // src/utils/thresholds.ts
1194
+ var DIRECT_THRESHOLD = 0.92;
1195
+ var REFERENCE_THRESHOLD = 0.72;
1196
+ function scoreToMode(score) {
1197
+ if (score >= DIRECT_THRESHOLD) return "direct";
1198
+ if (score >= REFERENCE_THRESHOLD) return "reference";
1199
+ return "scratch";
1200
+ }
1201
+
902
1202
  // src/generation/prompt-builder.ts
1203
+ var RULE_REMEDIES = {
1204
+ 1: "Provide a non-empty workflow name string",
1205
+ 2: "Include at least one node in the nodes array",
1206
+ 3: "Every node must have a unique UUID v4 string as its id field",
1207
+ 4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
1208
+ 5: "Every node must have a non-empty type string",
1209
+ 6: "Every node must have a positive integer typeVersion",
1210
+ 7: "Every node must have a position array of exactly [x, y] numbers",
1211
+ 8: "Every node must have a non-empty name string",
1212
+ 9: "connections must be a plain object (use {} if no connections)",
1213
+ 10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
1214
+ 12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
1215
+ 14: "Include at least one trigger node (e.g. webhook, scheduleTrigger, manualTrigger)",
1216
+ 15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1217
+ 16: "All node names must be unique within the workflow",
1218
+ 17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
1219
+ 18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
1220
+ 19: "Use known safe typeVersion values for each node type",
1221
+ 20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1222
+ 21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1223
+ 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
1224
+ };
903
1225
  var PromptBuilder = class {
904
- build(request, matches) {
1226
+ build(request, matches, globalFailureRates = []) {
905
1227
  const mode = this.resolveMode(matches);
906
- const system = this.buildSystem(matches, mode);
1228
+ const system = this.buildSystem(matches, mode, globalFailureRates);
907
1229
  const userMessage = this.buildUserMessage(request, matches, mode);
908
1230
  return { system, userMessage, mode, matches };
909
1231
  }
@@ -920,11 +1242,9 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
920
1242
  if (matches.length === 0) return "scratch";
921
1243
  const top = matches[0];
922
1244
  if (!top) return "scratch";
923
- if (top.score >= 0.92) return "direct";
924
- if (top.score >= 0.72) return "reference";
925
- return "scratch";
1245
+ return scoreToMode(top.score);
926
1246
  }
927
- buildSystem(matches, mode) {
1247
+ buildSystem(matches, mode, globalFailureRates = []) {
928
1248
  const blocks = [
929
1249
  {
930
1250
  type: "text",
@@ -948,15 +1268,63 @@ ${refText}`
948
1268
  }
949
1269
  if (mode === "direct" && matches[0]) {
950
1270
  const match = matches[0];
1271
+ const json = JSON.stringify(match.workflow.workflow, null, 2);
1272
+ if (json.length > 3e4) {
1273
+ const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1274
+ blocks.push({
1275
+ type: "text",
1276
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1277
+ Nodes:
1278
+ ${nodes}`
1279
+ });
1280
+ } else {
1281
+ blocks.push({
1282
+ type: "text",
1283
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1284
+
1285
+ ${json}`
1286
+ });
1287
+ }
1288
+ }
1289
+ if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1290
+ const hint = matches[0];
1291
+ const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
951
1292
  blocks.push({
952
1293
  type: "text",
953
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
954
-
955
- ${JSON.stringify(match.workflow.workflow, null, 2)}`
1294
+ text: `## Weak Structural Hint
1295
+ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
956
1296
  });
957
1297
  }
1298
+ const warnings = this.buildFailureWarnings(matches, globalFailureRates);
1299
+ if (warnings) {
1300
+ blocks.push({ type: "text", text: warnings });
1301
+ }
958
1302
  return blocks;
959
1303
  }
1304
+ buildFailureWarnings(matches, globalFailureRates) {
1305
+ const lines = [];
1306
+ for (const match of matches) {
1307
+ const patterns = match.workflow.failurePatterns;
1308
+ if (!patterns?.length) continue;
1309
+ for (const fp of patterns) {
1310
+ const remedy = RULE_REMEDIES[fp.rule];
1311
+ const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1312
+ lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
1313
+ }
1314
+ }
1315
+ const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
1316
+ for (const rule of highFreqRules) {
1317
+ const remedy = RULE_REMEDIES[rule.rule];
1318
+ const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1319
+ lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
1320
+ }
1321
+ if (lines.length === 0) return null;
1322
+ const unique = [...new Set(lines)];
1323
+ return `## Known Failure Patterns \u2014 AVOID THESE
1324
+
1325
+ Previous builds frequently failed the following validation rules. Ensure your output does NOT repeat these mistakes:
1326
+ ${unique.join("\n")}`;
1327
+ }
960
1328
  buildUserMessage(request, _matches, _mode) {
961
1329
  const namePart = request.name ? `
962
1330
  Workflow name: "${request.name}"` : "";
@@ -1003,7 +1371,7 @@ var GENERATE_WORKFLOW_TOOL = {
1003
1371
  description: "Set this if the request cannot be fulfilled \u2014 explain why"
1004
1372
  }
1005
1373
  },
1006
- required: ["workflow"]
1374
+ required: []
1007
1375
  }
1008
1376
  };
1009
1377
  var WorkflowDesigner = class {
@@ -1019,24 +1387,24 @@ var WorkflowDesigner = class {
1019
1387
  logger;
1020
1388
  validator;
1021
1389
  promptBuilder;
1022
- async design(request, matches) {
1023
- const allIssues = [];
1390
+ async design(request, matches, globalFailureRates = []) {
1024
1391
  const attemptMetadata = [];
1392
+ let lastErrors = [];
1025
1393
  let attempts = 0;
1394
+ const built = this.promptBuilder.build(request, matches, globalFailureRates);
1026
1395
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
1027
1396
  attempts = attempt;
1028
1397
  const temperature = attempt === MAX_ATTEMPTS ? FINAL_TEMPERATURE : BASE_TEMPERATURE;
1029
- const built = this.promptBuilder.build(request, matches);
1030
1398
  let userMessage;
1031
1399
  if (attempt === 1) {
1032
1400
  userMessage = built.userMessage;
1033
1401
  this.logger.debug("WorkflowDesigner: attempt 1", { description: request.description });
1034
1402
  } else {
1035
- const issueLines = allIssues.map(
1403
+ const issueLines = lastErrors.map(
1036
1404
  (i) => `- [Rule ${i.rule}] ${i.message}${i.nodeId ? ` (node: ${i.nodeId})` : ""}`
1037
1405
  );
1038
1406
  userMessage = this.promptBuilder.buildCorrectionMessage(request, matches, issueLines, attempt - 1);
1039
- this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: allIssues.length });
1407
+ this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: lastErrors.length });
1040
1408
  }
1041
1409
  const start = Date.now();
1042
1410
  const message = await this.callClaude(built.system, userMessage, temperature);
@@ -1054,35 +1422,43 @@ var WorkflowDesigner = class {
1054
1422
  tokensInput: message.usage.input_tokens,
1055
1423
  tokensOutput: message.usage.output_tokens,
1056
1424
  validationPassed: validation.valid,
1057
- issues: errors
1425
+ issues: validation.issues
1058
1426
  });
1059
1427
  if (validation.valid) {
1060
1428
  return { workflow: parsed.workflow, credentialsNeeded: parsed.credentialsNeeded, attempts, attemptMetadata };
1061
1429
  }
1062
- allIssues.push(...errors);
1430
+ lastErrors = errors;
1063
1431
  this.logger.warn(`WorkflowDesigner: validation failed on attempt ${attempt}`, {
1064
- newErrors: errors.length,
1065
- totalErrors: allIssues.length
1432
+ errorCount: errors.length
1066
1433
  });
1067
1434
  }
1435
+ const finalIssues = attemptMetadata.at(-1)?.issues ?? lastErrors;
1068
1436
  throw new ValidationError(
1069
- `Workflow failed validation after ${MAX_ATTEMPTS} attempts (${allIssues.length} total errors)`,
1070
- allIssues
1437
+ `Workflow failed validation after ${MAX_ATTEMPTS} attempts`,
1438
+ finalIssues
1071
1439
  );
1072
1440
  }
1073
1441
  async callClaude(system, userMessage, temperature) {
1442
+ const controller = new AbortController();
1443
+ const timer = setTimeout(() => controller.abort(), 12e4);
1074
1444
  try {
1075
- return await this.anthropic.messages.create({
1076
- model: this.model,
1077
- max_tokens: 8192,
1078
- temperature,
1079
- system,
1080
- messages: [{ role: "user", content: userMessage }],
1081
- tools: [GENERATE_WORKFLOW_TOOL],
1082
- tool_choice: { type: "tool", name: "generate_workflow" }
1083
- });
1445
+ return await this.anthropic.messages.create(
1446
+ {
1447
+ model: this.model,
1448
+ max_tokens: 8192,
1449
+ temperature,
1450
+ system: system.map((b) => ({ type: b.type, text: b.text, ...b.cache_control ? { cache_control: b.cache_control } : {} })),
1451
+ messages: [{ role: "user", content: userMessage }],
1452
+ tools: [GENERATE_WORKFLOW_TOOL],
1453
+ tool_choice: { type: "tool", name: "generate_workflow" }
1454
+ },
1455
+ { signal: controller.signal }
1456
+ );
1084
1457
  } catch (err) {
1085
- throw new GenerationError("Anthropic API call failed", err);
1458
+ const detail = err instanceof Error ? err.message : String(err);
1459
+ throw new GenerationError(`Anthropic API call failed: ${detail}`, err);
1460
+ } finally {
1461
+ clearTimeout(timer);
1086
1462
  }
1087
1463
  }
1088
1464
  extractToolUse(message) {
@@ -1120,27 +1496,53 @@ var Kairos = class {
1120
1496
  library;
1121
1497
  logger;
1122
1498
  telemetry;
1499
+ telemetryReader;
1123
1500
  model;
1501
+ saveQueue = Promise.resolve(null);
1124
1502
  constructor(options) {
1125
1503
  const logger = options.logger ?? nullLogger;
1126
1504
  this.model = options.model ?? DEFAULT_MODEL;
1505
+ if (options.n8nBaseUrl && options.n8nApiKey) {
1506
+ try {
1507
+ new URL(options.n8nBaseUrl);
1508
+ } catch {
1509
+ throw new GuardError(`Invalid n8nBaseUrl: "${options.n8nBaseUrl}" \u2014 must be a valid URL`);
1510
+ }
1511
+ const apiClient = new N8nApiClient(options.n8nBaseUrl, options.n8nApiKey, logger);
1512
+ const stripper = new N8nFieldStripper();
1513
+ this.provider = new N8nProvider(apiClient, stripper);
1514
+ } else {
1515
+ this.provider = null;
1516
+ }
1127
1517
  const anthropic = new Anthropic({ apiKey: options.anthropicApiKey });
1128
- const apiClient = new N8nApiClient(options.n8nBaseUrl, options.n8nApiKey, logger);
1129
- const stripper = new N8nFieldStripper();
1130
- this.provider = new N8nProvider(apiClient, stripper);
1131
1518
  this.designer = new WorkflowDesigner(anthropic, this.model, logger);
1132
1519
  this.validator = new N8nValidator();
1133
1520
  this.library = options.library ?? new NullLibrary();
1134
1521
  this.logger = logger;
1135
1522
  if (options.telemetry === true) {
1136
1523
  this.telemetry = new TelemetryCollector();
1524
+ this.telemetryReader = new TelemetryReader();
1137
1525
  } else if (typeof options.telemetry === "string") {
1138
1526
  this.telemetry = new TelemetryCollector(options.telemetry);
1527
+ this.telemetryReader = new TelemetryReader(options.telemetry);
1139
1528
  } else {
1140
1529
  this.telemetry = null;
1530
+ this.telemetryReader = null;
1531
+ }
1532
+ }
1533
+ requireProvider() {
1534
+ if (!this.provider) {
1535
+ 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");
1536
+ }
1537
+ return this.provider;
1538
+ }
1539
+ validateDescription(description) {
1540
+ if (!description || description.trim().length === 0) {
1541
+ throw new GuardError("Description is required and must be non-empty");
1141
1542
  }
1142
1543
  }
1143
1544
  async build(description, options) {
1545
+ this.validateDescription(description);
1144
1546
  this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
1145
1547
  const buildStart = Date.now();
1146
1548
  await this.telemetry?.emit("build_start", {
@@ -1150,33 +1552,28 @@ var Kairos = class {
1150
1552
  });
1151
1553
  await this.library.initialize();
1152
1554
  const matches = await this.library.search(description);
1555
+ if (matches.length > 0) {
1556
+ const top = matches[0];
1557
+ this.logger.info(`Library: ${matches.length} match(es), top="${top.workflow.description.slice(0, 50)}" score=${top.score.toFixed(2)} mode=${top.mode}`);
1558
+ } else {
1559
+ this.logger.info("Library: no matches (scratch mode)");
1560
+ }
1561
+ const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
1562
+ if (globalFailureRates.length > 0) {
1563
+ const highFreq = globalFailureRates.filter((r) => r.rate >= 0.15);
1564
+ if (highFreq.length > 0) {
1565
+ this.logger.info(`Telemetry: ${highFreq.length} high-frequency failure rule(s) will be warned about`);
1566
+ }
1567
+ }
1153
1568
  const designResult = await this.designer.design(
1154
1569
  { description, ...options?.name ? { name: options.name } : {} },
1155
- matches
1570
+ matches,
1571
+ globalFailureRates
1156
1572
  );
1157
- for (const meta of designResult.attemptMetadata) {
1158
- await this.telemetry?.emit("generation_attempt", {
1159
- description,
1160
- attempt: meta.attempt,
1161
- temperature: meta.temperature,
1162
- durationMs: meta.durationMs,
1163
- tokensInput: meta.tokensInput,
1164
- tokensOutput: meta.tokensOutput,
1165
- validationPassed: meta.validationPassed,
1166
- issueCount: meta.issues.length,
1167
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message }))
1168
- });
1169
- }
1573
+ await this.emitAttemptTelemetry(description, designResult);
1170
1574
  const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
1575
+ this.saveToLibrary(workflow, description, designResult, matches);
1171
1576
  if (options?.dryRun) {
1172
- const result2 = {
1173
- workflowId: null,
1174
- name: workflow.name,
1175
- credentialsNeeded: designResult.credentialsNeeded,
1176
- activationRequired: true,
1177
- generationAttempts: designResult.attempts,
1178
- dryRun: true
1179
- };
1180
1577
  const totalTokensInput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
1181
1578
  const totalTokensOutput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
1182
1579
  await this.telemetry?.emit("build_complete", {
@@ -1191,23 +1588,64 @@ var Kairos = class {
1191
1588
  dryRun: true,
1192
1589
  credentialsNeeded: designResult.credentialsNeeded.length
1193
1590
  });
1194
- return result2;
1591
+ return {
1592
+ workflowId: null,
1593
+ name: workflow.name,
1594
+ workflow,
1595
+ credentialsNeeded: designResult.credentialsNeeded,
1596
+ activationRequired: true,
1597
+ generationAttempts: designResult.attempts,
1598
+ dryRun: true
1599
+ };
1195
1600
  }
1196
- const deployed = await this.provider.deploy(workflow);
1197
- this.library.save(workflow, { description }).catch((err) => {
1198
- this.logger.warn("Failed to save workflow to library (non-fatal)", { err: String(err) });
1199
- });
1601
+ const provider = this.requireProvider();
1602
+ const deployed = await provider.deploy(workflow);
1603
+ this.recordDeploy();
1200
1604
  if (options?.activate) {
1201
- await this.provider.activate(deployed.workflowId);
1605
+ await provider.activate(deployed.workflowId);
1202
1606
  }
1203
- const result = {
1607
+ const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
1608
+ const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
1609
+ await this.telemetry?.emit("build_complete", {
1610
+ description,
1611
+ success: true,
1612
+ totalAttempts: designResult.attempts,
1613
+ totalDurationMs: Date.now() - buildStart,
1614
+ totalTokensInput,
1615
+ totalTokensOutput,
1616
+ workflowName: deployed.name,
1617
+ workflowId: deployed.workflowId,
1618
+ dryRun: false,
1619
+ credentialsNeeded: designResult.credentialsNeeded.length
1620
+ });
1621
+ return {
1204
1622
  workflowId: deployed.workflowId,
1205
1623
  name: deployed.name,
1624
+ workflow,
1206
1625
  credentialsNeeded: designResult.credentialsNeeded,
1207
1626
  activationRequired: !options?.activate,
1208
1627
  generationAttempts: designResult.attempts,
1209
1628
  dryRun: false
1210
1629
  };
1630
+ }
1631
+ async replace(id, description) {
1632
+ this.validateDescription(description);
1633
+ this.logger.info("Kairos.update", { id, description });
1634
+ const buildStart = Date.now();
1635
+ await this.telemetry?.emit("build_start", {
1636
+ description,
1637
+ model: this.model,
1638
+ dryRun: false
1639
+ });
1640
+ await this.library.initialize();
1641
+ const matches = await this.library.search(description);
1642
+ const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
1643
+ const designResult = await this.designer.design({ description }, matches, globalFailureRates);
1644
+ await this.emitAttemptTelemetry(description, designResult);
1645
+ const provider = this.requireProvider();
1646
+ const deployed = await provider.update(id, designResult.workflow);
1647
+ this.saveToLibrary(designResult.workflow, description, designResult, matches);
1648
+ this.recordDeploy();
1211
1649
  const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
1212
1650
  const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
1213
1651
  await this.telemetry?.emit("build_complete", {
@@ -1222,59 +1660,693 @@ var Kairos = class {
1222
1660
  dryRun: false,
1223
1661
  credentialsNeeded: designResult.credentialsNeeded.length
1224
1662
  });
1225
- return result;
1226
- }
1227
- async update(id, description) {
1228
- this.logger.info("Kairos.update", { id, description });
1229
- const matches = await this.library.search(description);
1230
- const designResult = await this.designer.design({ description }, matches);
1231
- const deployed = await this.provider.update(id, designResult.workflow);
1232
1663
  return {
1233
1664
  workflowId: deployed.workflowId,
1234
1665
  name: deployed.name,
1666
+ workflow: designResult.workflow,
1235
1667
  credentialsNeeded: designResult.credentialsNeeded,
1236
1668
  activationRequired: true,
1237
1669
  generationAttempts: designResult.attempts,
1238
1670
  dryRun: false
1239
1671
  };
1240
1672
  }
1673
+ async drain() {
1674
+ await this.saveQueue.catch(() => {
1675
+ });
1676
+ }
1677
+ async emitAttemptTelemetry(description, designResult) {
1678
+ for (const meta of designResult.attemptMetadata) {
1679
+ await this.telemetry?.emit("generation_attempt", {
1680
+ description,
1681
+ attempt: meta.attempt,
1682
+ temperature: meta.temperature,
1683
+ durationMs: meta.durationMs,
1684
+ tokensInput: meta.tokensInput,
1685
+ tokensOutput: meta.tokensOutput,
1686
+ validationPassed: meta.validationPassed,
1687
+ issueCount: meta.issues.length,
1688
+ issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message }))
1689
+ });
1690
+ }
1691
+ }
1692
+ recordDeploy() {
1693
+ this.saveQueue = this.saveQueue.then(async (savedId) => {
1694
+ if (savedId) {
1695
+ await this.library.recordDeployment(savedId);
1696
+ }
1697
+ return savedId;
1698
+ }).catch((err) => {
1699
+ this.logger.warn("Failed to record deployment (non-fatal)", { err: String(err) });
1700
+ return null;
1701
+ });
1702
+ }
1703
+ saveToLibrary(workflow, description, designResult, matches) {
1704
+ const failedAttempts = designResult.attemptMetadata.filter((m) => !m.validationPassed);
1705
+ const failurePatterns = failedAttempts.flatMap(
1706
+ (m) => m.issues.map((i) => ({ rule: i.rule, message: i.message }))
1707
+ );
1708
+ const topMatch = matches[0];
1709
+ const generationMode = topMatch ? scoreToMode(topMatch.score) : "scratch";
1710
+ const autoTags = Array.from(new Set(
1711
+ workflow.nodes.flatMap((n) => {
1712
+ const bare = n.type.split(".").pop() ?? "";
1713
+ const tags = [bare];
1714
+ if (n.type.includes("Trigger") || n.type.includes("trigger")) tags.push(`trigger:${bare}`);
1715
+ if (n.type.includes("langchain")) tags.push("ai");
1716
+ return tags;
1717
+ })
1718
+ ));
1719
+ const metadata = {
1720
+ description,
1721
+ generationMode,
1722
+ generationAttempts: designResult.attempts
1723
+ };
1724
+ if (autoTags.length > 0) metadata.tags = autoTags;
1725
+ if (failurePatterns.length > 0) metadata.failurePatterns = failurePatterns;
1726
+ if (matches.length > 0) metadata.sourceWorkflowIds = matches.map((m) => m.workflow.id);
1727
+ if (topMatch) metadata.topMatchScore = topMatch.score;
1728
+ if (designResult.credentialsNeeded.length > 0) metadata.credentialsNeeded = designResult.credentialsNeeded;
1729
+ const firstTryPass = designResult.attemptMetadata.length > 0 && designResult.attemptMetadata[0].validationPassed;
1730
+ const failedRules = Array.from(new Set(
1731
+ designResult.attemptMetadata.filter((m) => !m.validationPassed).flatMap((m) => m.issues.map((i) => i.rule))
1732
+ ));
1733
+ this.saveQueue = this.saveQueue.then(async () => {
1734
+ const savedId = await this.library.save(workflow, metadata);
1735
+ for (const match of matches) {
1736
+ if (match.mode === "direct" || match.mode === "reference") {
1737
+ await this.library.recordOutcome(match.workflow.id, {
1738
+ attempts: designResult.attempts,
1739
+ firstTryPass,
1740
+ failedRules,
1741
+ mode: match.mode
1742
+ });
1743
+ }
1744
+ }
1745
+ return savedId;
1746
+ }).catch((err) => {
1747
+ this.logger.warn("Failed to save workflow to library (non-fatal)", { err: String(err) });
1748
+ return null;
1749
+ });
1750
+ }
1241
1751
  async get(id) {
1242
- return this.provider.get(id);
1752
+ return this.requireProvider().get(id);
1243
1753
  }
1244
1754
  async list() {
1245
- return this.provider.list();
1755
+ return this.requireProvider().list();
1246
1756
  }
1247
1757
  async activate(id) {
1248
- await this.provider.activate(id);
1758
+ await this.requireProvider().activate(id);
1249
1759
  }
1250
1760
  async deactivate(id) {
1251
- await this.provider.deactivate(id);
1761
+ await this.requireProvider().deactivate(id);
1252
1762
  }
1253
1763
  async delete(id, options) {
1254
- await this.provider.delete(id, options);
1764
+ await this.requireProvider().delete(id, options);
1255
1765
  }
1256
1766
  async executions(workflowId, filter) {
1257
- return this.provider.executions(workflowId, filter);
1767
+ return this.requireProvider().executions(workflowId, filter);
1258
1768
  }
1259
1769
  async execution(id) {
1260
- return this.provider.execution(id);
1770
+ return this.requireProvider().execution(id);
1261
1771
  }
1262
1772
  async listTags() {
1263
- return this.provider.listTags();
1773
+ return this.requireProvider().listTags();
1264
1774
  }
1265
1775
  async createTag(name) {
1266
- return this.provider.createTag(name);
1776
+ return this.requireProvider().createTag(name);
1267
1777
  }
1268
1778
  async tag(workflowId, tagIds) {
1269
- await this.provider.tag(workflowId, tagIds);
1779
+ await this.requireProvider().tag(workflowId, tagIds);
1270
1780
  }
1271
1781
  async untag(workflowId, tagIds) {
1272
- await this.provider.untag(workflowId, tagIds);
1782
+ await this.requireProvider().untag(workflowId, tagIds);
1783
+ }
1784
+ };
1785
+
1786
+ // src/library/scorer.ts
1787
+ var WEIGHTS = {
1788
+ tfidf: 0.35,
1789
+ nodeFingerprint: 0.3,
1790
+ outcome: 0.2,
1791
+ deploy: 0.15
1792
+ };
1793
+ var NODE_KEYWORDS = {
1794
+ slack: ["slack", "slackApi"],
1795
+ email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
1796
+ webhook: ["webhook", "webhookTrigger"],
1797
+ schedule: ["scheduleTrigger", "cron"],
1798
+ http: ["httpRequest"],
1799
+ sheets: ["googleSheets"],
1800
+ github: ["github", "githubTrigger"],
1801
+ telegram: ["telegram", "telegramTrigger"],
1802
+ ai: ["agent", "openAi", "lmChatOpenAi", "lmChatAnthropic", "chainLlm", "chainSummarization"],
1803
+ memory: ["memoryBufferWindow", "memoryXata", "memoryPostgres"],
1804
+ vector: ["vectorStoreInMemory", "vectorStorePinecone", "vectorStoreQdrant"],
1805
+ database: ["postgres", "mySql", "redis", "mongoDb"],
1806
+ airtable: ["airtable"],
1807
+ notion: ["notion"],
1808
+ s3: ["awsS3"],
1809
+ code: ["code"],
1810
+ merge: ["merge"],
1811
+ switch: ["switch"],
1812
+ if: ["if"],
1813
+ wait: ["wait"],
1814
+ rss: ["rssFeedRead", "rssFeedReadTrigger"],
1815
+ form: ["formTrigger"],
1816
+ set: ["set"],
1817
+ split: ["splitInBatches"],
1818
+ filter: ["filter"],
1819
+ telegram_trigger: ["telegramTrigger"],
1820
+ stripe: ["stripe"]
1821
+ };
1822
+ function extractQueryFingerprint(description) {
1823
+ const lower = description.toLowerCase();
1824
+ const matches = /* @__PURE__ */ new Set();
1825
+ for (const [keyword, nodeTypes] of Object.entries(NODE_KEYWORDS)) {
1826
+ if (lower.includes(keyword)) {
1827
+ for (const nt of nodeTypes) matches.add(nt);
1828
+ }
1829
+ }
1830
+ if (/\bevery\b|\bdaily\b|\bhourly\b|\bweekly\b|\bmonthly\b|\bcron\b|\bschedule\b|\bat \d/.test(lower)) {
1831
+ matches.add("scheduleTrigger");
1832
+ }
1833
+ if (/\bwebhook\b|\breceive\b.*\bpost\b|\bpost\b.*\brequest\b/.test(lower)) {
1834
+ matches.add("webhook");
1835
+ }
1836
+ if (/\bchat\b|\bchatbot\b|\bconversation\b/.test(lower)) {
1837
+ matches.add("chatTrigger");
1838
+ }
1839
+ if (/\bai\b|\bllm\b|\bgpt\b|\bclaude\b|\bagent\b|\bsummariz/.test(lower)) {
1840
+ matches.add("agent");
1841
+ }
1842
+ return matches;
1843
+ }
1844
+ function extractWorkflowFingerprint(w) {
1845
+ const fp = /* @__PURE__ */ new Set();
1846
+ for (const node of w.workflow.nodes) {
1847
+ const bare = node.type.split(".").pop() ?? "";
1848
+ fp.add(bare);
1849
+ }
1850
+ return fp;
1851
+ }
1852
+ function jaccardSimilarity(a, b) {
1853
+ if (a.size === 0 && b.size === 0) return 0;
1854
+ let intersection = 0;
1855
+ for (const item of a) {
1856
+ if (b.has(item)) intersection++;
1857
+ }
1858
+ const union = a.size + b.size - intersection;
1859
+ return union > 0 ? intersection / union : 0;
1860
+ }
1861
+ function outcomeScore(w) {
1862
+ const stats = w.outcomeStats;
1863
+ if (!stats || stats.totalUses === 0) return 0.5;
1864
+ const passRate = stats.firstTryPasses / stats.totalUses;
1865
+ const avgAttempts = stats.totalAttempts / stats.totalUses;
1866
+ const attemptPenalty = Math.max(0, 1 - (avgAttempts - 1) * 0.3);
1867
+ return passRate * 0.6 + attemptPenalty * 0.4;
1868
+ }
1869
+ function deployScore(w) {
1870
+ return 1 + Math.log(w.deployCount + 1) * 0.1;
1871
+ }
1872
+ function hybridScore(queryTokens, queryDescription, workflows, docTokenArrays, idf) {
1873
+ const queryFp = extractQueryFingerprint(queryDescription);
1874
+ const ceiling = queryTokens.reduce((sum, qt) => sum + (idf.get(qt) ?? 0), 0) || 1;
1875
+ return workflows.map((w, i) => {
1876
+ const docTokens = docTokenArrays[i];
1877
+ let tfidfRaw = 0;
1878
+ const docFreq = /* @__PURE__ */ new Map();
1879
+ for (const t of docTokens) {
1880
+ docFreq.set(t, (docFreq.get(t) ?? 0) + 1);
1881
+ }
1882
+ for (const qt of queryTokens) {
1883
+ const tf = docTokens.length > 0 ? (docFreq.get(qt) ?? 0) / docTokens.length : 0;
1884
+ const idfVal = idf.get(qt) ?? 0;
1885
+ tfidfRaw += tf * idfVal;
1886
+ }
1887
+ const tfidf = Math.min(tfidfRaw / ceiling, 1);
1888
+ const workflowFp = extractWorkflowFingerprint(w);
1889
+ const nodeFingerprint = queryFp.size > 0 ? jaccardSimilarity(queryFp, workflowFp) : 0;
1890
+ const outcome = outcomeScore(w);
1891
+ const deploy = Math.min(deployScore(w), 1.5) / 1.5;
1892
+ const score = Math.min(
1893
+ WEIGHTS.tfidf * tfidf + WEIGHTS.nodeFingerprint * nodeFingerprint + WEIGHTS.outcome * outcome + WEIGHTS.deploy * deploy,
1894
+ 1
1895
+ );
1896
+ return {
1897
+ workflow: w,
1898
+ score,
1899
+ signals: { tfidf, nodeFingerprint, outcome, deploy }
1900
+ };
1901
+ });
1902
+ }
1903
+
1904
+ // src/library/cluster.ts
1905
+ function getFingerprint(w) {
1906
+ return w.workflow.nodes.map((n) => n.type.split(".").pop() ?? "").sort();
1907
+ }
1908
+ function fingerprintKey(fp) {
1909
+ return fp.join("|");
1910
+ }
1911
+ function describePattern(fp) {
1912
+ const triggers = fp.filter((n) => /trigger/i.test(n));
1913
+ const outputs = fp.filter((n) => /slack|gmail|email|telegram|sheets|airtable|notion/i.test(n));
1914
+ const ai = fp.filter((n) => /agent|openai|anthropic|chain|memory/i.test(n));
1915
+ const core = fp.filter((n) => /httpRequest|code|merge|switch|if|set|filter/i.test(n));
1916
+ const parts = [];
1917
+ if (triggers.length > 0) parts.push(triggers[0]);
1918
+ if (ai.length > 0) parts.push("AI");
1919
+ if (core.length > 0) parts.push(core.slice(0, 2).join("+"));
1920
+ if (outputs.length > 0) parts.push(outputs[0]);
1921
+ return parts.length > 0 ? parts.join(" \u2192 ") : fp.slice(0, 3).join(" \u2192 ");
1922
+ }
1923
+ function clusterWorkflows(workflows) {
1924
+ const groups = /* @__PURE__ */ new Map();
1925
+ for (const w of workflows) {
1926
+ const fp = getFingerprint(w);
1927
+ const key = fingerprintKey(fp);
1928
+ const existing = groups.get(key);
1929
+ if (existing) {
1930
+ existing.push(w);
1931
+ } else {
1932
+ groups.set(key, [w]);
1933
+ }
1934
+ }
1935
+ const clusters = [];
1936
+ for (const [, members] of groups) {
1937
+ if (members.length === 0) continue;
1938
+ const fp = getFingerprint(members[0]);
1939
+ const withStats = members.filter((m) => m.outcomeStats && m.outcomeStats.totalUses > 0);
1940
+ let avgFirstTryPassRate = 0;
1941
+ let avgAttempts = 0;
1942
+ if (withStats.length > 0) {
1943
+ avgFirstTryPassRate = withStats.reduce((sum, m) => {
1944
+ const s = m.outcomeStats;
1945
+ return sum + s.firstTryPasses / s.totalUses;
1946
+ }, 0) / withStats.length;
1947
+ avgAttempts = withStats.reduce((sum, m) => {
1948
+ const s = m.outcomeStats;
1949
+ return sum + s.totalAttempts / s.totalUses;
1950
+ }, 0) / withStats.length;
1951
+ }
1952
+ const ruleCounts = /* @__PURE__ */ new Map();
1953
+ let totalFailureInstances = 0;
1954
+ for (const m of withStats) {
1955
+ const rules = m.outcomeStats.failedRules;
1956
+ for (const [rule, count] of Object.entries(rules)) {
1957
+ const r = parseInt(rule, 10);
1958
+ ruleCounts.set(r, (ruleCounts.get(r) ?? 0) + count);
1959
+ totalFailureInstances += count;
1960
+ }
1961
+ }
1962
+ const commonFailedRules = [...ruleCounts.entries()].map(([rule, count]) => ({
1963
+ rule,
1964
+ frequency: totalFailureInstances > 0 ? count / totalFailureInstances : 0
1965
+ })).filter((r) => r.frequency >= 0.1).sort((a, b) => b.frequency - a.frequency);
1966
+ clusters.push({
1967
+ pattern: describePattern(fp),
1968
+ fingerprint: fp,
1969
+ members,
1970
+ avgFirstTryPassRate,
1971
+ avgAttempts,
1972
+ commonFailedRules
1973
+ });
1974
+ }
1975
+ return clusters.sort((a, b) => b.members.length - a.members.length);
1976
+ }
1977
+ function rerank(candidates, clusters) {
1978
+ const clusterMap = /* @__PURE__ */ new Map();
1979
+ for (const cluster of clusters) {
1980
+ for (const member of cluster.members) {
1981
+ clusterMap.set(member.id, cluster);
1982
+ }
1983
+ }
1984
+ return candidates.map((c) => {
1985
+ const cluster = clusterMap.get(c.workflow.id);
1986
+ let boost = 0;
1987
+ if (cluster && cluster.avgFirstTryPassRate > 0) {
1988
+ boost = (cluster.avgFirstTryPassRate - 0.5) * 0.1;
1989
+ }
1990
+ if (cluster && cluster.commonFailedRules.length > 0) {
1991
+ boost -= cluster.commonFailedRules.length * 0.02;
1992
+ }
1993
+ return {
1994
+ workflow: c.workflow,
1995
+ score: Math.max(0, Math.min(1, c.score + boost)),
1996
+ ...cluster ? { clusterPattern: cluster.pattern } : {}
1997
+ };
1998
+ }).sort((a, b) => b.score - a.score);
1999
+ }
2000
+
2001
+ // src/library/file-library.ts
2002
+ import { readFile as readFile2, writeFile, rename, mkdir as mkdir2 } from "fs/promises";
2003
+ import { join as join3 } from "path";
2004
+ import { homedir as homedir3 } from "os";
2005
+ function tokenize(text) {
2006
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2);
2007
+ }
2008
+ function buildSearchCorpus(w) {
2009
+ const nodeTokens = w.workflow.nodes.map((n) => {
2010
+ const bare = n.type.split(".").pop() ?? "";
2011
+ const spaced = bare.replace(/([A-Z])/g, " $1").trim().toLowerCase();
2012
+ return `${bare} ${spaced}`;
2013
+ });
2014
+ return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
2015
+ }
2016
+ var MAX_LIBRARY_SIZE = 500;
2017
+ var FileLibrary = class {
2018
+ dir;
2019
+ workflows = [];
2020
+ initPromise = null;
2021
+ writeQueue = Promise.resolve();
2022
+ constructor(dir) {
2023
+ this.dir = dir ?? join3(homedir3(), ".kairos", "library");
2024
+ }
2025
+ async initialize() {
2026
+ if (!this.initPromise) {
2027
+ this.initPromise = this.doInitialize();
2028
+ }
2029
+ return this.initPromise;
2030
+ }
2031
+ async doInitialize() {
2032
+ await mkdir2(this.dir, { recursive: true });
2033
+ const indexPath = join3(this.dir, "index.json");
2034
+ try {
2035
+ const raw = await readFile2(indexPath, "utf-8");
2036
+ const parsed = JSON.parse(raw);
2037
+ if (!Array.isArray(parsed)) {
2038
+ this.workflows = [];
2039
+ } else {
2040
+ this.workflows = parsed.filter(
2041
+ (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)
2042
+ );
2043
+ }
2044
+ } catch {
2045
+ this.workflows = [];
2046
+ }
2047
+ }
2048
+ async search(description, options) {
2049
+ const searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
2050
+ if (searchable.length === 0) return [];
2051
+ const limit = options?.limit ?? 3;
2052
+ const queryTokens = tokenize(description);
2053
+ if (queryTokens.length === 0) return [];
2054
+ const docTokenArrays = searchable.map((w) => tokenize(buildSearchCorpus(w)));
2055
+ const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
2056
+ const docCount = searchable.length;
2057
+ const idf = /* @__PURE__ */ new Map();
2058
+ const allTokens = new Set(queryTokens);
2059
+ for (const token of allTokens) {
2060
+ const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
2061
+ idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
2062
+ }
2063
+ const scored = hybridScore(queryTokens, description, searchable, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
2064
+ const clusters = clusterWorkflows(searchable);
2065
+ const reranked = rerank(scored, clusters).slice(0, limit);
2066
+ const results = reranked.map((m) => {
2067
+ return { workflow: m.workflow, score: m.score, mode: scoreToMode(m.score) };
2068
+ });
2069
+ if (results.length > 0) {
2070
+ for (const r of results) {
2071
+ r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
2072
+ }
2073
+ this.persist();
2074
+ }
2075
+ return results;
2076
+ }
2077
+ async save(workflow, metadata) {
2078
+ const id = generateUUID();
2079
+ const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
2080
+ const stored = {
2081
+ id,
2082
+ workflow,
2083
+ description: metadata.description,
2084
+ tags: metadata.tags ?? [],
2085
+ platform: metadata.platform ?? "n8n",
2086
+ deployCount: 0,
2087
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2088
+ ...failurePatterns?.length ? { failurePatterns } : {},
2089
+ ...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
2090
+ ...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
2091
+ ...metadata.topMatchScore != null ? { topMatchScore: metadata.topMatchScore } : {},
2092
+ ...metadata.generationAttempts != null ? { generationAttempts: metadata.generationAttempts } : {},
2093
+ ...metadata.credentialsNeeded?.length ? { credentialsNeeded: metadata.credentialsNeeded } : {},
2094
+ ...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
2095
+ ...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
2096
+ ...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
2097
+ ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
2098
+ };
2099
+ this.workflows.push(stored);
2100
+ if (this.workflows.length > MAX_LIBRARY_SIZE) {
2101
+ this.workflows.sort((a, b) => (b.deployCount ?? 1) - (a.deployCount ?? 1));
2102
+ this.workflows = this.workflows.slice(0, MAX_LIBRARY_SIZE);
2103
+ }
2104
+ await this.persist();
2105
+ return id;
2106
+ }
2107
+ async recordDeployment(id) {
2108
+ const w = this.workflows.find((w2) => w2.id === id);
2109
+ if (w) {
2110
+ w.deployCount++;
2111
+ w.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
2112
+ await this.persist();
2113
+ }
2114
+ }
2115
+ async recordOutcome(id, outcome) {
2116
+ const w = this.workflows.find((w2) => w2.id === id);
2117
+ if (!w) return;
2118
+ if (outcome.mode === "direct") {
2119
+ w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
2120
+ } else {
2121
+ w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
2122
+ }
2123
+ const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
2124
+ stats.totalUses++;
2125
+ stats.totalAttempts += outcome.attempts;
2126
+ if (outcome.firstTryPass) stats.firstTryPasses++;
2127
+ for (const rule of outcome.failedRules) {
2128
+ const key = String(rule);
2129
+ stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
2130
+ }
2131
+ w.outcomeStats = stats;
2132
+ await this.persist();
2133
+ }
2134
+ async drain() {
2135
+ await this.writeQueue;
2136
+ }
2137
+ async get(id) {
2138
+ return this.workflows.find((w) => w.id === id) ?? null;
2139
+ }
2140
+ async list(filters) {
2141
+ let result = this.workflows;
2142
+ if (filters?.platform) {
2143
+ result = result.filter((w) => w.platform === filters.platform);
2144
+ }
2145
+ if (filters?.tags && filters.tags.length > 0) {
2146
+ result = result.filter((w) => filters.tags.some((t) => w.tags.includes(t)));
2147
+ }
2148
+ return result;
2149
+ }
2150
+ deduplicateFailurePatterns(patterns) {
2151
+ if (!patterns?.length) return void 0;
2152
+ const map = /* @__PURE__ */ new Map();
2153
+ for (const fp of patterns) {
2154
+ const existing = map.get(fp.rule);
2155
+ if (existing) {
2156
+ existing.occurrences++;
2157
+ } else {
2158
+ map.set(fp.rule, { rule: fp.rule, message: fp.message, occurrences: 1 });
2159
+ }
2160
+ }
2161
+ return [...map.values()];
2162
+ }
2163
+ persist() {
2164
+ this.writeQueue = this.writeQueue.then(async () => {
2165
+ const indexPath = join3(this.dir, "index.json");
2166
+ const tmpPath = `${indexPath}.tmp`;
2167
+ await writeFile(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
2168
+ await rename(tmpPath, indexPath);
2169
+ });
2170
+ return this.writeQueue;
2171
+ }
2172
+ };
2173
+
2174
+ // src/templates/safety.ts
2175
+ var BLOCKED_NODE_TYPES = /* @__PURE__ */ new Set([
2176
+ "n8n-nodes-base.code",
2177
+ "n8n-nodes-base.executeCommand",
2178
+ "n8n-nodes-base.ssh"
2179
+ ]);
2180
+ var REVIEW_NODE_TYPES = /* @__PURE__ */ new Set([
2181
+ "n8n-nodes-base.httpRequest"
2182
+ ]);
2183
+ var SECRET_PATTERNS = [
2184
+ /sk-[a-zA-Z0-9]{20,}/,
2185
+ /ghp_[a-zA-Z0-9]{36}/,
2186
+ /xoxb-[0-9]+-[0-9]+-[a-zA-Z0-9]+/,
2187
+ /AIza[a-zA-Z0-9_-]{35}/,
2188
+ /AKIA[A-Z0-9]{16}/
2189
+ ];
2190
+ function assessTemplateSafety(workflow) {
2191
+ const reasons = [];
2192
+ let worst = "safe";
2193
+ const escalate = (level, reason) => {
2194
+ reasons.push(reason);
2195
+ if (level === "blocked") worst = "blocked";
2196
+ else if (level === "review" && worst === "safe") worst = "review";
2197
+ };
2198
+ for (const node of workflow.nodes) {
2199
+ if (BLOCKED_NODE_TYPES.has(node.type)) {
2200
+ escalate("blocked", `Contains ${node.type} node "${node.name}"`);
2201
+ }
2202
+ if (REVIEW_NODE_TYPES.has(node.type)) {
2203
+ escalate("review", `Contains ${node.type} node "${node.name}"`);
2204
+ }
2205
+ const paramStr = JSON.stringify(node.parameters);
2206
+ for (const pattern of SECRET_PATTERNS) {
2207
+ if (pattern.test(paramStr)) {
2208
+ escalate("blocked", `Node "${node.name}" parameters contain a hardcoded secret`);
2209
+ break;
2210
+ }
2211
+ }
2212
+ }
2213
+ return { trustLevel: worst, reasons };
2214
+ }
2215
+
2216
+ // src/templates/syncer.ts
2217
+ var N8N_TEMPLATE_API = "https://api.n8n.io/api/templates";
2218
+ var PAGE_SIZE = 50;
2219
+ var DELAY_BETWEEN_FETCHES_MS = 200;
2220
+ var DEFAULT_SETTINGS = {
2221
+ executionOrder: "v1",
2222
+ saveManualExecutions: true,
2223
+ timezone: "UTC"
2224
+ };
2225
+ var TemplateSyncer = class {
2226
+ constructor(library, logger) {
2227
+ this.library = library;
2228
+ this.validator = new N8nValidator();
2229
+ this.logger = logger;
2230
+ }
2231
+ library;
2232
+ validator;
2233
+ logger;
2234
+ async sync(options) {
2235
+ const maxTemplates = options?.maxTemplates ?? 500;
2236
+ await this.library.initialize();
2237
+ const existing = await this.library.list();
2238
+ const existingSourceIds = new Set(
2239
+ existing.filter((w) => w.sourceKind === "n8n-template" && w.sourceId).map((w) => w.sourceId)
2240
+ );
2241
+ const progress = {
2242
+ total: 0,
2243
+ processed: 0,
2244
+ saved: 0,
2245
+ skippedPaid: 0,
2246
+ skippedDuplicate: 0,
2247
+ blocked: 0,
2248
+ reviewed: 0
2249
+ };
2250
+ const templateIds = await this.fetchTemplateIds(maxTemplates, progress);
2251
+ for (const id of templateIds) {
2252
+ if (existingSourceIds.has(String(id))) {
2253
+ progress.skippedDuplicate++;
2254
+ progress.processed++;
2255
+ options?.onProgress?.(progress);
2256
+ continue;
2257
+ }
2258
+ try {
2259
+ await this.processTemplate(id, progress);
2260
+ } catch (err) {
2261
+ this.logger.warn(`Failed to process template ${id}`, { err: String(err) });
2262
+ }
2263
+ progress.processed++;
2264
+ options?.onProgress?.(progress);
2265
+ await new Promise((resolve) => setTimeout(resolve, DELAY_BETWEEN_FETCHES_MS));
2266
+ }
2267
+ return progress;
2268
+ }
2269
+ async fetchTemplateIds(max, progress) {
2270
+ const ids = [];
2271
+ let page = 1;
2272
+ while (ids.length < max) {
2273
+ const url = `${N8N_TEMPLATE_API}/search?page=${page}&rows=${PAGE_SIZE}`;
2274
+ const response = await fetch(url);
2275
+ if (!response.ok) break;
2276
+ const data = await response.json();
2277
+ progress.total = Math.min(data.totalWorkflows, max);
2278
+ for (const template of data.workflows) {
2279
+ if (ids.length >= max) break;
2280
+ if (template.price && template.price > 0) {
2281
+ progress.skippedPaid++;
2282
+ continue;
2283
+ }
2284
+ ids.push(template.id);
2285
+ }
2286
+ if (data.workflows.length < PAGE_SIZE) break;
2287
+ page++;
2288
+ await new Promise((resolve) => setTimeout(resolve, DELAY_BETWEEN_FETCHES_MS));
2289
+ }
2290
+ return ids;
2291
+ }
2292
+ async processTemplate(id, progress) {
2293
+ const url = `${N8N_TEMPLATE_API}/workflows/${id}`;
2294
+ const response = await fetch(url);
2295
+ if (!response.ok) return;
2296
+ const data = await response.json();
2297
+ const templateMeta = data.workflow;
2298
+ const rawWorkflow = templateMeta.workflow;
2299
+ if (!rawWorkflow?.nodes?.length) return;
2300
+ const workflow = {
2301
+ name: templateMeta.name,
2302
+ nodes: rawWorkflow.nodes.filter((n) => n.type && n.name),
2303
+ connections: rawWorkflow.connections,
2304
+ settings: rawWorkflow.settings ? { executionOrder: "v1", ...rawWorkflow.settings } : { ...DEFAULT_SETTINGS }
2305
+ };
2306
+ const validation = this.validator.validate(workflow);
2307
+ const validationErrors = validation.issues.filter((i) => i.severity === "error");
2308
+ if (validationErrors.length > 0) {
2309
+ progress.blocked++;
2310
+ this.logger.debug(`Template ${id} blocked: ${validationErrors.length} validation errors`);
2311
+ return;
2312
+ }
2313
+ const safety = assessTemplateSafety(workflow);
2314
+ if (safety.trustLevel === "blocked") {
2315
+ progress.blocked++;
2316
+ this.logger.debug(`Template ${id} blocked: ${safety.reasons.join(", ")}`);
2317
+ return;
2318
+ }
2319
+ if (safety.trustLevel === "review") {
2320
+ progress.reviewed++;
2321
+ }
2322
+ const description = this.cleanDescription(templateMeta.description);
2323
+ const autoTags = Array.from(new Set(
2324
+ workflow.nodes.flatMap((n) => {
2325
+ const bare = n.type.split(".").pop() ?? "";
2326
+ const tags = [bare];
2327
+ if (n.type.includes("Trigger") || n.type.includes("trigger")) tags.push(`trigger:${bare}`);
2328
+ if (n.type.includes("langchain")) tags.push("ai");
2329
+ return tags;
2330
+ })
2331
+ ));
2332
+ const metadata = {
2333
+ description,
2334
+ tags: autoTags,
2335
+ sourceKind: "n8n-template",
2336
+ sourceId: String(id),
2337
+ sourceUrl: `https://n8n.io/workflows/${id}`,
2338
+ trustLevel: safety.trustLevel
2339
+ };
2340
+ await this.library.save(workflow, metadata);
2341
+ progress.saved++;
2342
+ this.logger.debug(`Template ${id} saved: "${templateMeta.name}" (${safety.trustLevel})`);
2343
+ }
2344
+ cleanDescription(raw) {
2345
+ 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);
1273
2346
  }
1274
2347
  };
1275
2348
 
1276
2349
  export {
1277
- generateUUID,
1278
2350
  NullLibrary,
1279
2351
  KairosError,
1280
2352
  ApiError,
@@ -1290,7 +2362,15 @@ export {
1290
2362
  ResponseParseError,
1291
2363
  ValidationError,
1292
2364
  TelemetryCollector,
2365
+ TelemetryReader,
1293
2366
  nullLogger,
1294
- Kairos
2367
+ Kairos,
2368
+ hybridScore,
2369
+ clusterWorkflows,
2370
+ rerank,
2371
+ tokenize,
2372
+ buildSearchCorpus,
2373
+ FileLibrary,
2374
+ TemplateSyncer
1295
2375
  };
1296
- //# sourceMappingURL=chunk-IADOKKFO.js.map
2376
+ //# sourceMappingURL=chunk-QQJDLS5A.js.map