@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/README.md +109 -15
- package/dist/{chunk-IADOKKFO.js → chunk-QQJDLS5A.js} +1190 -110
- package/dist/chunk-QQJDLS5A.js.map +1 -0
- package/dist/cli.cjs +1349 -132
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +171 -27
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1135 -153
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -13
- package/dist/index.d.ts +154 -13
- package/dist/index.js +18 -111
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
- package/dist/chunk-IADOKKFO.js.map +0 -1
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
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
176
|
-
|
|
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 (
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
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: `##
|
|
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: [
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1320
|
+
lastErrors = errors;
|
|
1051
1321
|
this.logger.warn(`WorkflowDesigner: validation failed on attempt ${attempt}`, {
|
|
1052
|
-
|
|
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
|
|
1058
|
-
|
|
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
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
});
|
|
1626
|
+
const provider = this.requireProvider();
|
|
1627
|
+
const deployed = await provider.deploy(workflow);
|
|
1628
|
+
this.recordDeploy();
|
|
1225
1629
|
if (options?.activate) {
|
|
1226
|
-
await
|
|
1630
|
+
await provider.activate(deployed.workflowId);
|
|
1227
1631
|
}
|
|
1228
|
-
const
|
|
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.
|
|
1777
|
+
return this.requireProvider().get(id);
|
|
1268
1778
|
}
|
|
1269
1779
|
async list() {
|
|
1270
|
-
return this.
|
|
1780
|
+
return this.requireProvider().list();
|
|
1271
1781
|
}
|
|
1272
1782
|
async activate(id) {
|
|
1273
|
-
await this.
|
|
1783
|
+
await this.requireProvider().activate(id);
|
|
1274
1784
|
}
|
|
1275
1785
|
async deactivate(id) {
|
|
1276
|
-
await this.
|
|
1786
|
+
await this.requireProvider().deactivate(id);
|
|
1277
1787
|
}
|
|
1278
1788
|
async delete(id, options) {
|
|
1279
|
-
await this.
|
|
1789
|
+
await this.requireProvider().delete(id, options);
|
|
1280
1790
|
}
|
|
1281
1791
|
async executions(workflowId, filter) {
|
|
1282
|
-
return this.
|
|
1792
|
+
return this.requireProvider().executions(workflowId, filter);
|
|
1283
1793
|
}
|
|
1284
1794
|
async execution(id) {
|
|
1285
|
-
return this.
|
|
1795
|
+
return this.requireProvider().execution(id);
|
|
1286
1796
|
}
|
|
1287
1797
|
async listTags() {
|
|
1288
|
-
return this.
|
|
1798
|
+
return this.requireProvider().listTags();
|
|
1289
1799
|
}
|
|
1290
1800
|
async createTag(name) {
|
|
1291
|
-
return this.
|
|
1801
|
+
return this.requireProvider().createTag(name);
|
|
1292
1802
|
}
|
|
1293
1803
|
async tag(workflowId, tagIds) {
|
|
1294
|
-
await this.
|
|
1804
|
+
await this.requireProvider().tag(workflowId, tagIds);
|
|
1295
1805
|
}
|
|
1296
1806
|
async untag(workflowId, tagIds) {
|
|
1297
|
-
await this.
|
|
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
|
-
|
|
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
|
-
|
|
1358
|
-
if (telemetryEnv
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
});
|