@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/cli.cjs CHANGED
@@ -43,6 +43,8 @@ var NullLibrary = class {
43
43
  }
44
44
  async recordDeployment(_id) {
45
45
  }
46
+ async recordOutcome(_id, _outcome) {
47
+ }
46
48
  async get(_id) {
47
49
  return null;
48
50
  }
@@ -57,6 +59,9 @@ var KairosError = class extends Error {
57
59
  super(message);
58
60
  this.cause = cause;
59
61
  this.name = "KairosError";
62
+ if (Error.captureStackTrace) {
63
+ Error.captureStackTrace(this, this.constructor);
64
+ }
60
65
  }
61
66
  cause;
62
67
  };
@@ -79,8 +84,34 @@ var ProviderError = class extends KairosError {
79
84
  }
80
85
  };
81
86
 
87
+ // src/utils/retry.ts
88
+ async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
89
+ let lastError;
90
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
91
+ if (attempt > 0) {
92
+ const jitter = Math.random() * delayMs * 0.5;
93
+ await new Promise((resolve) => setTimeout(resolve, delayMs * 2 ** (attempt - 1) + jitter));
94
+ }
95
+ try {
96
+ return await fn();
97
+ } catch (err) {
98
+ lastError = err;
99
+ if (shouldRetry && !shouldRetry(err)) throw err;
100
+ }
101
+ }
102
+ throw lastError;
103
+ }
104
+ function fetchWithTimeout(url, init, timeoutMs) {
105
+ const controller = new AbortController();
106
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
107
+ return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
108
+ }
109
+
82
110
  // src/providers/n8n/api-client.ts
83
111
  var EXECUTION_LIMIT_CAP = 100;
112
+ var REQUEST_TIMEOUT_MS = 3e4;
113
+ var RETRY_ATTEMPTS = 3;
114
+ var RETRY_DELAY_MS = 1e3;
84
115
  var N8nApiClient = class {
85
116
  constructor(baseUrl, apiKey, logger) {
86
117
  this.baseUrl = baseUrl;
@@ -93,9 +124,21 @@ var N8nApiClient = class {
93
124
  async request(method, path, body) {
94
125
  const url = `${this.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
95
126
  this.logger.debug(`n8n ${method} ${path}`);
127
+ const isSafe = method === "GET";
128
+ if (!isSafe) {
129
+ return this.singleRequest(url, method, path, body);
130
+ }
131
+ return withRetry(
132
+ () => this.singleRequest(url, method, path, body),
133
+ RETRY_ATTEMPTS,
134
+ RETRY_DELAY_MS,
135
+ (err) => err instanceof ProviderError || err instanceof ApiError && err.statusCode === 429
136
+ );
137
+ }
138
+ async singleRequest(url, method, path, body) {
96
139
  let response;
97
140
  try {
98
- response = await fetch(url, {
141
+ response = await fetchWithTimeout(url, {
99
142
  method,
100
143
  headers: {
101
144
  "X-N8N-API-KEY": this.apiKey,
@@ -103,7 +146,7 @@ var N8nApiClient = class {
103
146
  Accept: "application/json"
104
147
  },
105
148
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
106
- });
149
+ }, REQUEST_TIMEOUT_MS);
107
150
  } catch (err) {
108
151
  throw new ProviderError(`Network error calling n8n API: ${path}`, err);
109
152
  }
@@ -137,15 +180,24 @@ var N8nApiClient = class {
137
180
  return this.request("GET", `/workflows/${id}`);
138
181
  }
139
182
  async listWorkflows() {
140
- const response = await this.request("GET", "/workflows?limit=250");
141
- return response.data.map((w) => ({
142
- id: w.id,
143
- name: w.name,
144
- active: w.active,
145
- createdAt: w.createdAt,
146
- updatedAt: w.updatedAt,
147
- ...w.tags !== void 0 ? { tags: w.tags } : {}
148
- }));
183
+ const all = [];
184
+ let path = "/workflows?limit=250";
185
+ for (; ; ) {
186
+ const response = await this.request("GET", path);
187
+ for (const w of response.data) {
188
+ all.push({
189
+ id: w.id,
190
+ name: w.name,
191
+ active: w.active,
192
+ createdAt: w.createdAt,
193
+ updatedAt: w.updatedAt,
194
+ ...w.tags !== void 0 ? { tags: w.tags } : {}
195
+ });
196
+ }
197
+ if (!response.nextCursor) break;
198
+ path = `/workflows?limit=250&cursor=${response.nextCursor}`;
199
+ }
200
+ return all;
149
201
  }
150
202
  async deleteWorkflow(id) {
151
203
  await this.request("DELETE", `/workflows/${id}`);
@@ -172,8 +224,17 @@ var N8nApiClient = class {
172
224
  return { ...this.mapExecution(response), data: response.data, workflowData: response.workflowData };
173
225
  }
174
226
  async listTags() {
175
- const response = await this.request("GET", "/tags");
176
- return response.data.map((t) => ({ id: t.id, name: t.name }));
227
+ const all = [];
228
+ let path = "/tags?limit=250";
229
+ for (; ; ) {
230
+ const response = await this.request("GET", path);
231
+ for (const t of response.data) {
232
+ all.push({ id: t.id, name: t.name });
233
+ }
234
+ if (!response.nextCursor) break;
235
+ path = `/tags?limit=250&cursor=${response.nextCursor}`;
236
+ }
237
+ return all;
177
238
  }
178
239
  async createTag(name) {
179
240
  const response = await this.request("POST", "/tags", { name });
@@ -212,7 +273,8 @@ var FORBIDDEN_ON_CREATE = [
212
273
  "active",
213
274
  "pinData",
214
275
  "triggerCount",
215
- "shared"
276
+ "shared",
277
+ "staticData"
216
278
  ];
217
279
  var FORBIDDEN_ON_UPDATE = FORBIDDEN_ON_CREATE.filter((f) => f !== "id");
218
280
 
@@ -371,6 +433,8 @@ var DEFAULT_REGISTRY = [
371
433
  { 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: [] },
372
434
  { type: "@n8n/n8n-nodes-langchain.chainLlm", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5], requiredParams: [] },
373
435
  { type: "@n8n/n8n-nodes-langchain.chainRetrievalQa", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4], requiredParams: [] },
436
+ { 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" },
437
+ { type: "@n8n/n8n-nodes-langchain.anthropic", safeTypeVersions: [1], requiredParams: [], credentialType: "anthropicApi" },
374
438
  { type: "@n8n/n8n-nodes-langchain.informationExtractor", safeTypeVersions: [1], requiredParams: [] },
375
439
  { type: "@n8n/n8n-nodes-langchain.textClassifier", safeTypeVersions: [1], requiredParams: [] },
376
440
  // AI / LangChain sub-nodes (models)
@@ -381,7 +445,8 @@ var DEFAULT_REGISTRY = [
381
445
  { type: "@n8n/n8n-nodes-langchain.memoryBufferWindow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
382
446
  { type: "@n8n/n8n-nodes-langchain.toolWorkflow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
383
447
  { type: "@n8n/n8n-nodes-langchain.toolCode", safeTypeVersions: [1, 1.1], requiredParams: [] },
384
- { type: "@n8n/n8n-nodes-langchain.toolHttpRequest", safeTypeVersions: [1, 1.1], requiredParams: [] }
448
+ { type: "@n8n/n8n-nodes-langchain.toolHttpRequest", safeTypeVersions: [1, 1.1], requiredParams: [] },
449
+ { type: "@n8n/n8n-nodes-langchain.toolCalculator", safeTypeVersions: [1], requiredParams: [] }
385
450
  ];
386
451
  var NodeRegistry = class {
387
452
  byType;
@@ -394,11 +459,17 @@ var NodeRegistry = class {
394
459
  isTrigger(type) {
395
460
  return this.byType.get(type)?.isTrigger === true;
396
461
  }
462
+ isKnown(type) {
463
+ return this.byType.has(type);
464
+ }
397
465
  isVersionSafe(type, version) {
398
466
  const def = this.byType.get(type);
399
467
  if (!def) return true;
400
468
  return def.safeTypeVersions.includes(version);
401
469
  }
470
+ getRequiredParams(type) {
471
+ return this.byType.get(type)?.requiredParams ?? [];
472
+ }
402
473
  };
403
474
 
404
475
  // src/validation/validator.ts
@@ -414,7 +485,7 @@ var AI_CONNECTION_TYPES = [
414
485
  "ai_vectorStore"
415
486
  ];
416
487
  var TRIGGER_TYPE_PATTERNS = [/trigger/i, /Trigger$/];
417
- var NODE_TYPE_PATTERN = /^(@[a-z0-9-]+\/[a-z0-9-]+\.|n8n-nodes-[a-z0-9-]+\.)[a-zA-Z][a-zA-Z0-9]+$/;
488
+ var NODE_TYPE_PATTERN = /^(@[a-z0-9-]+\/[a-z0-9-]+\.|n8n-nodes-[a-z0-9-]+\.)[a-zA-Z][a-zA-Z0-9-]+$/;
418
489
  var N8nValidator = class {
419
490
  registry;
420
491
  constructor(registry = new NodeRegistry(DEFAULT_REGISTRY)) {
@@ -441,6 +512,10 @@ var N8nValidator = class {
441
512
  this.checkRule17(workflow, issues);
442
513
  this.checkRule18(workflow, issues);
443
514
  this.checkRule19(workflow, issues);
515
+ this.checkRule20(workflow, issues);
516
+ this.checkRule21(workflow, issues);
517
+ this.checkRule22(workflow, issues);
518
+ this.checkRule23(workflow, issues);
444
519
  const errors = issues.filter((i) => i.severity === "error");
445
520
  return { valid: errors.length === 0, issues };
446
521
  }
@@ -576,6 +651,7 @@ var N8nValidator = class {
576
651
  }
577
652
  }
578
653
  for (const node of w.nodes) {
654
+ if (node.type.includes("stickyNote")) continue;
579
655
  if (!this.isTriggerNode(node) && !reachable.has(node.name)) {
580
656
  this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
581
657
  }
@@ -645,7 +721,7 @@ var N8nValidator = class {
645
721
  }
646
722
  }
647
723
  }
648
- // Rule 18 (WARN): AI connections originate from sub-nodes, not the agent/chain root
724
+ // Rule 18 (ERROR): AI connections must originate from sub-nodes, not the agent/chain root
649
725
  checkRule18(w, issues) {
650
726
  if (typeof w.connections !== "object" || w.connections === null) return;
651
727
  const agentTypes = /* @__PURE__ */ new Set([
@@ -663,7 +739,7 @@ var N8nValidator = class {
663
739
  if (typeof outputs !== "object" || outputs === null) continue;
664
740
  for (const connType of AI_CONNECTION_TYPES) {
665
741
  if (connType in outputs) {
666
- this.warn(
742
+ this.err(
667
743
  issues,
668
744
  18,
669
745
  `Node "${sourceName}" uses AI connection type "${connType}" as a SOURCE \u2014 AI sub-nodes should be the source, not the agent/chain root`,
@@ -688,6 +764,111 @@ var N8nValidator = class {
688
764
  }
689
765
  }
690
766
  }
767
+ // Rule 20 (WARN): cycle detection — no node should be reachable from itself
768
+ // Exempts splitInBatches loops which are an intentional n8n pattern
769
+ checkRule20(w, issues) {
770
+ if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
771
+ const splitBatchNodes = new Set(
772
+ w.nodes.filter((n) => n.type.includes("splitInBatches")).map((n) => n.name)
773
+ );
774
+ const adj = /* @__PURE__ */ new Map();
775
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
776
+ if (typeof outputs !== "object" || outputs === null) continue;
777
+ const targets = [];
778
+ for (const portGroup of Object.values(outputs)) {
779
+ if (!Array.isArray(portGroup)) continue;
780
+ for (const conns of portGroup) {
781
+ if (!Array.isArray(conns)) continue;
782
+ for (const conn of conns) {
783
+ const t = conn;
784
+ if (typeof t?.node === "string") {
785
+ if (splitBatchNodes.has(t.node)) continue;
786
+ targets.push(t.node);
787
+ }
788
+ }
789
+ }
790
+ }
791
+ adj.set(sourceName, targets);
792
+ }
793
+ const WHITE = 0, GRAY = 1, BLACK = 2;
794
+ const color = /* @__PURE__ */ new Map();
795
+ for (const node of w.nodes) color.set(node.name, WHITE);
796
+ const dfs = (name) => {
797
+ color.set(name, GRAY);
798
+ for (const neighbor of adj.get(name) ?? []) {
799
+ const c = color.get(neighbor);
800
+ if (c === GRAY) return true;
801
+ if (c === WHITE && dfs(neighbor)) return true;
802
+ }
803
+ color.set(name, BLACK);
804
+ return false;
805
+ };
806
+ for (const node of w.nodes) {
807
+ if (color.get(node.name) === WHITE && dfs(node.name)) {
808
+ this.warn(issues, 20, "Workflow contains a connection cycle \u2014 this may cause infinite loops");
809
+ return;
810
+ }
811
+ }
812
+ }
813
+ // Rule 22 (WARN): check requiredParams from registry
814
+ checkRule22(w, issues) {
815
+ if (!Array.isArray(w.nodes)) return;
816
+ for (const node of w.nodes) {
817
+ if (typeof node.type !== "string") continue;
818
+ const required = this.registry.getRequiredParams(node.type);
819
+ if (required.length === 0) continue;
820
+ const params = node.parameters ?? {};
821
+ for (const param of required) {
822
+ const value = params[param];
823
+ if (value === void 0 || value === null || value === "") {
824
+ this.warn(
825
+ issues,
826
+ 22,
827
+ `Node "${node.name}" (${node.type}) is missing required parameter "${param}"`,
828
+ node.id
829
+ );
830
+ }
831
+ }
832
+ }
833
+ }
834
+ // Rule 23 (WARN): unknown node types not in registry
835
+ checkRule23(w, issues) {
836
+ if (!Array.isArray(w.nodes)) return;
837
+ for (const node of w.nodes) {
838
+ if (typeof node.type !== "string") continue;
839
+ if (node.type.includes("stickyNote")) continue;
840
+ if (!NODE_TYPE_PATTERN.test(node.type)) continue;
841
+ if (!this.registry.isKnown(node.type)) {
842
+ this.warn(
843
+ issues,
844
+ 23,
845
+ `Node "${node.name}" uses unknown type "${node.type}" \u2014 it may not exist in n8n`,
846
+ node.id
847
+ );
848
+ }
849
+ }
850
+ }
851
+ // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
852
+ checkRule21(w, issues) {
853
+ if (!Array.isArray(w.nodes)) return;
854
+ const webhooksNeedingResponse = w.nodes.filter((n) => {
855
+ if (!n.type.includes("webhook")) return false;
856
+ const params = n.parameters;
857
+ return params?.responseMode === "responseNode";
858
+ });
859
+ if (webhooksNeedingResponse.length === 0) return;
860
+ const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
861
+ if (!hasRespondNode) {
862
+ for (const wh of webhooksNeedingResponse) {
863
+ this.warn(
864
+ issues,
865
+ 21,
866
+ `Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
867
+ wh.id
868
+ );
869
+ }
870
+ }
871
+ }
691
872
  };
692
873
 
693
874
  // src/errors/generation-error.ts
@@ -735,7 +916,7 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
735
916
  "saveDataErrorExecution": "all",
736
917
  "saveDataSuccessExecution": "all",
737
918
  "executionTimeout": 3600,
738
- "timezone": "America/New_York",
919
+ "timezone": "UTC",
739
920
  "executionOrder": "v1"
740
921
  }
741
922
  }
@@ -777,9 +958,21 @@ Every AI Agent must have at least one ai_languageModel sub-node connected.
777
958
  ### IF node \u2014 two output ports (0 = true, 1 = false):
778
959
  "IF Check": { "main": [ [{ "node": "True Path", "type": "main", "index": 0 }], [{ "node": "False Path", "type": "main", "index": 0 }] ] }
779
960
 
961
+ ### SplitInBatches \u2014 two output ports (0 = done/finished, 1 = loop body per batch):
962
+ Connect output 0 to the node that runs AFTER all batches complete.
963
+ Connect output 1 to the processing chain for each batch. The last node in the chain loops back to SplitInBatches via main input.
964
+
965
+ ### Webhook + RespondToWebhook pattern:
966
+ When webhook responseMode is "responseNode", you MUST include a respondToWebhook node in the flow.
967
+ "Webhook": { "main": [[{ "node": "Process Data", "type": "main", "index": 0 }]] }
968
+ "Process Data": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] }
969
+
780
970
  ### Triggers have no incoming connections.
781
971
  ### Connection keys are NODE NAMES, never node IDs.
782
972
 
973
+ ### Nested parameters:
974
+ Node parameters like conditions, assignments, and rule intervals MUST include all required nested fields. Do not leave nested objects empty or partially filled.
975
+
783
976
  ---
784
977
 
785
978
  ## NODE CATALOG \u2014 exact type strings and safe typeVersions
@@ -843,16 +1036,16 @@ n8n-nodes-base.redis typeVersion: 1 \u2014 cred: redis
843
1036
  n8n-nodes-base.supabase typeVersion: 1 \u2014 cred: supabaseApi
844
1037
  n8n-nodes-base.awsS3 typeVersion: 2 \u2014 cred: aws
845
1038
 
846
- ### AI \u2014 Root nodes (sit on main data flow):
1039
+ ### AI \u2014 Root nodes (sit on main data flow, receive ai_* connections as TARGETS):
847
1040
  @n8n/n8n-nodes-langchain.agent typeVersion: 1.9 \u2014 params: promptType, text (if define), options.systemMessage
848
1041
  @n8n/n8n-nodes-langchain.chainLlm typeVersion: 1.5
849
1042
  @n8n/n8n-nodes-langchain.chainRetrievalQa typeVersion: 1.4
850
- @n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi
851
- @n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi
1043
+ @n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi \u2014 standalone node, calls OpenAI directly without sub-nodes
1044
+ @n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi \u2014 standalone node, calls Anthropic directly without sub-nodes
852
1045
 
853
- ### AI \u2014 Sub-nodes (sources of ai_* connections):
854
- @n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel
855
- @n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel
1046
+ ### AI \u2014 Sub-nodes (sources of ai_* connections, wire INTO root nodes above):
1047
+ @n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
1048
+ @n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
856
1049
  @n8n/n8n-nodes-langchain.lmChatGoogleGemini typeVersion: 1 \u2014 cred: googlePalmApi \u2014 ai_languageModel
857
1050
  @n8n/n8n-nodes-langchain.memoryBufferWindow typeVersion: 1.3 \u2014 \u2014 ai_memory
858
1051
  @n8n/n8n-nodes-langchain.toolWorkflow typeVersion: 2 \u2014 \u2014 ai_tool
@@ -887,11 +1080,42 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
887
1080
  Respond ONLY with a generate_workflow tool call. No prose. No markdown outside the tool call.
888
1081
  If the request is impossible or unclear, set the error field instead of generating a workflow.`;
889
1082
 
1083
+ // src/utils/thresholds.ts
1084
+ var DIRECT_THRESHOLD = 0.92;
1085
+ var REFERENCE_THRESHOLD = 0.72;
1086
+ function scoreToMode(score) {
1087
+ if (score >= DIRECT_THRESHOLD) return "direct";
1088
+ if (score >= REFERENCE_THRESHOLD) return "reference";
1089
+ return "scratch";
1090
+ }
1091
+
890
1092
  // src/generation/prompt-builder.ts
1093
+ var RULE_REMEDIES = {
1094
+ 1: "Provide a non-empty workflow name string",
1095
+ 2: "Include at least one node in the nodes array",
1096
+ 3: "Every node must have a unique UUID v4 string as its id field",
1097
+ 4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
1098
+ 5: "Every node must have a non-empty type string",
1099
+ 6: "Every node must have a positive integer typeVersion",
1100
+ 7: "Every node must have a position array of exactly [x, y] numbers",
1101
+ 8: "Every node must have a non-empty name string",
1102
+ 9: "connections must be a plain object (use {} if no connections)",
1103
+ 10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
1104
+ 12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
1105
+ 14: "Include at least one trigger node (e.g. webhook, scheduleTrigger, manualTrigger)",
1106
+ 15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1107
+ 16: "All node names must be unique within the workflow",
1108
+ 17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
1109
+ 18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
1110
+ 19: "Use known safe typeVersion values for each node type",
1111
+ 20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1112
+ 21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1113
+ 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
1114
+ };
891
1115
  var PromptBuilder = class {
892
- build(request, matches) {
1116
+ build(request, matches, globalFailureRates = []) {
893
1117
  const mode = this.resolveMode(matches);
894
- const system = this.buildSystem(matches, mode);
1118
+ const system = this.buildSystem(matches, mode, globalFailureRates);
895
1119
  const userMessage = this.buildUserMessage(request, matches, mode);
896
1120
  return { system, userMessage, mode, matches };
897
1121
  }
@@ -908,11 +1132,9 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
908
1132
  if (matches.length === 0) return "scratch";
909
1133
  const top = matches[0];
910
1134
  if (!top) return "scratch";
911
- if (top.score >= 0.92) return "direct";
912
- if (top.score >= 0.72) return "reference";
913
- return "scratch";
1135
+ return scoreToMode(top.score);
914
1136
  }
915
- buildSystem(matches, mode) {
1137
+ buildSystem(matches, mode, globalFailureRates = []) {
916
1138
  const blocks = [
917
1139
  {
918
1140
  type: "text",
@@ -936,15 +1158,63 @@ ${refText}`
936
1158
  }
937
1159
  if (mode === "direct" && matches[0]) {
938
1160
  const match = matches[0];
1161
+ const json = JSON.stringify(match.workflow.workflow, null, 2);
1162
+ if (json.length > 3e4) {
1163
+ const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1164
+ blocks.push({
1165
+ type: "text",
1166
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1167
+ Nodes:
1168
+ ${nodes}`
1169
+ });
1170
+ } else {
1171
+ blocks.push({
1172
+ type: "text",
1173
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1174
+
1175
+ ${json}`
1176
+ });
1177
+ }
1178
+ }
1179
+ if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1180
+ const hint = matches[0];
1181
+ const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
939
1182
  blocks.push({
940
1183
  type: "text",
941
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
942
-
943
- ${JSON.stringify(match.workflow.workflow, null, 2)}`
1184
+ text: `## Weak Structural Hint
1185
+ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
944
1186
  });
945
1187
  }
1188
+ const warnings = this.buildFailureWarnings(matches, globalFailureRates);
1189
+ if (warnings) {
1190
+ blocks.push({ type: "text", text: warnings });
1191
+ }
946
1192
  return blocks;
947
1193
  }
1194
+ buildFailureWarnings(matches, globalFailureRates) {
1195
+ const lines = [];
1196
+ for (const match of matches) {
1197
+ const patterns = match.workflow.failurePatterns;
1198
+ if (!patterns?.length) continue;
1199
+ for (const fp of patterns) {
1200
+ const remedy = RULE_REMEDIES[fp.rule];
1201
+ const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1202
+ lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
1203
+ }
1204
+ }
1205
+ const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
1206
+ for (const rule of highFreqRules) {
1207
+ const remedy = RULE_REMEDIES[rule.rule];
1208
+ const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
1209
+ lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
1210
+ }
1211
+ if (lines.length === 0) return null;
1212
+ const unique = [...new Set(lines)];
1213
+ return `## Known Failure Patterns \u2014 AVOID THESE
1214
+
1215
+ Previous builds frequently failed the following validation rules. Ensure your output does NOT repeat these mistakes:
1216
+ ${unique.join("\n")}`;
1217
+ }
948
1218
  buildUserMessage(request, _matches, _mode) {
949
1219
  const namePart = request.name ? `
950
1220
  Workflow name: "${request.name}"` : "";
@@ -991,7 +1261,7 @@ var GENERATE_WORKFLOW_TOOL = {
991
1261
  description: "Set this if the request cannot be fulfilled \u2014 explain why"
992
1262
  }
993
1263
  },
994
- required: ["workflow"]
1264
+ required: []
995
1265
  }
996
1266
  };
997
1267
  var WorkflowDesigner = class {
@@ -1007,24 +1277,24 @@ var WorkflowDesigner = class {
1007
1277
  logger;
1008
1278
  validator;
1009
1279
  promptBuilder;
1010
- async design(request, matches) {
1011
- const allIssues = [];
1280
+ async design(request, matches, globalFailureRates = []) {
1012
1281
  const attemptMetadata = [];
1282
+ let lastErrors = [];
1013
1283
  let attempts = 0;
1284
+ const built = this.promptBuilder.build(request, matches, globalFailureRates);
1014
1285
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
1015
1286
  attempts = attempt;
1016
1287
  const temperature = attempt === MAX_ATTEMPTS ? FINAL_TEMPERATURE : BASE_TEMPERATURE;
1017
- const built = this.promptBuilder.build(request, matches);
1018
1288
  let userMessage;
1019
1289
  if (attempt === 1) {
1020
1290
  userMessage = built.userMessage;
1021
1291
  this.logger.debug("WorkflowDesigner: attempt 1", { description: request.description });
1022
1292
  } else {
1023
- const issueLines = allIssues.map(
1293
+ const issueLines = lastErrors.map(
1024
1294
  (i) => `- [Rule ${i.rule}] ${i.message}${i.nodeId ? ` (node: ${i.nodeId})` : ""}`
1025
1295
  );
1026
1296
  userMessage = this.promptBuilder.buildCorrectionMessage(request, matches, issueLines, attempt - 1);
1027
- this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: allIssues.length });
1297
+ this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: lastErrors.length });
1028
1298
  }
1029
1299
  const start = Date.now();
1030
1300
  const message = await this.callClaude(built.system, userMessage, temperature);
@@ -1042,35 +1312,43 @@ var WorkflowDesigner = class {
1042
1312
  tokensInput: message.usage.input_tokens,
1043
1313
  tokensOutput: message.usage.output_tokens,
1044
1314
  validationPassed: validation.valid,
1045
- issues: errors
1315
+ issues: validation.issues
1046
1316
  });
1047
1317
  if (validation.valid) {
1048
1318
  return { workflow: parsed.workflow, credentialsNeeded: parsed.credentialsNeeded, attempts, attemptMetadata };
1049
1319
  }
1050
- allIssues.push(...errors);
1320
+ lastErrors = errors;
1051
1321
  this.logger.warn(`WorkflowDesigner: validation failed on attempt ${attempt}`, {
1052
- newErrors: errors.length,
1053
- totalErrors: allIssues.length
1322
+ errorCount: errors.length
1054
1323
  });
1055
1324
  }
1325
+ const finalIssues = attemptMetadata.at(-1)?.issues ?? lastErrors;
1056
1326
  throw new ValidationError(
1057
- `Workflow failed validation after ${MAX_ATTEMPTS} attempts (${allIssues.length} total errors)`,
1058
- allIssues
1327
+ `Workflow failed validation after ${MAX_ATTEMPTS} attempts`,
1328
+ finalIssues
1059
1329
  );
1060
1330
  }
1061
1331
  async callClaude(system, userMessage, temperature) {
1332
+ const controller = new AbortController();
1333
+ const timer = setTimeout(() => controller.abort(), 12e4);
1062
1334
  try {
1063
- return await this.anthropic.messages.create({
1064
- model: this.model,
1065
- max_tokens: 8192,
1066
- temperature,
1067
- system,
1068
- messages: [{ role: "user", content: userMessage }],
1069
- tools: [GENERATE_WORKFLOW_TOOL],
1070
- tool_choice: { type: "tool", name: "generate_workflow" }
1071
- });
1335
+ return await this.anthropic.messages.create(
1336
+ {
1337
+ model: this.model,
1338
+ max_tokens: 8192,
1339
+ temperature,
1340
+ system: system.map((b) => ({ type: b.type, text: b.text, ...b.cache_control ? { cache_control: b.cache_control } : {} })),
1341
+ messages: [{ role: "user", content: userMessage }],
1342
+ tools: [GENERATE_WORKFLOW_TOOL],
1343
+ tool_choice: { type: "tool", name: "generate_workflow" }
1344
+ },
1345
+ { signal: controller.signal }
1346
+ );
1072
1347
  } catch (err) {
1073
- throw new GenerationError("Anthropic API call failed", err);
1348
+ const detail = err instanceof Error ? err.message : String(err);
1349
+ throw new GenerationError(`Anthropic API call failed: ${detail}`, err);
1350
+ } finally {
1351
+ clearTimeout(timer);
1074
1352
  }
1075
1353
  }
1076
1354
  extractToolUse(message) {
@@ -1103,27 +1381,125 @@ var WorkflowDesigner = class {
1103
1381
  var import_promises = require("fs/promises");
1104
1382
  var import_node_path = require("path");
1105
1383
  var import_node_os = require("os");
1384
+
1385
+ // src/telemetry/types.ts
1386
+ var TELEMETRY_SCHEMA_VERSION = 2;
1387
+
1388
+ // src/telemetry/collector.ts
1106
1389
  var TelemetryCollector = class {
1107
1390
  dir;
1108
1391
  sessionId;
1392
+ dirReady = null;
1109
1393
  constructor(dir) {
1110
1394
  this.dir = dir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "telemetry");
1111
1395
  this.sessionId = generateUUID();
1112
1396
  }
1113
1397
  async emit(eventType, data) {
1114
1398
  const event = {
1399
+ schemaVersion: TELEMETRY_SCHEMA_VERSION,
1115
1400
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1116
1401
  sessionId: this.sessionId,
1117
1402
  eventType,
1118
1403
  data
1119
1404
  };
1120
- await (0, import_promises.mkdir)(this.dir, { recursive: true });
1405
+ if (!this.dirReady) {
1406
+ this.dirReady = (0, import_promises.mkdir)(this.dir, { recursive: true }).then(() => {
1407
+ });
1408
+ }
1409
+ await this.dirReady;
1121
1410
  const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
1122
1411
  const filepath = (0, import_node_path.join)(this.dir, filename);
1123
1412
  await (0, import_promises.appendFile)(filepath, JSON.stringify(event) + "\n", "utf-8");
1124
1413
  }
1125
1414
  };
1126
1415
 
1416
+ // src/telemetry/reader.ts
1417
+ var import_promises2 = require("fs/promises");
1418
+ var import_node_path2 = require("path");
1419
+ var import_node_os2 = require("os");
1420
+ var TelemetryReader = class {
1421
+ dir;
1422
+ cache = null;
1423
+ cacheTime = 0;
1424
+ constructor(dir) {
1425
+ this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
1426
+ }
1427
+ async getFailureRates(days = 30) {
1428
+ const now = Date.now();
1429
+ if (this.cache && now - this.cacheTime < 5 * 60 * 1e3) {
1430
+ return this.cache;
1431
+ }
1432
+ const events = await this.readRecentEvents(days);
1433
+ const buildSessions = new Set(
1434
+ events.filter((e) => e.eventType === "build_complete" && !e.data.dryRun).map((e) => e.sessionId)
1435
+ );
1436
+ if (buildSessions.size === 0) return [];
1437
+ const ruleSessions = /* @__PURE__ */ new Map();
1438
+ for (const event of events) {
1439
+ if (event.eventType !== "generation_attempt") continue;
1440
+ if (!buildSessions.has(event.sessionId)) continue;
1441
+ const data = event.data;
1442
+ if (data.validationPassed || !data.issues) continue;
1443
+ for (const issue of data.issues) {
1444
+ const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
1445
+ entry.sessions.add(event.sessionId);
1446
+ entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
1447
+ ruleSessions.set(issue.rule, entry);
1448
+ }
1449
+ }
1450
+ const rates = [];
1451
+ for (const [rule, entry] of ruleSessions) {
1452
+ let topMessage = "";
1453
+ let topCount = 0;
1454
+ for (const [msg, count] of entry.messages) {
1455
+ if (count > topCount) {
1456
+ topMessage = msg;
1457
+ topCount = count;
1458
+ }
1459
+ }
1460
+ rates.push({
1461
+ rule,
1462
+ failureCount: entry.sessions.size,
1463
+ totalBuilds: buildSessions.size,
1464
+ rate: entry.sessions.size / buildSessions.size,
1465
+ commonMessage: topMessage
1466
+ });
1467
+ }
1468
+ rates.sort((a, b) => b.rate - a.rate);
1469
+ this.cache = rates;
1470
+ this.cacheTime = now;
1471
+ return rates;
1472
+ }
1473
+ async readRecentEvents(days) {
1474
+ let files;
1475
+ try {
1476
+ files = await (0, import_promises2.readdir)(this.dir);
1477
+ } catch {
1478
+ return [];
1479
+ }
1480
+ const cutoff = /* @__PURE__ */ new Date();
1481
+ cutoff.setDate(cutoff.getDate() - days);
1482
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
1483
+ const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1484
+ const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr).sort();
1485
+ const events = [];
1486
+ for (const file of recentFiles) {
1487
+ try {
1488
+ const content = await (0, import_promises2.readFile)((0, import_node_path2.join)(this.dir, file), "utf-8");
1489
+ for (const line of content.split("\n")) {
1490
+ if (!line.trim()) continue;
1491
+ try {
1492
+ events.push(JSON.parse(line));
1493
+ } catch {
1494
+ }
1495
+ }
1496
+ } catch {
1497
+ }
1498
+ }
1499
+ return events;
1500
+ }
1501
+ };
1502
+
1127
1503
  // src/utils/logger.ts
1128
1504
  var nullLogger = {
1129
1505
  debug() {
@@ -1145,27 +1521,53 @@ var Kairos = class {
1145
1521
  library;
1146
1522
  logger;
1147
1523
  telemetry;
1524
+ telemetryReader;
1148
1525
  model;
1526
+ saveQueue = Promise.resolve(null);
1149
1527
  constructor(options) {
1150
1528
  const logger = options.logger ?? nullLogger;
1151
1529
  this.model = options.model ?? DEFAULT_MODEL;
1530
+ if (options.n8nBaseUrl && options.n8nApiKey) {
1531
+ try {
1532
+ new URL(options.n8nBaseUrl);
1533
+ } catch {
1534
+ throw new GuardError(`Invalid n8nBaseUrl: "${options.n8nBaseUrl}" \u2014 must be a valid URL`);
1535
+ }
1536
+ const apiClient = new N8nApiClient(options.n8nBaseUrl, options.n8nApiKey, logger);
1537
+ const stripper = new N8nFieldStripper();
1538
+ this.provider = new N8nProvider(apiClient, stripper);
1539
+ } else {
1540
+ this.provider = null;
1541
+ }
1152
1542
  const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
1153
- const apiClient = new N8nApiClient(options.n8nBaseUrl, options.n8nApiKey, logger);
1154
- const stripper = new N8nFieldStripper();
1155
- this.provider = new N8nProvider(apiClient, stripper);
1156
1543
  this.designer = new WorkflowDesigner(anthropic, this.model, logger);
1157
1544
  this.validator = new N8nValidator();
1158
1545
  this.library = options.library ?? new NullLibrary();
1159
1546
  this.logger = logger;
1160
1547
  if (options.telemetry === true) {
1161
1548
  this.telemetry = new TelemetryCollector();
1549
+ this.telemetryReader = new TelemetryReader();
1162
1550
  } else if (typeof options.telemetry === "string") {
1163
1551
  this.telemetry = new TelemetryCollector(options.telemetry);
1552
+ this.telemetryReader = new TelemetryReader(options.telemetry);
1164
1553
  } else {
1165
1554
  this.telemetry = null;
1555
+ this.telemetryReader = null;
1556
+ }
1557
+ }
1558
+ requireProvider() {
1559
+ if (!this.provider) {
1560
+ 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");
1561
+ }
1562
+ return this.provider;
1563
+ }
1564
+ validateDescription(description) {
1565
+ if (!description || description.trim().length === 0) {
1566
+ throw new GuardError("Description is required and must be non-empty");
1166
1567
  }
1167
1568
  }
1168
1569
  async build(description, options) {
1570
+ this.validateDescription(description);
1169
1571
  this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
1170
1572
  const buildStart = Date.now();
1171
1573
  await this.telemetry?.emit("build_start", {
@@ -1175,33 +1577,28 @@ var Kairos = class {
1175
1577
  });
1176
1578
  await this.library.initialize();
1177
1579
  const matches = await this.library.search(description);
1580
+ if (matches.length > 0) {
1581
+ const top = matches[0];
1582
+ this.logger.info(`Library: ${matches.length} match(es), top="${top.workflow.description.slice(0, 50)}" score=${top.score.toFixed(2)} mode=${top.mode}`);
1583
+ } else {
1584
+ this.logger.info("Library: no matches (scratch mode)");
1585
+ }
1586
+ const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
1587
+ if (globalFailureRates.length > 0) {
1588
+ const highFreq = globalFailureRates.filter((r) => r.rate >= 0.15);
1589
+ if (highFreq.length > 0) {
1590
+ this.logger.info(`Telemetry: ${highFreq.length} high-frequency failure rule(s) will be warned about`);
1591
+ }
1592
+ }
1178
1593
  const designResult = await this.designer.design(
1179
1594
  { description, ...options?.name ? { name: options.name } : {} },
1180
- matches
1595
+ matches,
1596
+ globalFailureRates
1181
1597
  );
1182
- for (const meta of designResult.attemptMetadata) {
1183
- await this.telemetry?.emit("generation_attempt", {
1184
- description,
1185
- attempt: meta.attempt,
1186
- temperature: meta.temperature,
1187
- durationMs: meta.durationMs,
1188
- tokensInput: meta.tokensInput,
1189
- tokensOutput: meta.tokensOutput,
1190
- validationPassed: meta.validationPassed,
1191
- issueCount: meta.issues.length,
1192
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message }))
1193
- });
1194
- }
1598
+ await this.emitAttemptTelemetry(description, designResult);
1195
1599
  const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
1600
+ this.saveToLibrary(workflow, description, designResult, matches);
1196
1601
  if (options?.dryRun) {
1197
- const result2 = {
1198
- workflowId: null,
1199
- name: workflow.name,
1200
- credentialsNeeded: designResult.credentialsNeeded,
1201
- activationRequired: true,
1202
- generationAttempts: designResult.attempts,
1203
- dryRun: true
1204
- };
1205
1602
  const totalTokensInput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
1206
1603
  const totalTokensOutput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
1207
1604
  await this.telemetry?.emit("build_complete", {
@@ -1216,23 +1613,64 @@ var Kairos = class {
1216
1613
  dryRun: true,
1217
1614
  credentialsNeeded: designResult.credentialsNeeded.length
1218
1615
  });
1219
- return result2;
1616
+ return {
1617
+ workflowId: null,
1618
+ name: workflow.name,
1619
+ workflow,
1620
+ credentialsNeeded: designResult.credentialsNeeded,
1621
+ activationRequired: true,
1622
+ generationAttempts: designResult.attempts,
1623
+ dryRun: true
1624
+ };
1220
1625
  }
1221
- const deployed = await this.provider.deploy(workflow);
1222
- this.library.save(workflow, { description }).catch((err) => {
1223
- this.logger.warn("Failed to save workflow to library (non-fatal)", { err: String(err) });
1224
- });
1626
+ const provider = this.requireProvider();
1627
+ const deployed = await provider.deploy(workflow);
1628
+ this.recordDeploy();
1225
1629
  if (options?.activate) {
1226
- await this.provider.activate(deployed.workflowId);
1630
+ await provider.activate(deployed.workflowId);
1227
1631
  }
1228
- const result = {
1632
+ const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
1633
+ const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
1634
+ await this.telemetry?.emit("build_complete", {
1635
+ description,
1636
+ success: true,
1637
+ totalAttempts: designResult.attempts,
1638
+ totalDurationMs: Date.now() - buildStart,
1639
+ totalTokensInput,
1640
+ totalTokensOutput,
1641
+ workflowName: deployed.name,
1642
+ workflowId: deployed.workflowId,
1643
+ dryRun: false,
1644
+ credentialsNeeded: designResult.credentialsNeeded.length
1645
+ });
1646
+ return {
1229
1647
  workflowId: deployed.workflowId,
1230
1648
  name: deployed.name,
1649
+ workflow,
1231
1650
  credentialsNeeded: designResult.credentialsNeeded,
1232
1651
  activationRequired: !options?.activate,
1233
1652
  generationAttempts: designResult.attempts,
1234
1653
  dryRun: false
1235
1654
  };
1655
+ }
1656
+ async replace(id, description) {
1657
+ this.validateDescription(description);
1658
+ this.logger.info("Kairos.update", { id, description });
1659
+ const buildStart = Date.now();
1660
+ await this.telemetry?.emit("build_start", {
1661
+ description,
1662
+ model: this.model,
1663
+ dryRun: false
1664
+ });
1665
+ await this.library.initialize();
1666
+ const matches = await this.library.search(description);
1667
+ const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
1668
+ const designResult = await this.designer.design({ description }, matches, globalFailureRates);
1669
+ await this.emitAttemptTelemetry(description, designResult);
1670
+ const provider = this.requireProvider();
1671
+ const deployed = await provider.update(id, designResult.workflow);
1672
+ this.saveToLibrary(designResult.workflow, description, designResult, matches);
1673
+ this.recordDeploy();
1236
1674
  const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
1237
1675
  const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
1238
1676
  await this.telemetry?.emit("build_complete", {
@@ -1247,54 +1685,691 @@ var Kairos = class {
1247
1685
  dryRun: false,
1248
1686
  credentialsNeeded: designResult.credentialsNeeded.length
1249
1687
  });
1250
- return result;
1251
- }
1252
- async update(id, description) {
1253
- this.logger.info("Kairos.update", { id, description });
1254
- const matches = await this.library.search(description);
1255
- const designResult = await this.designer.design({ description }, matches);
1256
- const deployed = await this.provider.update(id, designResult.workflow);
1257
1688
  return {
1258
1689
  workflowId: deployed.workflowId,
1259
1690
  name: deployed.name,
1691
+ workflow: designResult.workflow,
1260
1692
  credentialsNeeded: designResult.credentialsNeeded,
1261
1693
  activationRequired: true,
1262
1694
  generationAttempts: designResult.attempts,
1263
1695
  dryRun: false
1264
1696
  };
1265
1697
  }
1698
+ async drain() {
1699
+ await this.saveQueue.catch(() => {
1700
+ });
1701
+ }
1702
+ async emitAttemptTelemetry(description, designResult) {
1703
+ for (const meta of designResult.attemptMetadata) {
1704
+ await this.telemetry?.emit("generation_attempt", {
1705
+ description,
1706
+ attempt: meta.attempt,
1707
+ temperature: meta.temperature,
1708
+ durationMs: meta.durationMs,
1709
+ tokensInput: meta.tokensInput,
1710
+ tokensOutput: meta.tokensOutput,
1711
+ validationPassed: meta.validationPassed,
1712
+ issueCount: meta.issues.length,
1713
+ issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message }))
1714
+ });
1715
+ }
1716
+ }
1717
+ recordDeploy() {
1718
+ this.saveQueue = this.saveQueue.then(async (savedId) => {
1719
+ if (savedId) {
1720
+ await this.library.recordDeployment(savedId);
1721
+ }
1722
+ return savedId;
1723
+ }).catch((err) => {
1724
+ this.logger.warn("Failed to record deployment (non-fatal)", { err: String(err) });
1725
+ return null;
1726
+ });
1727
+ }
1728
+ saveToLibrary(workflow, description, designResult, matches) {
1729
+ const failedAttempts = designResult.attemptMetadata.filter((m) => !m.validationPassed);
1730
+ const failurePatterns = failedAttempts.flatMap(
1731
+ (m) => m.issues.map((i) => ({ rule: i.rule, message: i.message }))
1732
+ );
1733
+ const topMatch = matches[0];
1734
+ const generationMode = topMatch ? scoreToMode(topMatch.score) : "scratch";
1735
+ const autoTags = Array.from(new Set(
1736
+ workflow.nodes.flatMap((n) => {
1737
+ const bare = n.type.split(".").pop() ?? "";
1738
+ const tags = [bare];
1739
+ if (n.type.includes("Trigger") || n.type.includes("trigger")) tags.push(`trigger:${bare}`);
1740
+ if (n.type.includes("langchain")) tags.push("ai");
1741
+ return tags;
1742
+ })
1743
+ ));
1744
+ const metadata = {
1745
+ description,
1746
+ generationMode,
1747
+ generationAttempts: designResult.attempts
1748
+ };
1749
+ if (autoTags.length > 0) metadata.tags = autoTags;
1750
+ if (failurePatterns.length > 0) metadata.failurePatterns = failurePatterns;
1751
+ if (matches.length > 0) metadata.sourceWorkflowIds = matches.map((m) => m.workflow.id);
1752
+ if (topMatch) metadata.topMatchScore = topMatch.score;
1753
+ if (designResult.credentialsNeeded.length > 0) metadata.credentialsNeeded = designResult.credentialsNeeded;
1754
+ const firstTryPass = designResult.attemptMetadata.length > 0 && designResult.attemptMetadata[0].validationPassed;
1755
+ const failedRules = Array.from(new Set(
1756
+ designResult.attemptMetadata.filter((m) => !m.validationPassed).flatMap((m) => m.issues.map((i) => i.rule))
1757
+ ));
1758
+ this.saveQueue = this.saveQueue.then(async () => {
1759
+ const savedId = await this.library.save(workflow, metadata);
1760
+ for (const match of matches) {
1761
+ if (match.mode === "direct" || match.mode === "reference") {
1762
+ await this.library.recordOutcome(match.workflow.id, {
1763
+ attempts: designResult.attempts,
1764
+ firstTryPass,
1765
+ failedRules,
1766
+ mode: match.mode
1767
+ });
1768
+ }
1769
+ }
1770
+ return savedId;
1771
+ }).catch((err) => {
1772
+ this.logger.warn("Failed to save workflow to library (non-fatal)", { err: String(err) });
1773
+ return null;
1774
+ });
1775
+ }
1266
1776
  async get(id) {
1267
- return this.provider.get(id);
1777
+ return this.requireProvider().get(id);
1268
1778
  }
1269
1779
  async list() {
1270
- return this.provider.list();
1780
+ return this.requireProvider().list();
1271
1781
  }
1272
1782
  async activate(id) {
1273
- await this.provider.activate(id);
1783
+ await this.requireProvider().activate(id);
1274
1784
  }
1275
1785
  async deactivate(id) {
1276
- await this.provider.deactivate(id);
1786
+ await this.requireProvider().deactivate(id);
1277
1787
  }
1278
1788
  async delete(id, options) {
1279
- await this.provider.delete(id, options);
1789
+ await this.requireProvider().delete(id, options);
1280
1790
  }
1281
1791
  async executions(workflowId, filter) {
1282
- return this.provider.executions(workflowId, filter);
1792
+ return this.requireProvider().executions(workflowId, filter);
1283
1793
  }
1284
1794
  async execution(id) {
1285
- return this.provider.execution(id);
1795
+ return this.requireProvider().execution(id);
1286
1796
  }
1287
1797
  async listTags() {
1288
- return this.provider.listTags();
1798
+ return this.requireProvider().listTags();
1289
1799
  }
1290
1800
  async createTag(name) {
1291
- return this.provider.createTag(name);
1801
+ return this.requireProvider().createTag(name);
1292
1802
  }
1293
1803
  async tag(workflowId, tagIds) {
1294
- await this.provider.tag(workflowId, tagIds);
1804
+ await this.requireProvider().tag(workflowId, tagIds);
1295
1805
  }
1296
1806
  async untag(workflowId, tagIds) {
1297
- await this.provider.untag(workflowId, tagIds);
1807
+ await this.requireProvider().untag(workflowId, tagIds);
1808
+ }
1809
+ };
1810
+
1811
+ // src/library/file-library.ts
1812
+ var import_promises3 = require("fs/promises");
1813
+ var import_node_path3 = require("path");
1814
+ var import_node_os3 = require("os");
1815
+
1816
+ // src/library/scorer.ts
1817
+ var WEIGHTS = {
1818
+ tfidf: 0.35,
1819
+ nodeFingerprint: 0.3,
1820
+ outcome: 0.2,
1821
+ deploy: 0.15
1822
+ };
1823
+ var NODE_KEYWORDS = {
1824
+ slack: ["slack", "slackApi"],
1825
+ email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
1826
+ webhook: ["webhook", "webhookTrigger"],
1827
+ schedule: ["scheduleTrigger", "cron"],
1828
+ http: ["httpRequest"],
1829
+ sheets: ["googleSheets"],
1830
+ github: ["github", "githubTrigger"],
1831
+ telegram: ["telegram", "telegramTrigger"],
1832
+ ai: ["agent", "openAi", "lmChatOpenAi", "lmChatAnthropic", "chainLlm", "chainSummarization"],
1833
+ memory: ["memoryBufferWindow", "memoryXata", "memoryPostgres"],
1834
+ vector: ["vectorStoreInMemory", "vectorStorePinecone", "vectorStoreQdrant"],
1835
+ database: ["postgres", "mySql", "redis", "mongoDb"],
1836
+ airtable: ["airtable"],
1837
+ notion: ["notion"],
1838
+ s3: ["awsS3"],
1839
+ code: ["code"],
1840
+ merge: ["merge"],
1841
+ switch: ["switch"],
1842
+ if: ["if"],
1843
+ wait: ["wait"],
1844
+ rss: ["rssFeedRead", "rssFeedReadTrigger"],
1845
+ form: ["formTrigger"],
1846
+ set: ["set"],
1847
+ split: ["splitInBatches"],
1848
+ filter: ["filter"],
1849
+ telegram_trigger: ["telegramTrigger"],
1850
+ stripe: ["stripe"]
1851
+ };
1852
+ function extractQueryFingerprint(description) {
1853
+ const lower = description.toLowerCase();
1854
+ const matches = /* @__PURE__ */ new Set();
1855
+ for (const [keyword, nodeTypes] of Object.entries(NODE_KEYWORDS)) {
1856
+ if (lower.includes(keyword)) {
1857
+ for (const nt of nodeTypes) matches.add(nt);
1858
+ }
1859
+ }
1860
+ if (/\bevery\b|\bdaily\b|\bhourly\b|\bweekly\b|\bmonthly\b|\bcron\b|\bschedule\b|\bat \d/.test(lower)) {
1861
+ matches.add("scheduleTrigger");
1862
+ }
1863
+ if (/\bwebhook\b|\breceive\b.*\bpost\b|\bpost\b.*\brequest\b/.test(lower)) {
1864
+ matches.add("webhook");
1865
+ }
1866
+ if (/\bchat\b|\bchatbot\b|\bconversation\b/.test(lower)) {
1867
+ matches.add("chatTrigger");
1868
+ }
1869
+ if (/\bai\b|\bllm\b|\bgpt\b|\bclaude\b|\bagent\b|\bsummariz/.test(lower)) {
1870
+ matches.add("agent");
1871
+ }
1872
+ return matches;
1873
+ }
1874
+ function extractWorkflowFingerprint(w) {
1875
+ const fp = /* @__PURE__ */ new Set();
1876
+ for (const node of w.workflow.nodes) {
1877
+ const bare = node.type.split(".").pop() ?? "";
1878
+ fp.add(bare);
1879
+ }
1880
+ return fp;
1881
+ }
1882
+ function jaccardSimilarity(a, b) {
1883
+ if (a.size === 0 && b.size === 0) return 0;
1884
+ let intersection = 0;
1885
+ for (const item of a) {
1886
+ if (b.has(item)) intersection++;
1887
+ }
1888
+ const union = a.size + b.size - intersection;
1889
+ return union > 0 ? intersection / union : 0;
1890
+ }
1891
+ function outcomeScore(w) {
1892
+ const stats = w.outcomeStats;
1893
+ if (!stats || stats.totalUses === 0) return 0.5;
1894
+ const passRate = stats.firstTryPasses / stats.totalUses;
1895
+ const avgAttempts = stats.totalAttempts / stats.totalUses;
1896
+ const attemptPenalty = Math.max(0, 1 - (avgAttempts - 1) * 0.3);
1897
+ return passRate * 0.6 + attemptPenalty * 0.4;
1898
+ }
1899
+ function deployScore(w) {
1900
+ return 1 + Math.log(w.deployCount + 1) * 0.1;
1901
+ }
1902
+ function hybridScore(queryTokens, queryDescription, workflows, docTokenArrays, idf) {
1903
+ const queryFp = extractQueryFingerprint(queryDescription);
1904
+ const ceiling = queryTokens.reduce((sum, qt) => sum + (idf.get(qt) ?? 0), 0) || 1;
1905
+ return workflows.map((w, i) => {
1906
+ const docTokens = docTokenArrays[i];
1907
+ let tfidfRaw = 0;
1908
+ const docFreq = /* @__PURE__ */ new Map();
1909
+ for (const t of docTokens) {
1910
+ docFreq.set(t, (docFreq.get(t) ?? 0) + 1);
1911
+ }
1912
+ for (const qt of queryTokens) {
1913
+ const tf = docTokens.length > 0 ? (docFreq.get(qt) ?? 0) / docTokens.length : 0;
1914
+ const idfVal = idf.get(qt) ?? 0;
1915
+ tfidfRaw += tf * idfVal;
1916
+ }
1917
+ const tfidf = Math.min(tfidfRaw / ceiling, 1);
1918
+ const workflowFp = extractWorkflowFingerprint(w);
1919
+ const nodeFingerprint = queryFp.size > 0 ? jaccardSimilarity(queryFp, workflowFp) : 0;
1920
+ const outcome = outcomeScore(w);
1921
+ const deploy = Math.min(deployScore(w), 1.5) / 1.5;
1922
+ const score = Math.min(
1923
+ WEIGHTS.tfidf * tfidf + WEIGHTS.nodeFingerprint * nodeFingerprint + WEIGHTS.outcome * outcome + WEIGHTS.deploy * deploy,
1924
+ 1
1925
+ );
1926
+ return {
1927
+ workflow: w,
1928
+ score,
1929
+ signals: { tfidf, nodeFingerprint, outcome, deploy }
1930
+ };
1931
+ });
1932
+ }
1933
+
1934
+ // src/library/cluster.ts
1935
+ function getFingerprint(w) {
1936
+ return w.workflow.nodes.map((n) => n.type.split(".").pop() ?? "").sort();
1937
+ }
1938
+ function fingerprintKey(fp) {
1939
+ return fp.join("|");
1940
+ }
1941
+ function describePattern(fp) {
1942
+ const triggers = fp.filter((n) => /trigger/i.test(n));
1943
+ const outputs = fp.filter((n) => /slack|gmail|email|telegram|sheets|airtable|notion/i.test(n));
1944
+ const ai = fp.filter((n) => /agent|openai|anthropic|chain|memory/i.test(n));
1945
+ const core = fp.filter((n) => /httpRequest|code|merge|switch|if|set|filter/i.test(n));
1946
+ const parts = [];
1947
+ if (triggers.length > 0) parts.push(triggers[0]);
1948
+ if (ai.length > 0) parts.push("AI");
1949
+ if (core.length > 0) parts.push(core.slice(0, 2).join("+"));
1950
+ if (outputs.length > 0) parts.push(outputs[0]);
1951
+ return parts.length > 0 ? parts.join(" \u2192 ") : fp.slice(0, 3).join(" \u2192 ");
1952
+ }
1953
+ function clusterWorkflows(workflows) {
1954
+ const groups = /* @__PURE__ */ new Map();
1955
+ for (const w of workflows) {
1956
+ const fp = getFingerprint(w);
1957
+ const key = fingerprintKey(fp);
1958
+ const existing = groups.get(key);
1959
+ if (existing) {
1960
+ existing.push(w);
1961
+ } else {
1962
+ groups.set(key, [w]);
1963
+ }
1964
+ }
1965
+ const clusters = [];
1966
+ for (const [, members] of groups) {
1967
+ if (members.length === 0) continue;
1968
+ const fp = getFingerprint(members[0]);
1969
+ const withStats = members.filter((m) => m.outcomeStats && m.outcomeStats.totalUses > 0);
1970
+ let avgFirstTryPassRate = 0;
1971
+ let avgAttempts = 0;
1972
+ if (withStats.length > 0) {
1973
+ avgFirstTryPassRate = withStats.reduce((sum, m) => {
1974
+ const s = m.outcomeStats;
1975
+ return sum + s.firstTryPasses / s.totalUses;
1976
+ }, 0) / withStats.length;
1977
+ avgAttempts = withStats.reduce((sum, m) => {
1978
+ const s = m.outcomeStats;
1979
+ return sum + s.totalAttempts / s.totalUses;
1980
+ }, 0) / withStats.length;
1981
+ }
1982
+ const ruleCounts = /* @__PURE__ */ new Map();
1983
+ let totalFailureInstances = 0;
1984
+ for (const m of withStats) {
1985
+ const rules = m.outcomeStats.failedRules;
1986
+ for (const [rule, count] of Object.entries(rules)) {
1987
+ const r = parseInt(rule, 10);
1988
+ ruleCounts.set(r, (ruleCounts.get(r) ?? 0) + count);
1989
+ totalFailureInstances += count;
1990
+ }
1991
+ }
1992
+ const commonFailedRules = [...ruleCounts.entries()].map(([rule, count]) => ({
1993
+ rule,
1994
+ frequency: totalFailureInstances > 0 ? count / totalFailureInstances : 0
1995
+ })).filter((r) => r.frequency >= 0.1).sort((a, b) => b.frequency - a.frequency);
1996
+ clusters.push({
1997
+ pattern: describePattern(fp),
1998
+ fingerprint: fp,
1999
+ members,
2000
+ avgFirstTryPassRate,
2001
+ avgAttempts,
2002
+ commonFailedRules
2003
+ });
2004
+ }
2005
+ return clusters.sort((a, b) => b.members.length - a.members.length);
2006
+ }
2007
+ function rerank(candidates, clusters) {
2008
+ const clusterMap = /* @__PURE__ */ new Map();
2009
+ for (const cluster of clusters) {
2010
+ for (const member of cluster.members) {
2011
+ clusterMap.set(member.id, cluster);
2012
+ }
2013
+ }
2014
+ return candidates.map((c) => {
2015
+ const cluster = clusterMap.get(c.workflow.id);
2016
+ let boost = 0;
2017
+ if (cluster && cluster.avgFirstTryPassRate > 0) {
2018
+ boost = (cluster.avgFirstTryPassRate - 0.5) * 0.1;
2019
+ }
2020
+ if (cluster && cluster.commonFailedRules.length > 0) {
2021
+ boost -= cluster.commonFailedRules.length * 0.02;
2022
+ }
2023
+ return {
2024
+ workflow: c.workflow,
2025
+ score: Math.max(0, Math.min(1, c.score + boost)),
2026
+ ...cluster ? { clusterPattern: cluster.pattern } : {}
2027
+ };
2028
+ }).sort((a, b) => b.score - a.score);
2029
+ }
2030
+
2031
+ // src/library/file-library.ts
2032
+ function tokenize(text) {
2033
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2);
2034
+ }
2035
+ function buildSearchCorpus(w) {
2036
+ const nodeTokens = w.workflow.nodes.map((n) => {
2037
+ const bare = n.type.split(".").pop() ?? "";
2038
+ const spaced = bare.replace(/([A-Z])/g, " $1").trim().toLowerCase();
2039
+ return `${bare} ${spaced}`;
2040
+ });
2041
+ return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
2042
+ }
2043
+ var MAX_LIBRARY_SIZE = 500;
2044
+ var FileLibrary = class {
2045
+ dir;
2046
+ workflows = [];
2047
+ initPromise = null;
2048
+ writeQueue = Promise.resolve();
2049
+ constructor(dir) {
2050
+ this.dir = dir ?? (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".kairos", "library");
2051
+ }
2052
+ async initialize() {
2053
+ if (!this.initPromise) {
2054
+ this.initPromise = this.doInitialize();
2055
+ }
2056
+ return this.initPromise;
2057
+ }
2058
+ async doInitialize() {
2059
+ await (0, import_promises3.mkdir)(this.dir, { recursive: true });
2060
+ const indexPath = (0, import_node_path3.join)(this.dir, "index.json");
2061
+ try {
2062
+ const raw = await (0, import_promises3.readFile)(indexPath, "utf-8");
2063
+ const parsed = JSON.parse(raw);
2064
+ if (!Array.isArray(parsed)) {
2065
+ this.workflows = [];
2066
+ } else {
2067
+ this.workflows = parsed.filter(
2068
+ (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)
2069
+ );
2070
+ }
2071
+ } catch {
2072
+ this.workflows = [];
2073
+ }
2074
+ }
2075
+ async search(description, options) {
2076
+ const searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
2077
+ if (searchable.length === 0) return [];
2078
+ const limit = options?.limit ?? 3;
2079
+ const queryTokens = tokenize(description);
2080
+ if (queryTokens.length === 0) return [];
2081
+ const docTokenArrays = searchable.map((w) => tokenize(buildSearchCorpus(w)));
2082
+ const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
2083
+ const docCount = searchable.length;
2084
+ const idf = /* @__PURE__ */ new Map();
2085
+ const allTokens = new Set(queryTokens);
2086
+ for (const token of allTokens) {
2087
+ const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
2088
+ idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
2089
+ }
2090
+ const scored = hybridScore(queryTokens, description, searchable, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
2091
+ const clusters = clusterWorkflows(searchable);
2092
+ const reranked = rerank(scored, clusters).slice(0, limit);
2093
+ const results = reranked.map((m) => {
2094
+ return { workflow: m.workflow, score: m.score, mode: scoreToMode(m.score) };
2095
+ });
2096
+ if (results.length > 0) {
2097
+ for (const r of results) {
2098
+ r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
2099
+ }
2100
+ this.persist();
2101
+ }
2102
+ return results;
2103
+ }
2104
+ async save(workflow, metadata) {
2105
+ const id = generateUUID();
2106
+ const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
2107
+ const stored = {
2108
+ id,
2109
+ workflow,
2110
+ description: metadata.description,
2111
+ tags: metadata.tags ?? [],
2112
+ platform: metadata.platform ?? "n8n",
2113
+ deployCount: 0,
2114
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2115
+ ...failurePatterns?.length ? { failurePatterns } : {},
2116
+ ...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
2117
+ ...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
2118
+ ...metadata.topMatchScore != null ? { topMatchScore: metadata.topMatchScore } : {},
2119
+ ...metadata.generationAttempts != null ? { generationAttempts: metadata.generationAttempts } : {},
2120
+ ...metadata.credentialsNeeded?.length ? { credentialsNeeded: metadata.credentialsNeeded } : {},
2121
+ ...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
2122
+ ...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
2123
+ ...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
2124
+ ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
2125
+ };
2126
+ this.workflows.push(stored);
2127
+ if (this.workflows.length > MAX_LIBRARY_SIZE) {
2128
+ this.workflows.sort((a, b) => (b.deployCount ?? 1) - (a.deployCount ?? 1));
2129
+ this.workflows = this.workflows.slice(0, MAX_LIBRARY_SIZE);
2130
+ }
2131
+ await this.persist();
2132
+ return id;
2133
+ }
2134
+ async recordDeployment(id) {
2135
+ const w = this.workflows.find((w2) => w2.id === id);
2136
+ if (w) {
2137
+ w.deployCount++;
2138
+ w.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
2139
+ await this.persist();
2140
+ }
2141
+ }
2142
+ async recordOutcome(id, outcome) {
2143
+ const w = this.workflows.find((w2) => w2.id === id);
2144
+ if (!w) return;
2145
+ if (outcome.mode === "direct") {
2146
+ w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
2147
+ } else {
2148
+ w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
2149
+ }
2150
+ const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
2151
+ stats.totalUses++;
2152
+ stats.totalAttempts += outcome.attempts;
2153
+ if (outcome.firstTryPass) stats.firstTryPasses++;
2154
+ for (const rule of outcome.failedRules) {
2155
+ const key = String(rule);
2156
+ stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
2157
+ }
2158
+ w.outcomeStats = stats;
2159
+ await this.persist();
2160
+ }
2161
+ async drain() {
2162
+ await this.writeQueue;
2163
+ }
2164
+ async get(id) {
2165
+ return this.workflows.find((w) => w.id === id) ?? null;
2166
+ }
2167
+ async list(filters) {
2168
+ let result = this.workflows;
2169
+ if (filters?.platform) {
2170
+ result = result.filter((w) => w.platform === filters.platform);
2171
+ }
2172
+ if (filters?.tags && filters.tags.length > 0) {
2173
+ result = result.filter((w) => filters.tags.some((t) => w.tags.includes(t)));
2174
+ }
2175
+ return result;
2176
+ }
2177
+ deduplicateFailurePatterns(patterns) {
2178
+ if (!patterns?.length) return void 0;
2179
+ const map = /* @__PURE__ */ new Map();
2180
+ for (const fp of patterns) {
2181
+ const existing = map.get(fp.rule);
2182
+ if (existing) {
2183
+ existing.occurrences++;
2184
+ } else {
2185
+ map.set(fp.rule, { rule: fp.rule, message: fp.message, occurrences: 1 });
2186
+ }
2187
+ }
2188
+ return [...map.values()];
2189
+ }
2190
+ persist() {
2191
+ this.writeQueue = this.writeQueue.then(async () => {
2192
+ const indexPath = (0, import_node_path3.join)(this.dir, "index.json");
2193
+ const tmpPath = `${indexPath}.tmp`;
2194
+ await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
2195
+ await (0, import_promises3.rename)(tmpPath, indexPath);
2196
+ });
2197
+ return this.writeQueue;
2198
+ }
2199
+ };
2200
+
2201
+ // src/templates/safety.ts
2202
+ var BLOCKED_NODE_TYPES = /* @__PURE__ */ new Set([
2203
+ "n8n-nodes-base.code",
2204
+ "n8n-nodes-base.executeCommand",
2205
+ "n8n-nodes-base.ssh"
2206
+ ]);
2207
+ var REVIEW_NODE_TYPES = /* @__PURE__ */ new Set([
2208
+ "n8n-nodes-base.httpRequest"
2209
+ ]);
2210
+ var SECRET_PATTERNS = [
2211
+ /sk-[a-zA-Z0-9]{20,}/,
2212
+ /ghp_[a-zA-Z0-9]{36}/,
2213
+ /xoxb-[0-9]+-[0-9]+-[a-zA-Z0-9]+/,
2214
+ /AIza[a-zA-Z0-9_-]{35}/,
2215
+ /AKIA[A-Z0-9]{16}/
2216
+ ];
2217
+ function assessTemplateSafety(workflow) {
2218
+ const reasons = [];
2219
+ let worst = "safe";
2220
+ const escalate = (level, reason) => {
2221
+ reasons.push(reason);
2222
+ if (level === "blocked") worst = "blocked";
2223
+ else if (level === "review" && worst === "safe") worst = "review";
2224
+ };
2225
+ for (const node of workflow.nodes) {
2226
+ if (BLOCKED_NODE_TYPES.has(node.type)) {
2227
+ escalate("blocked", `Contains ${node.type} node "${node.name}"`);
2228
+ }
2229
+ if (REVIEW_NODE_TYPES.has(node.type)) {
2230
+ escalate("review", `Contains ${node.type} node "${node.name}"`);
2231
+ }
2232
+ const paramStr = JSON.stringify(node.parameters);
2233
+ for (const pattern of SECRET_PATTERNS) {
2234
+ if (pattern.test(paramStr)) {
2235
+ escalate("blocked", `Node "${node.name}" parameters contain a hardcoded secret`);
2236
+ break;
2237
+ }
2238
+ }
2239
+ }
2240
+ return { trustLevel: worst, reasons };
2241
+ }
2242
+
2243
+ // src/templates/syncer.ts
2244
+ var N8N_TEMPLATE_API = "https://api.n8n.io/api/templates";
2245
+ var PAGE_SIZE = 50;
2246
+ var DELAY_BETWEEN_FETCHES_MS = 200;
2247
+ var DEFAULT_SETTINGS = {
2248
+ executionOrder: "v1",
2249
+ saveManualExecutions: true,
2250
+ timezone: "UTC"
2251
+ };
2252
+ var TemplateSyncer = class {
2253
+ constructor(library, logger) {
2254
+ this.library = library;
2255
+ this.validator = new N8nValidator();
2256
+ this.logger = logger;
2257
+ }
2258
+ library;
2259
+ validator;
2260
+ logger;
2261
+ async sync(options) {
2262
+ const maxTemplates = options?.maxTemplates ?? 500;
2263
+ await this.library.initialize();
2264
+ const existing = await this.library.list();
2265
+ const existingSourceIds = new Set(
2266
+ existing.filter((w) => w.sourceKind === "n8n-template" && w.sourceId).map((w) => w.sourceId)
2267
+ );
2268
+ const progress = {
2269
+ total: 0,
2270
+ processed: 0,
2271
+ saved: 0,
2272
+ skippedPaid: 0,
2273
+ skippedDuplicate: 0,
2274
+ blocked: 0,
2275
+ reviewed: 0
2276
+ };
2277
+ const templateIds = await this.fetchTemplateIds(maxTemplates, progress);
2278
+ for (const id of templateIds) {
2279
+ if (existingSourceIds.has(String(id))) {
2280
+ progress.skippedDuplicate++;
2281
+ progress.processed++;
2282
+ options?.onProgress?.(progress);
2283
+ continue;
2284
+ }
2285
+ try {
2286
+ await this.processTemplate(id, progress);
2287
+ } catch (err) {
2288
+ this.logger.warn(`Failed to process template ${id}`, { err: String(err) });
2289
+ }
2290
+ progress.processed++;
2291
+ options?.onProgress?.(progress);
2292
+ await new Promise((resolve) => setTimeout(resolve, DELAY_BETWEEN_FETCHES_MS));
2293
+ }
2294
+ return progress;
2295
+ }
2296
+ async fetchTemplateIds(max, progress) {
2297
+ const ids = [];
2298
+ let page = 1;
2299
+ while (ids.length < max) {
2300
+ const url = `${N8N_TEMPLATE_API}/search?page=${page}&rows=${PAGE_SIZE}`;
2301
+ const response = await fetch(url);
2302
+ if (!response.ok) break;
2303
+ const data = await response.json();
2304
+ progress.total = Math.min(data.totalWorkflows, max);
2305
+ for (const template of data.workflows) {
2306
+ if (ids.length >= max) break;
2307
+ if (template.price && template.price > 0) {
2308
+ progress.skippedPaid++;
2309
+ continue;
2310
+ }
2311
+ ids.push(template.id);
2312
+ }
2313
+ if (data.workflows.length < PAGE_SIZE) break;
2314
+ page++;
2315
+ await new Promise((resolve) => setTimeout(resolve, DELAY_BETWEEN_FETCHES_MS));
2316
+ }
2317
+ return ids;
2318
+ }
2319
+ async processTemplate(id, progress) {
2320
+ const url = `${N8N_TEMPLATE_API}/workflows/${id}`;
2321
+ const response = await fetch(url);
2322
+ if (!response.ok) return;
2323
+ const data = await response.json();
2324
+ const templateMeta = data.workflow;
2325
+ const rawWorkflow = templateMeta.workflow;
2326
+ if (!rawWorkflow?.nodes?.length) return;
2327
+ const workflow = {
2328
+ name: templateMeta.name,
2329
+ nodes: rawWorkflow.nodes.filter((n) => n.type && n.name),
2330
+ connections: rawWorkflow.connections,
2331
+ settings: rawWorkflow.settings ? { executionOrder: "v1", ...rawWorkflow.settings } : { ...DEFAULT_SETTINGS }
2332
+ };
2333
+ const validation = this.validator.validate(workflow);
2334
+ const validationErrors = validation.issues.filter((i) => i.severity === "error");
2335
+ if (validationErrors.length > 0) {
2336
+ progress.blocked++;
2337
+ this.logger.debug(`Template ${id} blocked: ${validationErrors.length} validation errors`);
2338
+ return;
2339
+ }
2340
+ const safety = assessTemplateSafety(workflow);
2341
+ if (safety.trustLevel === "blocked") {
2342
+ progress.blocked++;
2343
+ this.logger.debug(`Template ${id} blocked: ${safety.reasons.join(", ")}`);
2344
+ return;
2345
+ }
2346
+ if (safety.trustLevel === "review") {
2347
+ progress.reviewed++;
2348
+ }
2349
+ const description = this.cleanDescription(templateMeta.description);
2350
+ const autoTags = Array.from(new Set(
2351
+ workflow.nodes.flatMap((n) => {
2352
+ const bare = n.type.split(".").pop() ?? "";
2353
+ const tags = [bare];
2354
+ if (n.type.includes("Trigger") || n.type.includes("trigger")) tags.push(`trigger:${bare}`);
2355
+ if (n.type.includes("langchain")) tags.push("ai");
2356
+ return tags;
2357
+ })
2358
+ ));
2359
+ const metadata = {
2360
+ description,
2361
+ tags: autoTags,
2362
+ sourceKind: "n8n-template",
2363
+ sourceId: String(id),
2364
+ sourceUrl: `https://n8n.io/workflows/${id}`,
2365
+ trustLevel: safety.trustLevel
2366
+ };
2367
+ await this.library.save(workflow, metadata);
2368
+ progress.saved++;
2369
+ this.logger.debug(`Template ${id} saved: "${templateMeta.name}" (${safety.trustLevel})`);
2370
+ }
2371
+ cleanDescription(raw) {
2372
+ 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);
1298
2373
  }
1299
2374
  };
1300
2375
 
@@ -1303,22 +2378,27 @@ var HELP = `
1303
2378
  Kairos SDK \u2014 LLM-powered n8n workflow generation
1304
2379
 
1305
2380
  Usage:
2381
+ kairos init First-time setup wizard
1306
2382
  kairos build <description> [options]
1307
2383
  kairos list
1308
2384
  kairos get <id>
1309
2385
  kairos activate <id>
1310
2386
  kairos deactivate <id>
1311
2387
  kairos delete <id> --confirm
2388
+ kairos sync-templates [options]
1312
2389
 
1313
2390
  Build options:
1314
2391
  --dry-run Generate and validate without deploying
1315
2392
  --name <name> Override the generated workflow name
1316
2393
  --activate Activate the workflow after deployment
1317
2394
 
2395
+ Sync options:
2396
+ --max <count> Maximum templates to fetch (default: 500)
2397
+
1318
2398
  Environment variables:
1319
2399
  ANTHROPIC_API_KEY Anthropic API key (required)
1320
- N8N_BASE_URL n8n instance URL (required)
1321
- N8N_API_KEY n8n API key (required)
2400
+ N8N_BASE_URL n8n instance URL (required for deploy, optional for --dry-run)
2401
+ N8N_API_KEY n8n API key (required for deploy, optional for --dry-run)
1322
2402
  KAIROS_MODEL Claude model override (default: claude-sonnet-4-6)
1323
2403
  KAIROS_TELEMETRY Set to "true" or a directory path to enable telemetry logging
1324
2404
  `;
@@ -1352,44 +2432,59 @@ function parseArgs(argv) {
1352
2432
  }
1353
2433
  return { command, positional, flags };
1354
2434
  }
1355
- function createClient() {
2435
+ var CLI_LOGGER = {
2436
+ debug: () => {
2437
+ },
2438
+ info: (msg, meta) => console.error(meta ? `${msg} ${JSON.stringify(meta)}` : msg),
2439
+ warn: (msg, meta) => console.error(meta ? `[warn] ${msg} ${JSON.stringify(meta)}` : `[warn] ${msg}`),
2440
+ error: (msg, meta) => console.error(meta ? `[error] ${msg} ${JSON.stringify(meta)}` : `[error] ${msg}`)
2441
+ };
2442
+ function getTelemetryOption() {
1356
2443
  const telemetryEnv = process.env["KAIROS_TELEMETRY"];
1357
- let telemetry;
1358
- if (telemetryEnv === "true") {
1359
- telemetry = true;
1360
- } else if (telemetryEnv && telemetryEnv !== "false") {
1361
- telemetry = telemetryEnv;
1362
- }
2444
+ if (telemetryEnv === "true") return true;
2445
+ if (telemetryEnv && telemetryEnv !== "false") return telemetryEnv;
2446
+ return void 0;
2447
+ }
2448
+ function createClient() {
2449
+ const telemetry = getTelemetryOption();
1363
2450
  return new Kairos({
1364
2451
  anthropicApiKey: getEnvOrExit("ANTHROPIC_API_KEY"),
1365
2452
  n8nBaseUrl: getEnvOrExit("N8N_BASE_URL"),
1366
2453
  n8nApiKey: getEnvOrExit("N8N_API_KEY"),
1367
2454
  ...process.env["KAIROS_MODEL"] ? { model: process.env["KAIROS_MODEL"] } : {},
1368
2455
  ...telemetry !== void 0 ? { telemetry } : {},
1369
- logger: {
1370
- debug: () => {
1371
- },
1372
- info: () => {
1373
- },
1374
- warn: (msg) => console.error(`[warn] ${msg}`),
1375
- error: (msg) => console.error(`[error] ${msg}`)
1376
- }
2456
+ library: new FileLibrary(),
2457
+ logger: CLI_LOGGER
2458
+ });
2459
+ }
2460
+ function createDryRunClient() {
2461
+ const telemetry = getTelemetryOption();
2462
+ return new Kairos({
2463
+ anthropicApiKey: getEnvOrExit("ANTHROPIC_API_KEY"),
2464
+ ...process.env["N8N_BASE_URL"] ? { n8nBaseUrl: process.env["N8N_BASE_URL"] } : {},
2465
+ ...process.env["N8N_API_KEY"] ? { n8nApiKey: process.env["N8N_API_KEY"] } : {},
2466
+ ...process.env["KAIROS_MODEL"] ? { model: process.env["KAIROS_MODEL"] } : {},
2467
+ ...telemetry !== void 0 ? { telemetry } : {},
2468
+ library: new FileLibrary(),
2469
+ logger: CLI_LOGGER
1377
2470
  });
1378
2471
  }
1379
2472
  async function handleBuild(positional, flags) {
1380
- const description = positional[0];
2473
+ const description = positional.join(" ");
1381
2474
  if (!description) {
1382
2475
  console.error("Usage: kairos build <description> [--dry-run] [--name <name>] [--activate]");
1383
2476
  process.exit(1);
1384
2477
  }
1385
- const kairos = createClient();
2478
+ const isDryRun = flags["dry-run"] === true;
2479
+ const kairos = isDryRun ? createDryRunClient() : createClient();
1386
2480
  const start = Date.now();
1387
2481
  console.error(`Generating workflow...`);
1388
2482
  const result = await kairos.build(description, {
1389
- dryRun: flags["dry-run"] === true,
2483
+ dryRun: isDryRun,
1390
2484
  ...typeof flags["name"] === "string" ? { name: flags["name"] } : {},
1391
2485
  activate: flags["activate"] === true
1392
2486
  });
2487
+ await kairos.drain();
1393
2488
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
1394
2489
  console.error(`Done in ${elapsed}s (${result.generationAttempts} attempt${result.generationAttempts > 1 ? "s" : ""})`);
1395
2490
  console.error("");
@@ -1399,12 +2494,14 @@ async function handleBuild(positional, flags) {
1399
2494
  generationAttempts: result.generationAttempts,
1400
2495
  activationRequired: result.activationRequired,
1401
2496
  dryRun: result.dryRun,
1402
- credentialsNeeded: result.credentialsNeeded
2497
+ credentialsNeeded: result.credentialsNeeded,
2498
+ ...result.dryRun ? { workflow: result.workflow } : {}
1403
2499
  }, null, 2));
1404
2500
  }
1405
2501
  async function handleList() {
1406
2502
  const kairos = createClient();
1407
2503
  const workflows = await kairos.list();
2504
+ await kairos.drain();
1408
2505
  if (workflows.length === 0) {
1409
2506
  console.log("No workflows found.");
1410
2507
  return;
@@ -1424,6 +2521,7 @@ async function handleGet(positional) {
1424
2521
  }
1425
2522
  const kairos = createClient();
1426
2523
  const workflow = await kairos.get(id);
2524
+ await kairos.drain();
1427
2525
  console.log(JSON.stringify(workflow, null, 2));
1428
2526
  }
1429
2527
  async function handleActivate(positional) {
@@ -1434,6 +2532,7 @@ async function handleActivate(positional) {
1434
2532
  }
1435
2533
  const kairos = createClient();
1436
2534
  await kairos.activate(id);
2535
+ await kairos.drain();
1437
2536
  console.log(`Activated workflow ${id}`);
1438
2537
  }
1439
2538
  async function handleDeactivate(positional) {
@@ -1444,6 +2543,7 @@ async function handleDeactivate(positional) {
1444
2543
  }
1445
2544
  const kairos = createClient();
1446
2545
  await kairos.deactivate(id);
2546
+ await kairos.drain();
1447
2547
  console.log(`Deactivated workflow ${id}`);
1448
2548
  }
1449
2549
  async function handleDelete(positional, flags) {
@@ -1458,8 +2558,120 @@ async function handleDelete(positional, flags) {
1458
2558
  }
1459
2559
  const kairos = createClient();
1460
2560
  await kairos.delete(id, { confirm: true });
2561
+ await kairos.drain();
1461
2562
  console.log(`Deleted workflow ${id}`);
1462
2563
  }
2564
+ async function handleSyncTemplates(flags) {
2565
+ const max = typeof flags["max"] === "string" ? parseInt(flags["max"], 10) : 500;
2566
+ const library = new FileLibrary();
2567
+ const logger = {
2568
+ debug: () => {
2569
+ },
2570
+ info: (msg, meta) => console.error(meta ? `${msg} ${JSON.stringify(meta)}` : msg),
2571
+ warn: (msg, meta) => console.error(meta ? `[warn] ${msg} ${JSON.stringify(meta)}` : `[warn] ${msg}`),
2572
+ error: (msg, meta) => console.error(meta ? `[error] ${msg} ${JSON.stringify(meta)}` : `[error] ${msg}`)
2573
+ };
2574
+ const syncer = new TemplateSyncer(library, logger);
2575
+ console.error(`Syncing up to ${max} templates from n8n community library...`);
2576
+ const result = await syncer.sync({
2577
+ maxTemplates: max,
2578
+ onProgress: (p) => {
2579
+ if (p.processed % 25 === 0 && p.processed > 0) {
2580
+ console.error(` Progress: ${p.processed}/${p.total} processed, ${p.saved} saved`);
2581
+ }
2582
+ }
2583
+ });
2584
+ console.error("");
2585
+ console.error(`Sync complete:`);
2586
+ console.error(` Saved: ${result.saved}`);
2587
+ console.error(` Blocked: ${result.blocked} (validation errors or unsafe content)`);
2588
+ console.error(` Review: ${result.reviewed} (saved but flagged for review)`);
2589
+ console.error(` Duplicates: ${result.skippedDuplicate} (already in library)`);
2590
+ console.error(` Paid: ${result.skippedPaid} (skipped)`);
2591
+ }
2592
+ async function handleInit() {
2593
+ const { writeFile: writeFile2, readFile: readFile3, mkdir: mkdir3 } = await import("fs/promises");
2594
+ const { join: join4 } = await import("path");
2595
+ const { homedir: homedir4 } = await import("os");
2596
+ const readline = await import("readline");
2597
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
2598
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
2599
+ console.error("");
2600
+ console.error(" Kairos SDK \u2014 Setup Wizard");
2601
+ console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2602
+ console.error("");
2603
+ const envPath = join4(process.cwd(), ".env");
2604
+ let existingEnv = "";
2605
+ try {
2606
+ existingEnv = await readFile3(envPath, "utf-8");
2607
+ } catch {
2608
+ }
2609
+ const has = (key) => existingEnv.includes(key) || !!process.env[key];
2610
+ const lines = [];
2611
+ if (!has("ANTHROPIC_API_KEY")) {
2612
+ const key = await ask(" Anthropic API key (from console.anthropic.com): ");
2613
+ if (key.trim()) lines.push(`ANTHROPIC_API_KEY=${key.trim()}`);
2614
+ } else {
2615
+ console.error(" Anthropic API key: already set");
2616
+ }
2617
+ if (!has("N8N_BASE_URL")) {
2618
+ const url = await ask(" n8n instance URL (e.g. https://your-name.app.n8n.cloud): ");
2619
+ if (url.trim()) lines.push(`N8N_BASE_URL=${url.trim().replace(/\/$/, "")}`);
2620
+ } else {
2621
+ console.error(" n8n base URL: already set");
2622
+ }
2623
+ if (!has("N8N_API_KEY")) {
2624
+ const key = await ask(" n8n API key: ");
2625
+ if (key.trim()) lines.push(`N8N_API_KEY=${key.trim()}`);
2626
+ } else {
2627
+ console.error(" n8n API key: already set");
2628
+ }
2629
+ rl.close();
2630
+ if (lines.length > 0) {
2631
+ const newContent = existingEnv ? existingEnv.trimEnd() + "\n" + lines.join("\n") + "\n" : lines.join("\n") + "\n";
2632
+ await writeFile2(envPath, newContent, "utf-8");
2633
+ console.error(`
2634
+ Saved to ${envPath}`);
2635
+ } else {
2636
+ console.error("\n All credentials already configured.");
2637
+ }
2638
+ console.error("");
2639
+ console.error(" Seeding template library...");
2640
+ const library = new FileLibrary();
2641
+ const logger = {
2642
+ debug: () => {
2643
+ },
2644
+ info: () => {
2645
+ },
2646
+ warn: () => {
2647
+ },
2648
+ error: () => {
2649
+ }
2650
+ };
2651
+ const syncer = new TemplateSyncer(library, logger);
2652
+ await library.initialize();
2653
+ const existing = await library.list();
2654
+ if (existing.length >= 50) {
2655
+ console.error(` Library already has ${existing.length} entries \u2014 skipping sync.`);
2656
+ } else {
2657
+ const result = await syncer.sync({
2658
+ maxTemplates: 500,
2659
+ onProgress: (p) => {
2660
+ if (p.processed % 100 === 0 && p.processed > 0) {
2661
+ process.stderr.write(` ${p.processed}/${p.total} processed, ${p.saved} saved...\r`);
2662
+ }
2663
+ }
2664
+ });
2665
+ console.error(` Synced ${result.saved} templates (${result.blocked} blocked, ${result.skippedDuplicate} duplicates)`);
2666
+ }
2667
+ const kairosDir = join4(homedir4(), ".kairos");
2668
+ await mkdir3(join4(kairosDir, "telemetry"), { recursive: true });
2669
+ console.error("");
2670
+ console.error(" Setup complete! Try:");
2671
+ console.error("");
2672
+ console.error(' kairos build "Send a Slack message when a webhook fires" --dry-run');
2673
+ console.error("");
2674
+ }
1463
2675
  async function main() {
1464
2676
  const { command, positional, flags } = parseArgs(process.argv);
1465
2677
  if (!command || command === "help" || flags["help"] === true) {
@@ -1467,6 +2679,9 @@ async function main() {
1467
2679
  return;
1468
2680
  }
1469
2681
  switch (command) {
2682
+ case "init":
2683
+ await handleInit();
2684
+ break;
1470
2685
  case "build":
1471
2686
  await handleBuild(positional, flags);
1472
2687
  break;
@@ -1485,6 +2700,9 @@ async function main() {
1485
2700
  case "delete":
1486
2701
  await handleDelete(positional, flags);
1487
2702
  break;
2703
+ case "sync-templates":
2704
+ await handleSyncTemplates(flags);
2705
+ break;
1488
2706
  default:
1489
2707
  console.error(`Unknown command: ${command}`);
1490
2708
  console.log(HELP);
@@ -1494,14 +2712,13 @@ async function main() {
1494
2712
  main().catch((err) => {
1495
2713
  if (err instanceof Error) {
1496
2714
  console.error(`Error: ${err.message}`);
1497
- if ("issues" in err) {
1498
- const issues = err.issues;
1499
- for (const issue of issues) {
2715
+ if ("issues" in err && Array.isArray(err.issues)) {
2716
+ for (const issue of err.issues) {
1500
2717
  console.error(` [Rule ${issue.rule}] ${issue.message}`);
1501
2718
  }
1502
2719
  }
1503
2720
  } else {
1504
- console.error(err);
2721
+ console.error(String(err));
1505
2722
  }
1506
2723
  process.exit(1);
1507
2724
  });