@kairos-sdk/core 0.4.0 → 0.5.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 +21 -10
- package/dist/chunk-5GAY7CSJ.js +411 -0
- package/dist/chunk-5GAY7CSJ.js.map +1 -0
- package/dist/chunk-6FOFWVMG.js +1 -0
- package/dist/chunk-6FOFWVMG.js.map +1 -0
- package/dist/chunk-EVOAYH2K.js +569 -0
- package/dist/chunk-EVOAYH2K.js.map +1 -0
- package/dist/{chunk-N6LRD2FN.js → chunk-HBGZTUUZ.js} +81 -380
- package/dist/chunk-HBGZTUUZ.js.map +1 -0
- package/dist/{chunk-NJ6QZBIC.js → chunk-KIFT5LA7.js} +971 -572
- package/dist/chunk-KIFT5LA7.js.map +1 -0
- package/dist/cli.cjs +1341 -236
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +83 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1259 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -540
- package/dist/index.d.ts +3 -540
- package/dist/index.js +9 -5
- package/dist/mcp-server.cjs +1473 -402
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +357 -232
- package/dist/mcp-server.js.map +1 -1
- package/dist/reader-B5mV20H6.d.cts +596 -0
- package/dist/reader-B5mV20H6.d.ts +596 -0
- package/dist/standalone.cjs +2978 -0
- package/dist/standalone.cjs.map +1 -0
- package/dist/standalone.d.cts +106 -0
- package/dist/standalone.d.ts +106 -0
- package/dist/standalone.js +58 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +9 -1
- package/dist/chunk-N6LRD2FN.js.map +0 -1
- package/dist/chunk-NJ6QZBIC.js.map +0 -1
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
// src/utils/uuid.ts
|
|
2
|
+
function generateUUID() {
|
|
3
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
4
|
+
return crypto.randomUUID();
|
|
5
|
+
}
|
|
6
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
7
|
+
const r = Math.random() * 16 | 0;
|
|
8
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
// src/errors/base.ts
|
|
2
13
|
var KairosError = class extends Error {
|
|
3
14
|
constructor(message, cause) {
|
|
@@ -29,7 +40,26 @@ var ProviderError = class extends KairosError {
|
|
|
29
40
|
}
|
|
30
41
|
};
|
|
31
42
|
|
|
43
|
+
// src/errors/guard-error.ts
|
|
44
|
+
var GuardError = class extends KairosError {
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "GuardError";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
32
51
|
// src/utils/retry.ts
|
|
52
|
+
function isTransientNetworkError(err) {
|
|
53
|
+
const TRANSIENT_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "ENOTFOUND", "ECONNABORTED"]);
|
|
54
|
+
let current = err;
|
|
55
|
+
for (let i = 0; i < 4; i++) {
|
|
56
|
+
if (current === null || typeof current !== "object") break;
|
|
57
|
+
const code = current.code;
|
|
58
|
+
if (typeof code === "string" && TRANSIENT_CODES.has(code)) return true;
|
|
59
|
+
current = current.cause;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
33
63
|
async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
|
|
34
64
|
let lastError;
|
|
35
65
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
@@ -54,6 +84,7 @@ function fetchWithTimeout(url, init, timeoutMs) {
|
|
|
54
84
|
|
|
55
85
|
// src/providers/n8n/api-client.ts
|
|
56
86
|
var EXECUTION_LIMIT_CAP = 100;
|
|
87
|
+
var N8N_API_PAGE_SIZE = 250;
|
|
57
88
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
58
89
|
var RETRY_ATTEMPTS = 3;
|
|
59
90
|
var RETRY_DELAY_MS = 1e3;
|
|
@@ -62,6 +93,17 @@ var N8nApiClient = class {
|
|
|
62
93
|
this.baseUrl = baseUrl;
|
|
63
94
|
this.apiKey = apiKey;
|
|
64
95
|
this.logger = logger;
|
|
96
|
+
if (!baseUrl || typeof baseUrl !== "string") {
|
|
97
|
+
throw new GuardError("N8nApiClient: baseUrl must be a non-empty string");
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
new URL(baseUrl);
|
|
101
|
+
} catch {
|
|
102
|
+
throw new GuardError(`N8nApiClient: baseUrl is not a valid URL: "${baseUrl}"`);
|
|
103
|
+
}
|
|
104
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
105
|
+
throw new GuardError("N8nApiClient: apiKey must be a non-empty string");
|
|
106
|
+
}
|
|
65
107
|
}
|
|
66
108
|
baseUrl;
|
|
67
109
|
apiKey;
|
|
@@ -71,7 +113,12 @@ var N8nApiClient = class {
|
|
|
71
113
|
this.logger.debug(`n8n ${method} ${path}`);
|
|
72
114
|
const isSafe = method === "GET";
|
|
73
115
|
if (!isSafe) {
|
|
74
|
-
return
|
|
116
|
+
return withRetry(
|
|
117
|
+
() => this.singleRequest(url, method, path, body),
|
|
118
|
+
2,
|
|
119
|
+
RETRY_DELAY_MS,
|
|
120
|
+
isTransientNetworkError
|
|
121
|
+
);
|
|
75
122
|
}
|
|
76
123
|
return withRetry(
|
|
77
124
|
() => this.singleRequest(url, method, path, body),
|
|
@@ -126,7 +173,7 @@ var N8nApiClient = class {
|
|
|
126
173
|
}
|
|
127
174
|
async listWorkflows() {
|
|
128
175
|
const all = [];
|
|
129
|
-
let path =
|
|
176
|
+
let path = `/workflows?limit=${N8N_API_PAGE_SIZE}`;
|
|
130
177
|
for (; ; ) {
|
|
131
178
|
const response = await this.request("GET", path);
|
|
132
179
|
for (const w of response.data) {
|
|
@@ -140,7 +187,7 @@ var N8nApiClient = class {
|
|
|
140
187
|
});
|
|
141
188
|
}
|
|
142
189
|
if (!response.nextCursor) break;
|
|
143
|
-
path = `/workflows?limit
|
|
190
|
+
path = `/workflows?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
144
191
|
}
|
|
145
192
|
return all;
|
|
146
193
|
}
|
|
@@ -170,14 +217,14 @@ var N8nApiClient = class {
|
|
|
170
217
|
}
|
|
171
218
|
async listTags() {
|
|
172
219
|
const all = [];
|
|
173
|
-
let path =
|
|
220
|
+
let path = `/tags?limit=${N8N_API_PAGE_SIZE}`;
|
|
174
221
|
for (; ; ) {
|
|
175
222
|
const response = await this.request("GET", path);
|
|
176
223
|
for (const t of response.data) {
|
|
177
224
|
all.push({ id: t.id, name: t.name });
|
|
178
225
|
}
|
|
179
226
|
if (!response.nextCursor) break;
|
|
180
|
-
path = `/tags?limit
|
|
227
|
+
path = `/tags?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
181
228
|
}
|
|
182
229
|
return all;
|
|
183
230
|
}
|
|
@@ -201,6 +248,32 @@ var N8nApiClient = class {
|
|
|
201
248
|
return [];
|
|
202
249
|
}
|
|
203
250
|
}
|
|
251
|
+
async triggerManual(workflowId) {
|
|
252
|
+
const raw = await this.request("POST", `/workflows/${workflowId}/run`);
|
|
253
|
+
const inner = raw["data"];
|
|
254
|
+
const execId = inner?.["executionId"] ?? raw["executionId"];
|
|
255
|
+
if (execId === void 0 || execId === null) {
|
|
256
|
+
throw new ProviderError(
|
|
257
|
+
`n8n trigger response missing executionId \u2014 got: ${JSON.stringify(raw)}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return String(execId);
|
|
261
|
+
}
|
|
262
|
+
async triggerWebhookTest(path) {
|
|
263
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
264
|
+
const url = `${this.baseUrl.replace(/\/$/, "")}/webhook-test${cleanPath}`;
|
|
265
|
+
this.logger.debug(`n8n POST webhook-test ${cleanPath}`);
|
|
266
|
+
try {
|
|
267
|
+
const response = await fetchWithTimeout(
|
|
268
|
+
url,
|
|
269
|
+
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) },
|
|
270
|
+
REQUEST_TIMEOUT_MS
|
|
271
|
+
);
|
|
272
|
+
return response.status;
|
|
273
|
+
} catch (err) {
|
|
274
|
+
throw new ProviderError(`Webhook test request failed for path "${path}"`, err);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
204
277
|
mapExecution(e) {
|
|
205
278
|
return {
|
|
206
279
|
id: e.id,
|
|
@@ -348,6 +421,14 @@ var NodeRegistry = class {
|
|
|
348
421
|
if (!def) return true;
|
|
349
422
|
return def.safeTypeVersions.includes(version);
|
|
350
423
|
}
|
|
424
|
+
// Returns true when the version is a positive integer greater than the highest
|
|
425
|
+
// known safe version — indicates a newer release rather than a bad value.
|
|
426
|
+
isVersionNewer(type, version) {
|
|
427
|
+
const def = this.byType.get(type);
|
|
428
|
+
if (!def || def.safeTypeVersions.length === 0) return false;
|
|
429
|
+
const max = Math.max(...def.safeTypeVersions);
|
|
430
|
+
return Number.isInteger(version) && version > max;
|
|
431
|
+
}
|
|
351
432
|
getRequiredParams(type) {
|
|
352
433
|
return this.byType.get(type)?.requiredParams ?? [];
|
|
353
434
|
}
|
|
@@ -397,6 +478,17 @@ var N8nValidator = class {
|
|
|
397
478
|
this.checkRule21(workflow, issues);
|
|
398
479
|
this.checkRule22(workflow, issues);
|
|
399
480
|
this.checkRule23(workflow, issues);
|
|
481
|
+
this.checkRule24(workflow, issues);
|
|
482
|
+
this.checkRule25(workflow, issues);
|
|
483
|
+
this.checkRule26(workflow, issues);
|
|
484
|
+
this.checkRule27(workflow, issues);
|
|
485
|
+
this.checkRule28(workflow, issues);
|
|
486
|
+
this.checkRule29(workflow, issues);
|
|
487
|
+
this.checkRule30(workflow, issues);
|
|
488
|
+
this.checkRule31(workflow, issues);
|
|
489
|
+
this.checkRule32(workflow, issues);
|
|
490
|
+
this.checkRule33(workflow, issues);
|
|
491
|
+
this.checkRule34(workflow, issues);
|
|
400
492
|
if (Array.isArray(workflow.nodes)) {
|
|
401
493
|
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
402
494
|
for (const issue of issues) {
|
|
@@ -529,10 +621,14 @@ var N8nValidator = class {
|
|
|
529
621
|
checkRule11(w, issues) {
|
|
530
622
|
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
531
623
|
const reachable = /* @__PURE__ */ new Set();
|
|
532
|
-
|
|
624
|
+
const aiSubNodeSources = /* @__PURE__ */ new Set();
|
|
625
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
533
626
|
if (typeof outputs !== "object" || outputs === null) continue;
|
|
534
|
-
|
|
627
|
+
let hasAiPort = false;
|
|
628
|
+
for (const [portName, portGroup] of Object.entries(outputs)) {
|
|
535
629
|
if (!Array.isArray(portGroup)) continue;
|
|
630
|
+
const isAiPort = portName.startsWith("ai_");
|
|
631
|
+
if (isAiPort) hasAiPort = true;
|
|
536
632
|
for (const targets of portGroup) {
|
|
537
633
|
if (!Array.isArray(targets)) continue;
|
|
538
634
|
for (const target of targets) {
|
|
@@ -541,10 +637,13 @@ var N8nValidator = class {
|
|
|
541
637
|
}
|
|
542
638
|
}
|
|
543
639
|
}
|
|
640
|
+
if (hasAiPort) aiSubNodeSources.add(sourceName);
|
|
544
641
|
}
|
|
545
642
|
for (const node of w.nodes) {
|
|
546
643
|
if (node.type.includes("stickyNote")) continue;
|
|
547
|
-
if (
|
|
644
|
+
if (this.isTriggerNode(node)) continue;
|
|
645
|
+
if (aiSubNodeSources.has(node.name)) continue;
|
|
646
|
+
if (!reachable.has(node.name)) {
|
|
548
647
|
this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
|
|
549
648
|
}
|
|
550
649
|
}
|
|
@@ -641,19 +740,22 @@ var N8nValidator = class {
|
|
|
641
740
|
}
|
|
642
741
|
}
|
|
643
742
|
}
|
|
644
|
-
// Rule 19 (WARN): typeVersion is within known safe range for registered node types
|
|
743
|
+
// Rule 19 (WARN): typeVersion is within known safe range for registered node types.
|
|
744
|
+
// In lenient mode (KAIROS_REGISTRY_STRICT != 'true'), versions higher than the known
|
|
745
|
+
// max are allowed — they likely represent newer n8n releases Kairos hasn't catalogued yet.
|
|
645
746
|
checkRule19(w, issues) {
|
|
646
747
|
if (!Array.isArray(w.nodes)) return;
|
|
748
|
+
const strict = process.env["KAIROS_REGISTRY_STRICT"] === "true";
|
|
647
749
|
for (const node of w.nodes) {
|
|
648
750
|
if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
751
|
+
if (this.registry.isVersionSafe(node.type, node.typeVersion)) continue;
|
|
752
|
+
if (!strict && this.registry.isVersionNewer(node.type, node.typeVersion)) continue;
|
|
753
|
+
this.warn(
|
|
754
|
+
issues,
|
|
755
|
+
19,
|
|
756
|
+
`Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
|
|
757
|
+
node.id
|
|
758
|
+
);
|
|
657
759
|
}
|
|
658
760
|
}
|
|
659
761
|
// Rule 20 (WARN): cycle detection — no node should be reachable from itself
|
|
@@ -702,6 +804,27 @@ var N8nValidator = class {
|
|
|
702
804
|
}
|
|
703
805
|
}
|
|
704
806
|
}
|
|
807
|
+
// Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
|
|
808
|
+
checkRule21(w, issues) {
|
|
809
|
+
if (!Array.isArray(w.nodes)) return;
|
|
810
|
+
const webhooksNeedingResponse = w.nodes.filter((n) => {
|
|
811
|
+
if (!n.type.includes("webhook")) return false;
|
|
812
|
+
const params = n.parameters;
|
|
813
|
+
return params?.responseMode === "responseNode";
|
|
814
|
+
});
|
|
815
|
+
if (webhooksNeedingResponse.length === 0) return;
|
|
816
|
+
const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
|
|
817
|
+
if (!hasRespondNode) {
|
|
818
|
+
for (const wh of webhooksNeedingResponse) {
|
|
819
|
+
this.warn(
|
|
820
|
+
issues,
|
|
821
|
+
21,
|
|
822
|
+
`Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
|
|
823
|
+
wh.id
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
705
828
|
// Rule 22 (WARN): check requiredParams from registry
|
|
706
829
|
checkRule22(w, issues) {
|
|
707
830
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -740,493 +863,272 @@ var N8nValidator = class {
|
|
|
740
863
|
}
|
|
741
864
|
}
|
|
742
865
|
}
|
|
743
|
-
// Rule
|
|
744
|
-
|
|
866
|
+
// Rule 24 (WARN): deprecated accessor syntax in expressions
|
|
867
|
+
checkRule24(w, issues) {
|
|
745
868
|
if (!Array.isArray(w.nodes)) return;
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
`Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
|
|
759
|
-
wh.id
|
|
760
|
-
);
|
|
869
|
+
const deprecated = /\$node\s*\[/;
|
|
870
|
+
for (const node of w.nodes) {
|
|
871
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
872
|
+
if (deprecated.test(expr)) {
|
|
873
|
+
this.warn(
|
|
874
|
+
issues,
|
|
875
|
+
24,
|
|
876
|
+
`Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
|
|
877
|
+
node.id
|
|
878
|
+
);
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
761
881
|
}
|
|
762
882
|
}
|
|
763
883
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
"name": "<descriptive name>",
|
|
782
|
-
"nodes": [...],
|
|
783
|
-
"connections": {...},
|
|
784
|
-
"settings": {
|
|
785
|
-
"saveExecutionProgress": true,
|
|
786
|
-
"saveManualExecutions": true,
|
|
787
|
-
"saveDataErrorExecution": "all",
|
|
788
|
-
"saveDataSuccessExecution": "all",
|
|
789
|
-
"executionTimeout": 3600,
|
|
790
|
-
"timezone": "UTC",
|
|
791
|
-
"executionOrder": "v1"
|
|
884
|
+
// Rule 25 (WARN): wrong item index assumptions in expressions
|
|
885
|
+
checkRule25(w, issues) {
|
|
886
|
+
if (!Array.isArray(w.nodes)) return;
|
|
887
|
+
const itemIndex = /\$json\s*\.\s*items\s*\[/;
|
|
888
|
+
for (const node of w.nodes) {
|
|
889
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
890
|
+
if (itemIndex.test(expr)) {
|
|
891
|
+
this.warn(
|
|
892
|
+
issues,
|
|
893
|
+
25,
|
|
894
|
+
`Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
|
|
895
|
+
node.id
|
|
896
|
+
);
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
792
901
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
- Trigger node: [250, 300]
|
|
810
|
-
- Each subsequent step: x + 220 minimum
|
|
811
|
-
- Parallel branches: offset y by \xB1150
|
|
812
|
-
- AI sub-nodes: place below their root node (y + 200)
|
|
813
|
-
|
|
814
|
-
---
|
|
815
|
-
|
|
816
|
-
## CONNECTION RULES \u2014 the most common source of errors
|
|
817
|
-
|
|
818
|
-
### Standard connections (main data flow):
|
|
819
|
-
"NodeA": { "main": [ [ { "node": "NodeB", "type": "main", "index": 0 } ] ] }
|
|
820
|
-
|
|
821
|
-
### AI connections \u2014 CRITICAL: the SUB-NODE is the SOURCE, NOT the agent/chain:
|
|
822
|
-
"OpenAI Chat Model": { "ai_languageModel": [ [ { "node": "AI Agent", "type": "ai_languageModel", "index": 0 } ] ] }
|
|
823
|
-
"Simple Memory": { "ai_memory": [ [ { "node": "AI Agent", "type": "ai_memory", "index": 0 } ] ] }
|
|
824
|
-
"Calculator Tool": { "ai_tool": [ [ { "node": "AI Agent", "type": "ai_tool", "index": 0 } ] ] }
|
|
825
|
-
|
|
826
|
-
The AI Agent node does NOT appear in connections as a source for ai_* types.
|
|
827
|
-
Every AI Agent must have at least one ai_languageModel sub-node connected.
|
|
828
|
-
|
|
829
|
-
### IF node \u2014 two output ports (0 = true, 1 = false):
|
|
830
|
-
"IF Check": { "main": [ [{ "node": "True Path", "type": "main", "index": 0 }], [{ "node": "False Path", "type": "main", "index": 0 }] ] }
|
|
831
|
-
|
|
832
|
-
### SplitInBatches \u2014 two output ports (0 = done/finished, 1 = loop body per batch):
|
|
833
|
-
Connect output 0 to the node that runs AFTER all batches complete.
|
|
834
|
-
Connect output 1 to the processing chain for each batch. The last node in the chain loops back to SplitInBatches via main input.
|
|
835
|
-
|
|
836
|
-
### Webhook + RespondToWebhook pattern:
|
|
837
|
-
When webhook responseMode is "responseNode", you MUST include a respondToWebhook node in the flow.
|
|
838
|
-
"Webhook": { "main": [[{ "node": "Process Data", "type": "main", "index": 0 }]] }
|
|
839
|
-
"Process Data": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] }
|
|
840
|
-
|
|
841
|
-
### Triggers have no incoming connections.
|
|
842
|
-
### Connection keys are NODE NAMES, never node IDs.
|
|
843
|
-
|
|
844
|
-
### Nested parameters:
|
|
845
|
-
Node parameters like conditions, assignments, and rule intervals MUST include all required nested fields. Do not leave nested objects empty or partially filled.
|
|
846
|
-
|
|
847
|
-
---
|
|
848
|
-
|
|
849
|
-
## NODE CATALOG \u2014 exact type strings and safe typeVersions
|
|
850
|
-
|
|
851
|
-
### Triggers (always at least one required):
|
|
852
|
-
n8n-nodes-base.manualTrigger typeVersion: 1 \u2014 testing only
|
|
853
|
-
n8n-nodes-base.scheduleTrigger typeVersion: 1.2 \u2014 params: rule.interval[{field, ...}]
|
|
854
|
-
n8n-nodes-base.webhook typeVersion: 2 \u2014 params: httpMethod, path, responseMode
|
|
855
|
-
n8n-nodes-base.formTrigger typeVersion: 2.2
|
|
856
|
-
n8n-nodes-base.emailReadImap typeVersion: 2 \u2014 cred: imap
|
|
857
|
-
n8n-nodes-base.errorTrigger typeVersion: 1
|
|
858
|
-
n8n-nodes-base.executeWorkflowTrigger typeVersion: 1.1
|
|
859
|
-
n8n-nodes-base.gmailTrigger typeVersion: 1.2 \u2014 cred: gmailOAuth2
|
|
860
|
-
n8n-nodes-base.slackTrigger typeVersion: 1 \u2014 cred: slackApi
|
|
861
|
-
n8n-nodes-base.telegramTrigger typeVersion: 1.2 \u2014 cred: telegramApi
|
|
862
|
-
n8n-nodes-base.githubTrigger typeVersion: 1 \u2014 cred: githubApi
|
|
863
|
-
n8n-nodes-base.airtableTrigger typeVersion: 1 \u2014 cred: airtableTokenApi
|
|
864
|
-
n8n-nodes-base.notionTrigger typeVersion: 1 \u2014 cred: notionApi
|
|
865
|
-
@n8n/n8n-nodes-langchain.chatTrigger typeVersion: 1.1 \u2014 pairs with AI Agent
|
|
866
|
-
|
|
867
|
-
### Core logic:
|
|
868
|
-
n8n-nodes-base.code typeVersion: 2 \u2014 params: mode, jsCode
|
|
869
|
-
n8n-nodes-base.httpRequest typeVersion: 4.2 \u2014 params: method, url, [sendBody, jsonBody, sendHeaders, headerParameters]
|
|
870
|
-
n8n-nodes-base.set typeVersion: 3.4 \u2014 params: assignments.assignments[{id, name, value, type}]
|
|
871
|
-
n8n-nodes-base.if typeVersion: 2.2 \u2014 params: conditions.conditions[{id, leftValue, rightValue, operator}], combinator
|
|
872
|
-
n8n-nodes-base.switch typeVersion: 3.2 \u2014 multi-branch routing
|
|
873
|
-
n8n-nodes-base.filter typeVersion: 2.2 \u2014 params: conditions (same as IF), 1 output
|
|
874
|
-
n8n-nodes-base.merge typeVersion: 3 \u2014 modes: append/combine/chooseBranch
|
|
875
|
-
n8n-nodes-base.splitInBatches typeVersion: 3 \u2014 output 0=done, output 1=loop body
|
|
876
|
-
n8n-nodes-base.wait typeVersion: 1.1
|
|
877
|
-
n8n-nodes-base.executeWorkflow typeVersion: 1.2
|
|
878
|
-
n8n-nodes-base.respondToWebhook typeVersion: 1.1 \u2014 required when webhook responseMode is "responseNode"
|
|
879
|
-
n8n-nodes-base.noOp typeVersion: 1
|
|
880
|
-
n8n-nodes-base.splitOut typeVersion: 1
|
|
881
|
-
n8n-nodes-base.aggregate typeVersion: 1
|
|
882
|
-
n8n-nodes-base.stickyNote typeVersion: 1 \u2014 never connected, canvas annotation only
|
|
883
|
-
|
|
884
|
-
### Email / messaging:
|
|
885
|
-
n8n-nodes-base.emailSend typeVersion: 2.1 \u2014 cred: smtp
|
|
886
|
-
n8n-nodes-base.slack typeVersion: 2.2 \u2014 cred: slackOAuth2Api \u2014 params: resource, operation, select, channelId{__rl}, text
|
|
887
|
-
n8n-nodes-base.telegram typeVersion: 1.2 \u2014 cred: telegramApi
|
|
888
|
-
n8n-nodes-base.discord typeVersion: 2 \u2014 cred: discordWebhookApi
|
|
889
|
-
|
|
890
|
-
### Google:
|
|
891
|
-
n8n-nodes-base.gmail typeVersion: 2.1 \u2014 cred: gmailOAuth2 \u2014 params: resource, operation
|
|
892
|
-
n8n-nodes-base.googleSheets typeVersion: 4.5 \u2014 cred: googleSheetsOAuth2Api \u2014 params: resource, operation, documentId{__rl}, sheetName{__rl}
|
|
893
|
-
n8n-nodes-base.googleDrive typeVersion: 3 \u2014 cred: googleDriveOAuth2Api
|
|
894
|
-
n8n-nodes-base.googleCalendar typeVersion: 1.3 \u2014 cred: googleCalendarOAuth2Api
|
|
895
|
-
|
|
896
|
-
### Productivity:
|
|
897
|
-
n8n-nodes-base.notion typeVersion: 2.2 \u2014 cred: notionApi
|
|
898
|
-
n8n-nodes-base.airtable typeVersion: 2.1 \u2014 cred: airtableTokenApi
|
|
899
|
-
n8n-nodes-base.github typeVersion: 1.1 \u2014 cred: githubApi
|
|
900
|
-
n8n-nodes-base.jira typeVersion: 1 \u2014 cred: jiraSoftwareCloudApi
|
|
901
|
-
n8n-nodes-base.hubspot typeVersion: 2.1 \u2014 cred: hubspotOAuth2Api
|
|
902
|
-
|
|
903
|
-
### Databases:
|
|
904
|
-
n8n-nodes-base.postgres typeVersion: 2.5 \u2014 cred: postgres
|
|
905
|
-
n8n-nodes-base.mySql typeVersion: 2.4 \u2014 cred: mySql
|
|
906
|
-
n8n-nodes-base.redis typeVersion: 1 \u2014 cred: redis
|
|
907
|
-
n8n-nodes-base.supabase typeVersion: 1 \u2014 cred: supabaseApi
|
|
908
|
-
n8n-nodes-base.awsS3 typeVersion: 2 \u2014 cred: aws
|
|
909
|
-
|
|
910
|
-
### AI \u2014 Root nodes (sit on main data flow, receive ai_* connections as TARGETS):
|
|
911
|
-
@n8n/n8n-nodes-langchain.agent typeVersion: 1.9 \u2014 params: promptType, text (if define), options.systemMessage
|
|
912
|
-
@n8n/n8n-nodes-langchain.chainLlm typeVersion: 1.5
|
|
913
|
-
@n8n/n8n-nodes-langchain.chainRetrievalQa typeVersion: 1.4
|
|
914
|
-
@n8n/n8n-nodes-langchain.openAi typeVersion: 1.8 \u2014 cred: openAiApi \u2014 standalone node, calls OpenAI directly without sub-nodes
|
|
915
|
-
@n8n/n8n-nodes-langchain.anthropic typeVersion: 1 \u2014 cred: anthropicApi \u2014 standalone node, calls Anthropic directly without sub-nodes
|
|
916
|
-
|
|
917
|
-
### AI \u2014 Sub-nodes (sources of ai_* connections, wire INTO root nodes above):
|
|
918
|
-
@n8n/n8n-nodes-langchain.lmChatOpenAi typeVersion: 1.7 \u2014 cred: openAiApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
|
|
919
|
-
@n8n/n8n-nodes-langchain.lmChatAnthropic typeVersion: 1.3 \u2014 cred: anthropicApi \u2014 ai_languageModel \u2014 use with agent/chain, NOT standalone
|
|
920
|
-
@n8n/n8n-nodes-langchain.lmChatGoogleGemini typeVersion: 1 \u2014 cred: googlePalmApi \u2014 ai_languageModel
|
|
921
|
-
@n8n/n8n-nodes-langchain.memoryBufferWindow typeVersion: 1.3 \u2014 \u2014 ai_memory
|
|
922
|
-
@n8n/n8n-nodes-langchain.toolWorkflow typeVersion: 2 \u2014 \u2014 ai_tool
|
|
923
|
-
@n8n/n8n-nodes-langchain.toolCode typeVersion: 1.1 \u2014 \u2014 ai_tool
|
|
924
|
-
@n8n/n8n-nodes-langchain.toolHttpRequest typeVersion: 1.1 \u2014 \u2014 ai_tool
|
|
925
|
-
@n8n/n8n-nodes-langchain.toolCalculator typeVersion: 1 \u2014 \u2014 ai_tool
|
|
926
|
-
|
|
927
|
-
### Resource locator (__rl) format (Google / Slack / Notion modern nodes):
|
|
928
|
-
{ "__rl": true, "mode": "id", "value": "ACTUAL_ID" }
|
|
929
|
-
{ "__rl": true, "mode": "name", "value": "#channel-name" }
|
|
930
|
-
|
|
931
|
-
### App node parameter pattern:
|
|
932
|
-
{ "resource": "message", "operation": "send", ...operation-specific fields }
|
|
933
|
-
|
|
934
|
-
### Schedule Trigger \u2014 daily at 9am example:
|
|
935
|
-
{ "rule": { "interval": [{ "field": "days", "daysInterval": 1, "triggerAtHour": 9, "triggerAtMinute": 0 }] } }
|
|
936
|
-
Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] } }
|
|
937
|
-
|
|
938
|
-
---
|
|
939
|
-
|
|
940
|
-
## PRE-DELIVERY SELF-CHECK (do this before calling the tool):
|
|
941
|
-
1. Every connection source/target name exists in nodes array
|
|
942
|
-
2. No duplicate node names
|
|
943
|
-
3. No duplicate node IDs
|
|
944
|
-
4. No forbidden fields at the workflow root
|
|
945
|
-
5. At least one trigger node present
|
|
946
|
-
6. Every AI Agent has an ai_languageModel sub-node
|
|
947
|
-
7. settings block is complete with executionOrder: "v1"
|
|
948
|
-
|
|
949
|
-
---
|
|
950
|
-
|
|
951
|
-
Respond ONLY with a generate_workflow tool call. No prose. No markdown outside the tool call.
|
|
952
|
-
If the request is impossible or unclear, set the error field instead of generating a workflow.`;
|
|
953
|
-
|
|
954
|
-
// src/utils/thresholds.ts
|
|
955
|
-
var DIRECT_THRESHOLD = 0.92;
|
|
956
|
-
var REFERENCE_THRESHOLD = 0.72;
|
|
957
|
-
function scoreToMode(score) {
|
|
958
|
-
if (score >= DIRECT_THRESHOLD) return "direct";
|
|
959
|
-
if (score >= REFERENCE_THRESHOLD) return "reference";
|
|
960
|
-
return "scratch";
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
// src/validation/rule-metadata.ts
|
|
964
|
-
var VALIDATOR_RULE_IDS = Array.from({ length: 23 }, (_, i) => i + 1);
|
|
965
|
-
var RULE_PIPELINE_STAGES = {
|
|
966
|
-
1: "node_generation",
|
|
967
|
-
2: "node_generation",
|
|
968
|
-
3: "node_generation",
|
|
969
|
-
4: "node_generation",
|
|
970
|
-
5: "node_generation",
|
|
971
|
-
6: "node_generation",
|
|
972
|
-
7: "node_generation",
|
|
973
|
-
8: "node_generation",
|
|
974
|
-
9: "connection_wiring",
|
|
975
|
-
10: "connection_wiring",
|
|
976
|
-
11: "connection_wiring",
|
|
977
|
-
12: "workflow_structure",
|
|
978
|
-
13: "node_generation",
|
|
979
|
-
14: "workflow_structure",
|
|
980
|
-
15: "node_generation",
|
|
981
|
-
16: "node_generation",
|
|
982
|
-
17: "credential_injection",
|
|
983
|
-
18: "connection_wiring",
|
|
984
|
-
19: "node_generation",
|
|
985
|
-
20: "connection_wiring",
|
|
986
|
-
21: "workflow_structure",
|
|
987
|
-
22: "workflow_structure",
|
|
988
|
-
23: "node_generation"
|
|
989
|
-
};
|
|
990
|
-
var RULE_MITIGATIONS = {
|
|
991
|
-
1: "Provide a non-empty workflow name string",
|
|
992
|
-
2: "Include at least one node in the nodes array",
|
|
993
|
-
3: "Every node must have a unique UUID v4 string as its id field",
|
|
994
|
-
4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
|
|
995
|
-
5: "Every node must have a non-empty type string",
|
|
996
|
-
6: "Every node must have a positive integer typeVersion",
|
|
997
|
-
7: "Every node must have a position array of exactly [x, y] numbers",
|
|
998
|
-
8: "Every node must have a non-empty name string",
|
|
999
|
-
9: "connections must be a plain object (use {} if no connections)",
|
|
1000
|
-
10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
|
|
1001
|
-
11: "Every non-trigger node should have at least one incoming connection",
|
|
1002
|
-
12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
|
|
1003
|
-
13: "workflow.settings must be a plain object if present",
|
|
1004
|
-
14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
|
|
1005
|
-
15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
|
|
1006
|
-
16: "All node names must be unique within the workflow",
|
|
1007
|
-
17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
|
|
1008
|
-
18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
|
|
1009
|
-
19: "Use known safe typeVersion values for each node type",
|
|
1010
|
-
20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
|
|
1011
|
-
21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
|
|
1012
|
-
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
|
|
1013
|
-
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync"
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
// src/generation/prompt-builder.ts
|
|
1017
|
-
var CRITICAL_SCORE_THRESHOLD = 0.15;
|
|
1018
|
-
var PromptBuilder = class {
|
|
1019
|
-
patternsPath;
|
|
1020
|
-
constructor(patternsPath) {
|
|
1021
|
-
this.patternsPath = patternsPath ?? join(homedir(), ".kairos", "patterns.json");
|
|
1022
|
-
}
|
|
1023
|
-
build(request, matches, globalFailureRates = [], dynamicCatalog) {
|
|
1024
|
-
const mode = this.resolveMode(matches);
|
|
1025
|
-
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
|
|
1026
|
-
const userMessage = this.buildUserMessage(request, matches, mode);
|
|
1027
|
-
return { system, userMessage, mode, matches };
|
|
1028
|
-
}
|
|
1029
|
-
buildCorrectionMessage(request, matches, allIssues, attempt) {
|
|
1030
|
-
const base = this.buildUserMessage(request, matches, this.resolveMode(matches));
|
|
1031
|
-
return `${base}
|
|
1032
|
-
|
|
1033
|
-
IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
|
|
1034
|
-
${allIssues.join("\n")}
|
|
1035
|
-
|
|
1036
|
-
Fix ALL of the above issues in your new response. Do not repeat any of these mistakes.`;
|
|
1037
|
-
}
|
|
1038
|
-
resolveMode(matches) {
|
|
1039
|
-
if (matches.length === 0) return "scratch";
|
|
1040
|
-
const top = matches[0];
|
|
1041
|
-
if (!top) return "scratch";
|
|
1042
|
-
return scoreToMode(top.score);
|
|
1043
|
-
}
|
|
1044
|
-
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog) {
|
|
1045
|
-
let basePrompt = SYSTEM_PROMPT_V1;
|
|
1046
|
-
if (dynamicCatalog) {
|
|
1047
|
-
basePrompt = basePrompt.replace(
|
|
1048
|
-
/## NODE CATALOG — exact type strings and safe typeVersions[\s\S]*?(?=## PRE-DELIVERY SELF-CHECK)/,
|
|
1049
|
-
dynamicCatalog + "\n\n"
|
|
1050
|
-
);
|
|
902
|
+
// Rule 26 (WARN): missing .first() or .all() on node references
|
|
903
|
+
checkRule26(w, issues) {
|
|
904
|
+
if (!Array.isArray(w.nodes)) return;
|
|
905
|
+
const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
|
|
906
|
+
for (const node of w.nodes) {
|
|
907
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
908
|
+
if (bareRef.test(expr)) {
|
|
909
|
+
this.warn(
|
|
910
|
+
issues,
|
|
911
|
+
26,
|
|
912
|
+
`Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
|
|
913
|
+
node.id
|
|
914
|
+
);
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
1051
918
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
919
|
+
}
|
|
920
|
+
extractExpressions(params) {
|
|
921
|
+
const expressions = [];
|
|
922
|
+
const walk = (val) => {
|
|
923
|
+
if (typeof val === "string") {
|
|
924
|
+
if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
|
|
925
|
+
expressions.push(val);
|
|
926
|
+
}
|
|
927
|
+
} else if (Array.isArray(val)) {
|
|
928
|
+
for (const item of val) walk(item);
|
|
929
|
+
} else if (val !== null && typeof val === "object") {
|
|
930
|
+
for (const v of Object.values(val)) walk(v);
|
|
1057
931
|
}
|
|
932
|
+
};
|
|
933
|
+
walk(params);
|
|
934
|
+
return expressions;
|
|
935
|
+
}
|
|
936
|
+
// Rule 27 (WARN): httpRequest URL is a placeholder
|
|
937
|
+
checkRule27(w, issues) {
|
|
938
|
+
if (!Array.isArray(w.nodes)) return;
|
|
939
|
+
const PLACEHOLDER_RE = [
|
|
940
|
+
/^https?:\/\/example\.com/i,
|
|
941
|
+
/your[-_]?(api[-_]?)?url/i,
|
|
942
|
+
/^https?:\/\/$/,
|
|
943
|
+
/^<.+>$/,
|
|
944
|
+
/placeholder/i
|
|
1058
945
|
];
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
1073
|
-
if (mode === "direct" && matches[0]) {
|
|
1074
|
-
const match = matches[0];
|
|
1075
|
-
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1076
|
-
if (json.length > 3e4) {
|
|
1077
|
-
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1078
|
-
blocks.push({
|
|
1079
|
-
type: "text",
|
|
1080
|
-
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
|
|
1081
|
-
Nodes:
|
|
1082
|
-
${nodes}`
|
|
1083
|
-
});
|
|
1084
|
-
} else {
|
|
1085
|
-
blocks.push({
|
|
1086
|
-
type: "text",
|
|
1087
|
-
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
|
|
1088
|
-
|
|
1089
|
-
${json}`
|
|
1090
|
-
});
|
|
946
|
+
for (const node of w.nodes) {
|
|
947
|
+
if (node.type !== "n8n-nodes-base.httpRequest") continue;
|
|
948
|
+
const params = node.parameters;
|
|
949
|
+
const url = params?.["url"];
|
|
950
|
+
if (typeof url !== "string" || url.trim() === "") continue;
|
|
951
|
+
if (PLACEHOLDER_RE.some((re) => re.test(url.trim()))) {
|
|
952
|
+
this.warn(
|
|
953
|
+
issues,
|
|
954
|
+
27,
|
|
955
|
+
`Node "${node.name}" httpRequest URL appears to be a placeholder: "${url}" \u2014 replace with your actual endpoint`,
|
|
956
|
+
node.id
|
|
957
|
+
);
|
|
1091
958
|
}
|
|
1092
959
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
960
|
+
}
|
|
961
|
+
// Rule 28 (WARN): code node with empty or comment-only code
|
|
962
|
+
checkRule28(w, issues) {
|
|
963
|
+
if (!Array.isArray(w.nodes)) return;
|
|
964
|
+
for (const node of w.nodes) {
|
|
965
|
+
if (node.type !== "n8n-nodes-base.code") continue;
|
|
966
|
+
const params = node.parameters;
|
|
967
|
+
const jsCode = typeof params?.["jsCode"] === "string" ? params["jsCode"] : "";
|
|
968
|
+
const pythonCode = typeof params?.["pythonCode"] === "string" ? params["pythonCode"] : "";
|
|
969
|
+
const code = jsCode || pythonCode;
|
|
970
|
+
const stripped = code.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/#[^\n]*/g, "").trim();
|
|
971
|
+
if (!stripped) {
|
|
972
|
+
this.warn(issues, 28, `Node "${node.name}" code node has no executable code`, node.id);
|
|
973
|
+
}
|
|
1105
974
|
}
|
|
1106
|
-
return blocks;
|
|
1107
975
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
976
|
+
// Rule 29 (WARN): slack node message operation missing channel
|
|
977
|
+
checkRule29(w, issues) {
|
|
978
|
+
if (!Array.isArray(w.nodes)) return;
|
|
979
|
+
for (const node of w.nodes) {
|
|
980
|
+
if (node.type !== "n8n-nodes-base.slack") continue;
|
|
981
|
+
const params = node.parameters;
|
|
982
|
+
const resource = params?.["resource"];
|
|
983
|
+
const operation = params?.["operation"];
|
|
984
|
+
const isMessageOp = resource === "message" || operation === "sendMessage" || operation === "post";
|
|
985
|
+
if (!isMessageOp) continue;
|
|
986
|
+
const channel = params?.["channel"] ?? params?.["channelId"];
|
|
987
|
+
const rlValue = typeof channel === "object" && channel !== null ? channel["value"] : void 0;
|
|
988
|
+
const isEmpty = channel === void 0 || channel === null || typeof channel === "string" && channel.trim() === "" || typeof channel === "object" && (!rlValue || typeof rlValue === "string" && rlValue.trim() === "");
|
|
989
|
+
if (isEmpty) {
|
|
990
|
+
this.warn(issues, 29, `Node "${node.name}" Slack message has no channel specified`, node.id);
|
|
991
|
+
}
|
|
1116
992
|
}
|
|
1117
993
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
994
|
+
// Rule 30 (WARN): gmail node send operation missing recipient
|
|
995
|
+
checkRule30(w, issues) {
|
|
996
|
+
if (!Array.isArray(w.nodes)) return;
|
|
997
|
+
for (const node of w.nodes) {
|
|
998
|
+
if (node.type !== "n8n-nodes-base.gmail") continue;
|
|
999
|
+
const params = node.parameters;
|
|
1000
|
+
const operation = params?.["operation"];
|
|
1001
|
+
if (operation !== "send") continue;
|
|
1002
|
+
const to = params?.["to"] ?? params?.["toList"];
|
|
1003
|
+
const isEmpty = to === void 0 || to === null || typeof to === "string" && to.trim() === "" || Array.isArray(to) && to.length === 0;
|
|
1004
|
+
if (isEmpty) {
|
|
1005
|
+
this.warn(issues, 30, `Node "${node.name}" gmail send has no recipient (to) specified`, node.id);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1120
1008
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1009
|
+
// Rule 31 (WARN): if node with empty conditions
|
|
1010
|
+
checkRule31(w, issues) {
|
|
1011
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1012
|
+
for (const node of w.nodes) {
|
|
1013
|
+
if (node.type !== "n8n-nodes-base.if") continue;
|
|
1014
|
+
const params = node.parameters;
|
|
1015
|
+
const conditions = params?.["conditions"];
|
|
1016
|
+
if (conditions === void 0 || conditions === null) {
|
|
1017
|
+
this.warn(issues, 31, `Node "${node.name}" if node has no conditions defined`, node.id);
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if (typeof conditions === "object" && !Array.isArray(conditions)) {
|
|
1021
|
+
const conds = conditions["conditions"];
|
|
1022
|
+
if (!Array.isArray(conds) || conds.length === 0) {
|
|
1023
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1024
|
+
}
|
|
1025
|
+
} else if (Array.isArray(conditions) && conditions.length === 0) {
|
|
1026
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1128
1029
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (
|
|
1132
|
-
|
|
1030
|
+
// Rule 32 (WARN): set node with no assignments
|
|
1031
|
+
checkRule32(w, issues) {
|
|
1032
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1033
|
+
for (const node of w.nodes) {
|
|
1034
|
+
if (node.type !== "n8n-nodes-base.set") continue;
|
|
1035
|
+
const params = node.parameters;
|
|
1036
|
+
const assignmentsObj = params?.["assignments"];
|
|
1037
|
+
const assignmentsArr = assignmentsObj?.["assignments"];
|
|
1038
|
+
const valuesObj = params?.["values"];
|
|
1039
|
+
const hasV1 = valuesObj && Object.values(valuesObj).some((v) => Array.isArray(v) && v.length > 0);
|
|
1040
|
+
const hasV3 = Array.isArray(assignmentsArr) && assignmentsArr.length > 0;
|
|
1041
|
+
if (!hasV1 && !hasV3) {
|
|
1042
|
+
this.warn(
|
|
1043
|
+
issues,
|
|
1044
|
+
32,
|
|
1045
|
+
`Node "${node.name}" set node has no fields defined \u2014 it will pass data through unchanged`,
|
|
1046
|
+
node.id
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1133
1049
|
}
|
|
1134
|
-
return this.buildLegacyWarnings(matches, globalFailureRates);
|
|
1135
1050
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
list.push(p);
|
|
1147
|
-
byStage.set(p.pipelineStage, list);
|
|
1148
|
-
}
|
|
1149
|
-
const sections = [];
|
|
1150
|
-
for (const [stage, stagePatterns] of byStage) {
|
|
1151
|
-
const label = stageLabels[stage] ?? stage;
|
|
1152
|
-
const byMitigation = /* @__PURE__ */ new Map();
|
|
1153
|
-
for (const p of stagePatterns) {
|
|
1154
|
-
const key = p.mitigation ?? `rule_${p.rule}`;
|
|
1155
|
-
const list = byMitigation.get(key) ?? [];
|
|
1156
|
-
list.push(p);
|
|
1157
|
-
byMitigation.set(key, list);
|
|
1158
|
-
}
|
|
1159
|
-
const lines = [];
|
|
1160
|
-
for (const group of byMitigation.values()) {
|
|
1161
|
-
if (group.length === 1) {
|
|
1162
|
-
const p = group[0];
|
|
1163
|
-
const urgency = p.regressed ? "CRITICAL REGRESSION: " : (p.compositeScore ?? 0) >= CRITICAL_SCORE_THRESHOLD ? "CRITICAL: " : "";
|
|
1164
|
-
const statePrefix = p.state === "confirmed" ? "[CONFIRMED] " : "";
|
|
1165
|
-
const trendSuffix = p.trend === "worsening" ? " (GETTING WORSE)" : p.trend === "improving" ? " (improving)" : "";
|
|
1166
|
-
const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
|
|
1167
|
-
const remedyStr = remedy ? `
|
|
1168
|
-
Fix: ${remedy}` : "";
|
|
1169
|
-
lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}`);
|
|
1170
|
-
} else {
|
|
1171
|
-
const ruleNums = group.map((p) => p.rule).join(", ");
|
|
1172
|
-
const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
|
|
1173
|
-
const hasConfirmed = group.some((p) => p.state === "confirmed");
|
|
1174
|
-
const statePrefix = hasConfirmed ? "[CONFIRMED] " : "";
|
|
1175
|
-
const remedy = group[0].mitigation;
|
|
1176
|
-
const remedyStr = remedy ? `
|
|
1177
|
-
Fix: ${remedy}` : "";
|
|
1178
|
-
lines.push(`- ${statePrefix}Rules ${ruleNums} (${totalFailures} failures combined): same root cause${remedyStr}`);
|
|
1179
|
-
}
|
|
1051
|
+
// Rule 33 (WARN): scheduleTrigger with no schedule rules
|
|
1052
|
+
checkRule33(w, issues) {
|
|
1053
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1054
|
+
for (const node of w.nodes) {
|
|
1055
|
+
if (node.type !== "n8n-nodes-base.scheduleTrigger") continue;
|
|
1056
|
+
const params = node.parameters;
|
|
1057
|
+
const rule = params?.["rule"];
|
|
1058
|
+
const intervals = rule?.["interval"];
|
|
1059
|
+
if (!Array.isArray(intervals) || intervals.length === 0) {
|
|
1060
|
+
this.warn(issues, 33, `Node "${node.name}" scheduleTrigger has no schedule rules defined`, node.id);
|
|
1180
1061
|
}
|
|
1181
|
-
sections.push(`### ${label}
|
|
1182
|
-
${lines.join("\n")}`);
|
|
1183
1062
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1063
|
+
}
|
|
1064
|
+
// Rule 34 (WARN): webhook path contains spaces, starts with slash, or looks like a full URL
|
|
1065
|
+
checkRule34(w, issues) {
|
|
1066
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1067
|
+
for (const node of w.nodes) {
|
|
1068
|
+
if (node.type !== "n8n-nodes-base.webhook") continue;
|
|
1069
|
+
const params = node.parameters;
|
|
1070
|
+
const path = params?.["path"];
|
|
1071
|
+
if (typeof path !== "string") continue;
|
|
1072
|
+
if (/\s/.test(path)) {
|
|
1073
|
+
this.warn(
|
|
1074
|
+
issues,
|
|
1075
|
+
34,
|
|
1076
|
+
`Node "${node.name}" webhook path contains spaces: "${path}" \u2014 use hyphens or underscores instead`,
|
|
1077
|
+
node.id
|
|
1078
|
+
);
|
|
1079
|
+
} else if (/^https?:\/\//i.test(path)) {
|
|
1080
|
+
this.warn(
|
|
1081
|
+
issues,
|
|
1082
|
+
34,
|
|
1083
|
+
`Node "${node.name}" webhook path looks like a full URL \u2014 it should be a relative path (e.g. "my-hook")`,
|
|
1084
|
+
node.id
|
|
1085
|
+
);
|
|
1086
|
+
} else if (path.startsWith("/")) {
|
|
1087
|
+
this.warn(
|
|
1088
|
+
issues,
|
|
1089
|
+
34,
|
|
1090
|
+
`Node "${node.name}" webhook path starts with "/" \u2014 n8n adds the leading slash automatically`,
|
|
1091
|
+
node.id
|
|
1092
|
+
);
|
|
1193
1093
|
}
|
|
1194
1094
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1197
1097
|
|
|
1198
|
-
|
|
1098
|
+
// src/telemetry/collector.ts
|
|
1099
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
1100
|
+
import { join } from "path";
|
|
1101
|
+
import { homedir } from "os";
|
|
1199
1102
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
buildLegacyWarnings(matches, globalFailureRates) {
|
|
1203
|
-
const lines = [];
|
|
1204
|
-
for (const match of matches) {
|
|
1205
|
-
const patterns = match.workflow.failurePatterns;
|
|
1206
|
-
if (!patterns?.length) continue;
|
|
1207
|
-
for (const fp of patterns) {
|
|
1208
|
-
const remedy = RULE_MITIGATIONS[fp.rule];
|
|
1209
|
-
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1210
|
-
lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
|
|
1214
|
-
for (const rule of highFreqRules) {
|
|
1215
|
-
const remedy = RULE_MITIGATIONS[rule.rule];
|
|
1216
|
-
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1217
|
-
lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
|
|
1218
|
-
}
|
|
1219
|
-
if (lines.length === 0) return null;
|
|
1220
|
-
const unique = [...new Set(lines)];
|
|
1221
|
-
return `## Known Failure Patterns \u2014 AVOID THESE
|
|
1103
|
+
// src/telemetry/types.ts
|
|
1104
|
+
var TELEMETRY_SCHEMA_VERSION = 2;
|
|
1222
1105
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1106
|
+
// src/telemetry/collector.ts
|
|
1107
|
+
var TelemetryCollector = class {
|
|
1108
|
+
dir;
|
|
1109
|
+
sessionId;
|
|
1110
|
+
dirReady = null;
|
|
1111
|
+
constructor(dir) {
|
|
1112
|
+
this.dir = dir ?? join(homedir(), ".kairos", "telemetry");
|
|
1113
|
+
this.sessionId = generateUUID();
|
|
1114
|
+
}
|
|
1115
|
+
async emit(eventType, data, runId) {
|
|
1116
|
+
const event = {
|
|
1117
|
+
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
1118
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1119
|
+
sessionId: this.sessionId,
|
|
1120
|
+
...runId ? { runId } : {},
|
|
1121
|
+
eventType,
|
|
1122
|
+
data
|
|
1123
|
+
};
|
|
1124
|
+
if (!this.dirReady) {
|
|
1125
|
+
this.dirReady = mkdir(this.dir, { recursive: true }).then(() => {
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
await this.dirReady;
|
|
1129
|
+
const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
|
|
1130
|
+
const filepath = join(this.dir, filename);
|
|
1131
|
+
await appendFile(filepath, JSON.stringify(event) + "\n", "utf-8");
|
|
1230
1132
|
}
|
|
1231
1133
|
};
|
|
1232
1134
|
|
|
@@ -1288,19 +1190,20 @@ var TelemetryReader = class {
|
|
|
1288
1190
|
}
|
|
1289
1191
|
const events = await this.readRecentEvents(days);
|
|
1290
1192
|
const buildSessions = new Set(
|
|
1291
|
-
events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
|
|
1193
|
+
events.filter((e) => e.eventType === "build_complete").map((e) => e.runId ?? e.sessionId)
|
|
1292
1194
|
);
|
|
1293
1195
|
const MIN_BUILDS_FOR_RATES = 3;
|
|
1294
1196
|
if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
|
|
1295
1197
|
const ruleSessions = /* @__PURE__ */ new Map();
|
|
1296
1198
|
for (const event of events) {
|
|
1297
1199
|
if (event.eventType !== "generation_attempt") continue;
|
|
1298
|
-
|
|
1200
|
+
const eventKey = event.runId ?? event.sessionId;
|
|
1201
|
+
if (!buildSessions.has(eventKey)) continue;
|
|
1299
1202
|
const data = event.data;
|
|
1300
1203
|
if (data.validationPassed || !data.issues) continue;
|
|
1301
1204
|
for (const issue of data.issues) {
|
|
1302
1205
|
const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
|
|
1303
|
-
entry.sessions.add(
|
|
1206
|
+
entry.sessions.add(eventKey);
|
|
1304
1207
|
entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
|
|
1305
1208
|
ruleSessions.set(issue.rule, entry);
|
|
1306
1209
|
}
|
|
@@ -1334,29 +1237,159 @@ var TelemetryReader = class {
|
|
|
1334
1237
|
};
|
|
1335
1238
|
|
|
1336
1239
|
// src/telemetry/pattern-analyzer.ts
|
|
1337
|
-
import { writeFile, readFile as fsReadFile, appendFile, mkdir, rename } from "fs/promises";
|
|
1240
|
+
import { writeFile, readFile as fsReadFile, appendFile as appendFile2, mkdir as mkdir2, rename } from "fs/promises";
|
|
1338
1241
|
import { join as join4 } from "path";
|
|
1339
1242
|
import { homedir as homedir3 } from "os";
|
|
1243
|
+
|
|
1244
|
+
// src/validation/rule-metadata.ts
|
|
1245
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 34 }, (_, i) => i + 1);
|
|
1246
|
+
var RULE_PIPELINE_STAGES = {
|
|
1247
|
+
1: "node_generation",
|
|
1248
|
+
2: "node_generation",
|
|
1249
|
+
3: "node_generation",
|
|
1250
|
+
4: "node_generation",
|
|
1251
|
+
5: "node_generation",
|
|
1252
|
+
6: "node_generation",
|
|
1253
|
+
7: "node_generation",
|
|
1254
|
+
8: "node_generation",
|
|
1255
|
+
9: "connection_wiring",
|
|
1256
|
+
10: "connection_wiring",
|
|
1257
|
+
11: "connection_wiring",
|
|
1258
|
+
12: "workflow_structure",
|
|
1259
|
+
13: "node_generation",
|
|
1260
|
+
14: "workflow_structure",
|
|
1261
|
+
15: "node_generation",
|
|
1262
|
+
16: "node_generation",
|
|
1263
|
+
17: "credential_injection",
|
|
1264
|
+
18: "connection_wiring",
|
|
1265
|
+
19: "node_generation",
|
|
1266
|
+
20: "connection_wiring",
|
|
1267
|
+
21: "workflow_structure",
|
|
1268
|
+
22: "workflow_structure",
|
|
1269
|
+
23: "node_generation",
|
|
1270
|
+
24: "expression_syntax",
|
|
1271
|
+
25: "expression_syntax",
|
|
1272
|
+
26: "expression_syntax",
|
|
1273
|
+
27: "node_generation",
|
|
1274
|
+
28: "node_generation",
|
|
1275
|
+
29: "node_generation",
|
|
1276
|
+
30: "node_generation",
|
|
1277
|
+
31: "node_generation",
|
|
1278
|
+
32: "node_generation",
|
|
1279
|
+
33: "node_generation",
|
|
1280
|
+
34: "node_generation"
|
|
1281
|
+
};
|
|
1282
|
+
var RULE_EXAMPLES = {
|
|
1283
|
+
17: {
|
|
1284
|
+
bad: '"credentials": { "slackOAuth2Api": "my-token" }',
|
|
1285
|
+
good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
|
|
1286
|
+
},
|
|
1287
|
+
24: {
|
|
1288
|
+
bad: '$node["Fetch Data"].json.email',
|
|
1289
|
+
good: "$('Fetch Data').item.json.email"
|
|
1290
|
+
},
|
|
1291
|
+
25: {
|
|
1292
|
+
bad: "$json.items[0].email",
|
|
1293
|
+
good: "$json.email"
|
|
1294
|
+
},
|
|
1295
|
+
26: {
|
|
1296
|
+
bad: "$('Fetch Data').json.email",
|
|
1297
|
+
good: "$('Fetch Data').first().json.email"
|
|
1298
|
+
},
|
|
1299
|
+
27: {
|
|
1300
|
+
bad: '"url": "https://example.com/api/data"',
|
|
1301
|
+
good: '"url": "https://api.yourservice.com/v1/endpoint"'
|
|
1302
|
+
},
|
|
1303
|
+
28: {
|
|
1304
|
+
bad: '"jsCode": "// TODO: implement this"',
|
|
1305
|
+
good: '"jsCode": "return items.map(item => ({ json: { result: item.json.value * 2 } }))"'
|
|
1306
|
+
},
|
|
1307
|
+
29: {
|
|
1308
|
+
bad: '"channelId": ""',
|
|
1309
|
+
good: '"channelId": { "__rl": true, "value": "C0123456789", "mode": "id" }'
|
|
1310
|
+
},
|
|
1311
|
+
30: {
|
|
1312
|
+
bad: '"operation": "send", "to": ""',
|
|
1313
|
+
good: '"operation": "send", "to": "recipient@example.com"'
|
|
1314
|
+
},
|
|
1315
|
+
31: {
|
|
1316
|
+
bad: '"conditions": { "combinator": "and", "conditions": [] }',
|
|
1317
|
+
good: '"conditions": { "combinator": "and", "conditions": [{ "leftValue": "={{ $json.status }}", "rightValue": "active", "operator": { "type": "string", "operation": "equals" } }] }'
|
|
1318
|
+
},
|
|
1319
|
+
32: {
|
|
1320
|
+
bad: '"assignments": { "assignments": [] }',
|
|
1321
|
+
good: '"assignments": { "assignments": [{ "id": "f1", "name": "status", "value": "processed", "type": "string" }] }'
|
|
1322
|
+
},
|
|
1323
|
+
33: {
|
|
1324
|
+
bad: '"rule": { "interval": [] }',
|
|
1325
|
+
good: '"rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] }'
|
|
1326
|
+
},
|
|
1327
|
+
34: {
|
|
1328
|
+
bad: '"path": "/my webhook"',
|
|
1329
|
+
good: '"path": "my-webhook"'
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
var RULE_MITIGATIONS = {
|
|
1333
|
+
1: "Provide a non-empty workflow name string",
|
|
1334
|
+
2: "Include at least one node in the nodes array",
|
|
1335
|
+
3: "Every node must have a unique UUID v4 string as its id field",
|
|
1336
|
+
4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
|
|
1337
|
+
5: "Every node must have a non-empty type string",
|
|
1338
|
+
6: "Every node must have a positive integer typeVersion",
|
|
1339
|
+
7: "Every node must have a position array of exactly [x, y] numbers",
|
|
1340
|
+
8: "Every node must have a non-empty name string",
|
|
1341
|
+
9: "connections must be a plain object (use {} if no connections)",
|
|
1342
|
+
10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
|
|
1343
|
+
11: "Every non-trigger node should have at least one incoming connection",
|
|
1344
|
+
12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
|
|
1345
|
+
13: "workflow.settings must be a plain object if present",
|
|
1346
|
+
14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
|
|
1347
|
+
15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
|
|
1348
|
+
16: "All node names must be unique within the workflow",
|
|
1349
|
+
17: 'Each credential entry must be keyed by credential type with an object value: { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Credential" } } \u2014 the key is the credential type, the value has id and name strings',
|
|
1350
|
+
18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
|
|
1351
|
+
19: "Use known safe typeVersion values for each node type",
|
|
1352
|
+
20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
|
|
1353
|
+
21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
|
|
1354
|
+
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
|
|
1355
|
+
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
1356
|
+
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
1357
|
+
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
1358
|
+
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime',
|
|
1359
|
+
27: 'Replace placeholder URLs with your actual API endpoint \u2014 do not use "example.com" or "YOUR_URL" patterns',
|
|
1360
|
+
28: "Add executable code to the code node \u2014 empty or comment-only code nodes do nothing at runtime",
|
|
1361
|
+
29: "Set the channel parameter for Slack message operations (channelId with __rl object, or channel as string)",
|
|
1362
|
+
30: "Set the to parameter for Gmail send operations with at least one recipient email address",
|
|
1363
|
+
31: "Add at least one condition to the if node \u2014 conditions.conditions array must be non-empty",
|
|
1364
|
+
32: "Add field assignments to the set node \u2014 assignments.assignments array must be non-empty for typeVersion 3.x",
|
|
1365
|
+
33: "Add at least one schedule rule to scheduleTrigger \u2014 rule.interval array must have at least one entry",
|
|
1366
|
+
34: 'Webhook path must be a relative path without spaces, leading slashes, or protocol prefixes (e.g. "my-hook")'
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
// src/telemetry/pattern-analyzer.ts
|
|
1340
1370
|
var PATTERN_SCHEMA_VERSION = 2;
|
|
1341
1371
|
var PatternAnalyzer = class _PatternAnalyzer {
|
|
1342
1372
|
telemetryDir;
|
|
1343
1373
|
outputDir;
|
|
1374
|
+
_cachedEvents = null;
|
|
1375
|
+
_cachedPreviousPatterns = null;
|
|
1344
1376
|
constructor(telemetryDir) {
|
|
1345
1377
|
const defaultDir = join4(homedir3(), ".kairos", "telemetry");
|
|
1346
1378
|
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
1347
1379
|
this.outputDir = telemetryDir ? join4(telemetryDir, "..") : join4(homedir3(), ".kairos");
|
|
1348
1380
|
}
|
|
1349
1381
|
async loadPreviousPatterns() {
|
|
1382
|
+
if (this._cachedPreviousPatterns !== null) return this._cachedPreviousPatterns;
|
|
1350
1383
|
try {
|
|
1351
1384
|
const raw = await fsReadFile(join4(this.outputDir, "patterns.json"), "utf-8");
|
|
1352
1385
|
const prev = JSON.parse(raw);
|
|
1353
1386
|
const version = prev.schemaVersion ?? 0;
|
|
1354
1387
|
const patterns = prev.topFailureRules ?? [];
|
|
1355
|
-
|
|
1356
|
-
return this.migratePatterns(patterns, version);
|
|
1388
|
+
this._cachedPreviousPatterns = version === PATTERN_SCHEMA_VERSION ? patterns : this.migratePatterns(patterns, version);
|
|
1357
1389
|
} catch {
|
|
1358
|
-
|
|
1390
|
+
this._cachedPreviousPatterns = [];
|
|
1359
1391
|
}
|
|
1392
|
+
return this._cachedPreviousPatterns;
|
|
1360
1393
|
}
|
|
1361
1394
|
migratePatterns(patterns, fromVersion) {
|
|
1362
1395
|
let migrated = patterns;
|
|
@@ -1369,19 +1402,23 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1369
1402
|
}));
|
|
1370
1403
|
}
|
|
1371
1404
|
if (fromVersion < 2) {
|
|
1372
|
-
migrated = migrated.map((p) =>
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
...p
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1405
|
+
migrated = migrated.map((p) => {
|
|
1406
|
+
const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
|
|
1407
|
+
return {
|
|
1408
|
+
...p,
|
|
1409
|
+
scoringFactors: {
|
|
1410
|
+
...sf,
|
|
1411
|
+
stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
});
|
|
1379
1415
|
}
|
|
1380
1416
|
return migrated;
|
|
1381
1417
|
}
|
|
1382
1418
|
async analyze(days = 30) {
|
|
1383
1419
|
const previousPatterns = await this.loadPreviousPatterns();
|
|
1384
1420
|
const events = await this.readAllEvents(days);
|
|
1421
|
+
this._cachedEvents = events;
|
|
1385
1422
|
const starts = events.filter((e) => e.eventType === "build_start");
|
|
1386
1423
|
const attempts = events.filter((e) => e.eventType === "generation_attempt");
|
|
1387
1424
|
const passed = attempts.filter(
|
|
@@ -1394,13 +1431,18 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1394
1431
|
const credentialFailures = /* @__PURE__ */ new Map();
|
|
1395
1432
|
for (const a of failed) {
|
|
1396
1433
|
const weight = this.recencyWeight(a.fileDate);
|
|
1434
|
+
const buildId = a.runId ?? a.sessionId;
|
|
1397
1435
|
const data = a.data;
|
|
1398
1436
|
for (const issue of data.issues ?? []) {
|
|
1399
|
-
|
|
1437
|
+
if (issue.severity === "warn") continue;
|
|
1438
|
+
const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
|
|
1400
1439
|
entry.count++;
|
|
1401
|
-
entry.sessions.add(
|
|
1440
|
+
entry.sessions.add(buildId);
|
|
1402
1441
|
entry.recencyWeights.push(weight);
|
|
1403
1442
|
entry.allMessages.push(issue.message);
|
|
1443
|
+
if (data.workflowType) {
|
|
1444
|
+
entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
|
|
1445
|
+
}
|
|
1404
1446
|
ruleFailures.set(issue.rule, entry);
|
|
1405
1447
|
if (issue.rule === 17) {
|
|
1406
1448
|
const credPatterns = [
|
|
@@ -1453,9 +1495,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1453
1495
|
}
|
|
1454
1496
|
const sessions = /* @__PURE__ */ new Map();
|
|
1455
1497
|
for (const a of attempts) {
|
|
1456
|
-
const
|
|
1498
|
+
const buildId = a.runId ?? a.sessionId;
|
|
1499
|
+
const list = sessions.get(buildId) ?? [];
|
|
1457
1500
|
list.push(a);
|
|
1458
|
-
sessions.set(
|
|
1501
|
+
sessions.set(buildId, list);
|
|
1459
1502
|
}
|
|
1460
1503
|
let firstTryPass = 0;
|
|
1461
1504
|
let correctionNeeded = 0;
|
|
@@ -1502,7 +1545,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1502
1545
|
const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
|
|
1503
1546
|
const stickiness = stickinessCount.get(rule) ?? 0;
|
|
1504
1547
|
const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
|
|
1505
|
-
|
|
1548
|
+
const pattern = {
|
|
1506
1549
|
rule,
|
|
1507
1550
|
failureCount: entry.count,
|
|
1508
1551
|
confidence: Math.round(rawConfidence * 1e3) / 1e3,
|
|
@@ -1514,6 +1557,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1514
1557
|
exampleMessages: this.deduplicateMessages(entry.allMessages),
|
|
1515
1558
|
mitigation: RULE_MITIGATIONS[rule] ?? null
|
|
1516
1559
|
};
|
|
1560
|
+
if (entry.workflowTypes.size > 0) {
|
|
1561
|
+
pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
|
|
1562
|
+
}
|
|
1563
|
+
return pattern;
|
|
1517
1564
|
}).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1518
1565
|
const activeRules = new Set(activePatterns.map((p) => p.rule));
|
|
1519
1566
|
for (const p of activePatterns) {
|
|
@@ -1570,7 +1617,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1570
1617
|
const warned = bcData.warnedRules ?? [];
|
|
1571
1618
|
if (warned.length === 0) continue;
|
|
1572
1619
|
const sessionFailedRules = /* @__PURE__ */ new Set();
|
|
1573
|
-
const sessionAttempts = sessions.get(bc.sessionId) ?? [];
|
|
1620
|
+
const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
|
|
1574
1621
|
for (const a of sessionAttempts) {
|
|
1575
1622
|
const ad = a.data;
|
|
1576
1623
|
if (ad.validationPassed === false) {
|
|
@@ -1637,11 +1684,12 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1637
1684
|
}
|
|
1638
1685
|
async analyzeAndSave(days = 30) {
|
|
1639
1686
|
const analysis = await this.analyze(days);
|
|
1640
|
-
await
|
|
1687
|
+
await mkdir2(this.outputDir, { recursive: true });
|
|
1641
1688
|
const outputPath = join4(this.outputDir, "patterns.json");
|
|
1642
1689
|
const tmpPath = `${outputPath}.tmp`;
|
|
1643
1690
|
await writeFile(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
1644
1691
|
await rename(tmpPath, outputPath);
|
|
1692
|
+
this._cachedPreviousPatterns = null;
|
|
1645
1693
|
const historySummary = {
|
|
1646
1694
|
timestamp: analysis.generatedAt,
|
|
1647
1695
|
totalBuilds: analysis.summary.totalBuilds,
|
|
@@ -1652,9 +1700,56 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1652
1700
|
topRules: analysis.topFailureRules.filter((p) => p.state !== "resolved").slice(0, 5).map((p) => ({ rule: p.rule, compositeScore: p.compositeScore, state: p.state }))
|
|
1653
1701
|
};
|
|
1654
1702
|
const historyPath = join4(this.outputDir, "pattern-history.jsonl");
|
|
1655
|
-
await
|
|
1703
|
+
await appendFile2(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
|
|
1704
|
+
const sessions = await this.buildSessionSummaries(days);
|
|
1705
|
+
const sessionHistoryPath = join4(this.outputDir, "session-history.json");
|
|
1706
|
+
const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
|
|
1707
|
+
await writeFile(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
|
|
1708
|
+
await rename(sessionHistoryTmp, sessionHistoryPath);
|
|
1656
1709
|
return analysis;
|
|
1657
1710
|
}
|
|
1711
|
+
async getSessions(limit = 20) {
|
|
1712
|
+
try {
|
|
1713
|
+
const raw = await fsReadFile(join4(this.outputDir, "session-history.json"), "utf-8");
|
|
1714
|
+
const all = JSON.parse(raw);
|
|
1715
|
+
return all.slice(-limit);
|
|
1716
|
+
} catch {
|
|
1717
|
+
return [];
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
async buildSessionSummaries(days = 30) {
|
|
1721
|
+
const events = this._cachedEvents ?? await this.readAllEvents(days);
|
|
1722
|
+
const buildCompletes = events.filter((e) => e.eventType === "build_complete");
|
|
1723
|
+
const attemptsByBuild = /* @__PURE__ */ new Map();
|
|
1724
|
+
for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
|
|
1725
|
+
const buildId = e.runId ?? e.sessionId;
|
|
1726
|
+
const list = attemptsByBuild.get(buildId) ?? [];
|
|
1727
|
+
list.push(e);
|
|
1728
|
+
attemptsByBuild.set(buildId, list);
|
|
1729
|
+
}
|
|
1730
|
+
const summaries = buildCompletes.map((bc) => {
|
|
1731
|
+
const data = bc.data;
|
|
1732
|
+
const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
|
|
1733
|
+
const failedRules = Array.from(new Set(
|
|
1734
|
+
sessionAttempts.flatMap((a) => {
|
|
1735
|
+
const ad = a.data;
|
|
1736
|
+
if (ad.validationPassed !== false) return [];
|
|
1737
|
+
return (ad.issues ?? []).map((i) => i.rule);
|
|
1738
|
+
})
|
|
1739
|
+
));
|
|
1740
|
+
return {
|
|
1741
|
+
sessionId: bc.runId ?? bc.sessionId,
|
|
1742
|
+
date: bc.fileDate,
|
|
1743
|
+
description: data.description ?? "",
|
|
1744
|
+
workflowType: data.workflowType ?? null,
|
|
1745
|
+
attempts: data.totalAttempts ?? 1,
|
|
1746
|
+
success: data.success ?? false,
|
|
1747
|
+
failedRules,
|
|
1748
|
+
workflowName: data.workflowName ?? null
|
|
1749
|
+
};
|
|
1750
|
+
});
|
|
1751
|
+
return summaries.sort((a, b) => a.date.localeCompare(b.date));
|
|
1752
|
+
}
|
|
1658
1753
|
async getHistory(limit = 20) {
|
|
1659
1754
|
try {
|
|
1660
1755
|
const raw = await fsReadFile(join4(this.outputDir, "pattern-history.jsonl"), "utf-8");
|
|
@@ -1676,7 +1771,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1676
1771
|
alerts.push({
|
|
1677
1772
|
type: "stale_pattern",
|
|
1678
1773
|
rule: p.rule,
|
|
1679
|
-
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-
|
|
1774
|
+
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-34)`
|
|
1680
1775
|
});
|
|
1681
1776
|
}
|
|
1682
1777
|
}
|
|
@@ -1764,12 +1859,32 @@ var nullLogger = {
|
|
|
1764
1859
|
};
|
|
1765
1860
|
|
|
1766
1861
|
// src/library/scorer.ts
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1862
|
+
function loadWeights() {
|
|
1863
|
+
const raw = {
|
|
1864
|
+
tfidf: parseFloat(process.env["KAIROS_WEIGHT_TFIDF"] ?? ""),
|
|
1865
|
+
nodeFingerprint: parseFloat(process.env["KAIROS_WEIGHT_JACCARD"] ?? ""),
|
|
1866
|
+
outcome: parseFloat(process.env["KAIROS_WEIGHT_OUTCOME"] ?? ""),
|
|
1867
|
+
deploy: parseFloat(process.env["KAIROS_WEIGHT_DEPLOY"] ?? "")
|
|
1868
|
+
};
|
|
1869
|
+
const defaults = { tfidf: 0.35, nodeFingerprint: 0.3, outcome: 0.2, deploy: 0.15 };
|
|
1870
|
+
const anySet = Object.values(raw).some((v) => !isNaN(v) && v >= 0);
|
|
1871
|
+
if (!anySet) return defaults;
|
|
1872
|
+
const w = {
|
|
1873
|
+
tfidf: !isNaN(raw.tfidf) && raw.tfidf >= 0 ? raw.tfidf : defaults.tfidf,
|
|
1874
|
+
nodeFingerprint: !isNaN(raw.nodeFingerprint) && raw.nodeFingerprint >= 0 ? raw.nodeFingerprint : defaults.nodeFingerprint,
|
|
1875
|
+
outcome: !isNaN(raw.outcome) && raw.outcome >= 0 ? raw.outcome : defaults.outcome,
|
|
1876
|
+
deploy: !isNaN(raw.deploy) && raw.deploy >= 0 ? raw.deploy : defaults.deploy
|
|
1877
|
+
};
|
|
1878
|
+
const total = w.tfidf + w.nodeFingerprint + w.outcome + w.deploy;
|
|
1879
|
+
if (total <= 0) return defaults;
|
|
1880
|
+
return {
|
|
1881
|
+
tfidf: w.tfidf / total,
|
|
1882
|
+
nodeFingerprint: w.nodeFingerprint / total,
|
|
1883
|
+
outcome: w.outcome / total,
|
|
1884
|
+
deploy: w.deploy / total
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
var WEIGHTS = loadWeights();
|
|
1773
1888
|
var NODE_KEYWORDS = {
|
|
1774
1889
|
slack: ["slack", "slackApi"],
|
|
1775
1890
|
email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
|
|
@@ -1954,6 +2069,8 @@ function clusterWorkflows(workflows) {
|
|
|
1954
2069
|
}
|
|
1955
2070
|
return clusters.sort((a, b) => b.members.length - a.members.length);
|
|
1956
2071
|
}
|
|
2072
|
+
var NOVELTY_BOOST = 0.05;
|
|
2073
|
+
var NOVELTY_PENALTY = 0.03;
|
|
1957
2074
|
function rerank(candidates, clusters) {
|
|
1958
2075
|
const clusterMap = /* @__PURE__ */ new Map();
|
|
1959
2076
|
for (const cluster of clusters) {
|
|
@@ -1961,7 +2078,7 @@ function rerank(candidates, clusters) {
|
|
|
1961
2078
|
clusterMap.set(member.id, cluster);
|
|
1962
2079
|
}
|
|
1963
2080
|
}
|
|
1964
|
-
|
|
2081
|
+
const pass1 = candidates.map((c) => {
|
|
1965
2082
|
const cluster = clusterMap.get(c.workflow.id);
|
|
1966
2083
|
let boost = 0;
|
|
1967
2084
|
if (cluster && cluster.avgFirstTryPassRate > 0) {
|
|
@@ -1973,19 +2090,41 @@ function rerank(candidates, clusters) {
|
|
|
1973
2090
|
return {
|
|
1974
2091
|
workflow: c.workflow,
|
|
1975
2092
|
score: Math.max(0, Math.min(1, c.score + boost)),
|
|
1976
|
-
|
|
2093
|
+
cluster
|
|
2094
|
+
};
|
|
2095
|
+
}).sort((a, b) => b.score - a.score);
|
|
2096
|
+
const seenFingerprints = /* @__PURE__ */ new Set();
|
|
2097
|
+
return pass1.map((c) => {
|
|
2098
|
+
const fpKey = c.cluster ? fingerprintKey(c.cluster.fingerprint) : null;
|
|
2099
|
+
let noveltyAdjust = 0;
|
|
2100
|
+
if (fpKey !== null) {
|
|
2101
|
+
if (!seenFingerprints.has(fpKey)) {
|
|
2102
|
+
seenFingerprints.add(fpKey);
|
|
2103
|
+
noveltyAdjust = NOVELTY_BOOST;
|
|
2104
|
+
} else {
|
|
2105
|
+
noveltyAdjust = -NOVELTY_PENALTY;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return {
|
|
2109
|
+
workflow: c.workflow,
|
|
2110
|
+
score: Math.max(0, Math.min(1, c.score + noveltyAdjust)),
|
|
2111
|
+
...c.cluster ? { clusterPattern: c.cluster.pattern } : {}
|
|
1977
2112
|
};
|
|
1978
2113
|
}).sort((a, b) => b.score - a.score);
|
|
1979
2114
|
}
|
|
1980
2115
|
|
|
1981
2116
|
// src/library/file-library.ts
|
|
1982
|
-
import { readFile, writeFile as writeFile2, rename as rename2, mkdir as
|
|
2117
|
+
import { readFile, writeFile as writeFile2, rename as rename2, mkdir as mkdir3, stat, readdir as readdir2, unlink, open } from "fs/promises";
|
|
1983
2118
|
import { join as join5 } from "path";
|
|
1984
2119
|
import { homedir as homedir4 } from "os";
|
|
1985
2120
|
|
|
1986
|
-
// src/utils/
|
|
1987
|
-
|
|
1988
|
-
|
|
2121
|
+
// src/utils/thresholds.ts
|
|
2122
|
+
var DIRECT_THRESHOLD = 0.92;
|
|
2123
|
+
var REFERENCE_THRESHOLD = 0.72;
|
|
2124
|
+
function scoreToMode(score) {
|
|
2125
|
+
if (score >= DIRECT_THRESHOLD) return "direct";
|
|
2126
|
+
if (score >= REFERENCE_THRESHOLD) return "reference";
|
|
2127
|
+
return "scratch";
|
|
1989
2128
|
}
|
|
1990
2129
|
|
|
1991
2130
|
// src/library/file-library.ts
|
|
@@ -2000,15 +2139,33 @@ function buildSearchCorpus(w) {
|
|
|
2000
2139
|
});
|
|
2001
2140
|
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
2002
2141
|
}
|
|
2003
|
-
var
|
|
2142
|
+
var _rawSize = parseInt(process.env["KAIROS_LIBRARY_SIZE"] ?? "500", 10);
|
|
2143
|
+
var MAX_LIBRARY_SIZE = Number.isFinite(_rawSize) && _rawSize >= 10 ? _rawSize : 500;
|
|
2144
|
+
function evictionScore(m) {
|
|
2145
|
+
return (m.deployCount ?? 0) * 3 + (m.timesRetrieved ?? 0) + (m.outcomeStats?.totalUses ?? 0);
|
|
2146
|
+
}
|
|
2147
|
+
function isValidMeta(item) {
|
|
2148
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
2149
|
+
}
|
|
2150
|
+
function isValidOldEntry(item) {
|
|
2151
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
|
|
2152
|
+
item.workflow.nodes
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2004
2155
|
var FileLibrary = class {
|
|
2005
2156
|
dir;
|
|
2006
|
-
|
|
2157
|
+
meta = [];
|
|
2007
2158
|
initPromise = null;
|
|
2008
2159
|
writeQueue = Promise.resolve();
|
|
2009
2160
|
constructor(dir) {
|
|
2010
2161
|
this.dir = dir ?? join5(homedir4(), ".kairos", "library");
|
|
2011
2162
|
}
|
|
2163
|
+
get workflowsDir() {
|
|
2164
|
+
return join5(this.dir, "workflows");
|
|
2165
|
+
}
|
|
2166
|
+
workflowFilePath(id) {
|
|
2167
|
+
return join5(this.workflowsDir, `${id}.json`);
|
|
2168
|
+
}
|
|
2012
2169
|
async initialize() {
|
|
2013
2170
|
if (!this.initPromise) {
|
|
2014
2171
|
this.initPromise = this.doInitialize();
|
|
@@ -2016,62 +2173,198 @@ var FileLibrary = class {
|
|
|
2016
2173
|
return this.initPromise;
|
|
2017
2174
|
}
|
|
2018
2175
|
async doInitialize() {
|
|
2019
|
-
await
|
|
2176
|
+
await mkdir3(this.dir, { recursive: true });
|
|
2020
2177
|
const indexPath = join5(this.dir, "index.json");
|
|
2178
|
+
let workflowsDirExists = false;
|
|
2021
2179
|
try {
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
);
|
|
2180
|
+
await stat(this.workflowsDir);
|
|
2181
|
+
workflowsDirExists = true;
|
|
2182
|
+
} catch {
|
|
2183
|
+
}
|
|
2184
|
+
if (workflowsDirExists) {
|
|
2185
|
+
try {
|
|
2186
|
+
const raw = await readFile(indexPath, "utf-8");
|
|
2187
|
+
const parsed = JSON.parse(raw);
|
|
2188
|
+
if (Array.isArray(parsed)) {
|
|
2189
|
+
this.meta = parsed.filter(isValidMeta);
|
|
2190
|
+
}
|
|
2191
|
+
} catch {
|
|
2192
|
+
this.meta = [];
|
|
2193
|
+
}
|
|
2194
|
+
await this.scanForOrphansAndCleanup();
|
|
2195
|
+
} else {
|
|
2196
|
+
try {
|
|
2197
|
+
const raw = await readFile(indexPath, "utf-8");
|
|
2198
|
+
const parsed = JSON.parse(raw);
|
|
2199
|
+
if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
|
|
2200
|
+
await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
} catch {
|
|
2204
|
+
}
|
|
2205
|
+
this.meta = [];
|
|
2206
|
+
await mkdir3(this.workflowsDir, { recursive: true });
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
async scanForOrphansAndCleanup() {
|
|
2210
|
+
let entries;
|
|
2211
|
+
try {
|
|
2212
|
+
entries = await readdir2(this.workflowsDir);
|
|
2213
|
+
} catch {
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
const indexedIds = new Set(this.meta.map((m) => m.id));
|
|
2217
|
+
const orphanIds = [];
|
|
2218
|
+
for (const filename of entries) {
|
|
2219
|
+
if (filename.endsWith(".tmp")) {
|
|
2220
|
+
await unlink(join5(this.workflowsDir, filename)).catch(() => {
|
|
2221
|
+
});
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
if (!filename.endsWith(".json")) continue;
|
|
2225
|
+
const id = filename.slice(0, -5);
|
|
2226
|
+
if (!indexedIds.has(id)) {
|
|
2227
|
+
orphanIds.push(id);
|
|
2030
2228
|
}
|
|
2229
|
+
}
|
|
2230
|
+
if (orphanIds.length > 0) {
|
|
2231
|
+
console.warn(`[FileLibrary] Found ${orphanIds.length} orphaned workflow file(s) not in index: ${orphanIds.join(", ")}`);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
/**
|
|
2235
|
+
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
2236
|
+
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
2237
|
+
* meta entry. Rewrites index.json in the new format.
|
|
2238
|
+
*/
|
|
2239
|
+
async migrateFromMonolithic(oldEntries) {
|
|
2240
|
+
await mkdir3(this.workflowsDir, { recursive: true });
|
|
2241
|
+
const newMeta = [];
|
|
2242
|
+
for (const entry of oldEntries) {
|
|
2243
|
+
const wfPath = this.workflowFilePath(entry.id);
|
|
2244
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
2245
|
+
await writeFile2(tmpPath, JSON.stringify(entry.workflow), "utf-8");
|
|
2246
|
+
await rename2(tmpPath, wfPath);
|
|
2247
|
+
const { workflow, ...metaFields } = entry;
|
|
2248
|
+
newMeta.push({
|
|
2249
|
+
...metaFields,
|
|
2250
|
+
workflowName: workflow.name,
|
|
2251
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type)
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
this.meta = newMeta;
|
|
2255
|
+
await this.persistNow();
|
|
2256
|
+
}
|
|
2257
|
+
async loadWorkflowFile(id) {
|
|
2258
|
+
try {
|
|
2259
|
+
const raw = await readFile(this.workflowFilePath(id), "utf-8");
|
|
2260
|
+
return JSON.parse(raw);
|
|
2031
2261
|
} catch {
|
|
2032
|
-
|
|
2262
|
+
return null;
|
|
2033
2263
|
}
|
|
2034
2264
|
}
|
|
2265
|
+
async writeWorkflowFile(id, workflow) {
|
|
2266
|
+
const wfPath = this.workflowFilePath(id);
|
|
2267
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
2268
|
+
await writeFile2(tmpPath, JSON.stringify(workflow), "utf-8");
|
|
2269
|
+
await rename2(tmpPath, wfPath);
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Build a lightweight StoredWorkflow shell from a meta entry for use in
|
|
2273
|
+
* scoring / clustering. Only node.type is populated in each node — no other
|
|
2274
|
+
* node fields are used by hybridScore or clusterWorkflows.
|
|
2275
|
+
*/
|
|
2276
|
+
makeSearchShell(m) {
|
|
2277
|
+
return {
|
|
2278
|
+
...m,
|
|
2279
|
+
workflow: {
|
|
2280
|
+
name: m.workflowName,
|
|
2281
|
+
nodes: m.cachedNodeTypes.map((type) => ({
|
|
2282
|
+
id: "",
|
|
2283
|
+
name: "",
|
|
2284
|
+
type,
|
|
2285
|
+
typeVersion: 1,
|
|
2286
|
+
position: [0, 0],
|
|
2287
|
+
parameters: {}
|
|
2288
|
+
})),
|
|
2289
|
+
connections: {}
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2035
2293
|
async search(description, options) {
|
|
2036
|
-
const
|
|
2037
|
-
if (
|
|
2294
|
+
const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
|
|
2295
|
+
if (filteredMeta.length === 0) return [];
|
|
2038
2296
|
const limit = options?.limit ?? 3;
|
|
2039
2297
|
const queryTokens = tokenize(description);
|
|
2040
2298
|
if (queryTokens.length === 0) return [];
|
|
2041
|
-
const
|
|
2299
|
+
const shells = filteredMeta.map((m) => this.makeSearchShell(m));
|
|
2300
|
+
const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
|
|
2042
2301
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
2043
|
-
const docCount =
|
|
2302
|
+
const docCount = shells.length;
|
|
2044
2303
|
const idf = /* @__PURE__ */ new Map();
|
|
2304
|
+
const idfCeiling = Math.log(docCount + 1) + 1;
|
|
2045
2305
|
const allTokens = new Set(queryTokens);
|
|
2046
2306
|
for (const token of allTokens) {
|
|
2047
2307
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
2048
|
-
|
|
2308
|
+
const rawIdf = Math.log((docCount + 1) / (docsWithToken + 1)) + 1;
|
|
2309
|
+
idf.set(token, rawIdf / idfCeiling);
|
|
2049
2310
|
}
|
|
2050
|
-
const scored = hybridScore(queryTokens, description,
|
|
2051
|
-
const clusters = clusterWorkflows(
|
|
2311
|
+
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
2312
|
+
const clusters = clusterWorkflows(shells);
|
|
2052
2313
|
const reranked = rerank(scored, clusters).slice(0, limit);
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2314
|
+
if (reranked.length === 0) return [];
|
|
2315
|
+
for (const r of reranked) {
|
|
2316
|
+
const m = this.meta.find((m2) => m2.id === r.workflow.id);
|
|
2317
|
+
if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
|
|
2318
|
+
}
|
|
2319
|
+
this.persist();
|
|
2320
|
+
const results = await Promise.all(
|
|
2321
|
+
reranked.map(async (r) => {
|
|
2322
|
+
const m = this.meta.find((meta) => meta.id === r.workflow.id);
|
|
2323
|
+
const workflow = await this.loadWorkflowFile(r.workflow.id);
|
|
2324
|
+
if (!workflow) return null;
|
|
2325
|
+
return {
|
|
2326
|
+
workflow: { ...m, workflow },
|
|
2327
|
+
score: r.score,
|
|
2328
|
+
mode: scoreToMode(r.score)
|
|
2329
|
+
};
|
|
2330
|
+
})
|
|
2331
|
+
);
|
|
2332
|
+
return results.filter((r) => r !== null);
|
|
2063
2333
|
}
|
|
2064
2334
|
async save(workflow, metadata) {
|
|
2335
|
+
const existingByN8nId = metadata.n8nWorkflowId ? this.meta.find((m) => m.n8nWorkflowId === metadata.n8nWorkflowId) : void 0;
|
|
2336
|
+
const normalizedDesc = metadata.description.trim().toLowerCase();
|
|
2337
|
+
const existing = existingByN8nId ?? this.meta.find((m) => m.description.trim().toLowerCase() === normalizedDesc);
|
|
2338
|
+
if (existing) {
|
|
2339
|
+
existing.description = metadata.description;
|
|
2340
|
+
existing.workflowName = workflow.name;
|
|
2341
|
+
existing.cachedNodeTypes = workflow.nodes.map((n) => n.type);
|
|
2342
|
+
if (metadata.n8nWorkflowId) existing.n8nWorkflowId = metadata.n8nWorkflowId;
|
|
2343
|
+
if (metadata.generationAttempts != null) {
|
|
2344
|
+
existing.generationAttempts = metadata.generationAttempts;
|
|
2345
|
+
}
|
|
2346
|
+
if (metadata.failurePatterns?.length) {
|
|
2347
|
+
existing.failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
2348
|
+
}
|
|
2349
|
+
if (metadata.tags?.length) {
|
|
2350
|
+
existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...metadata.tags])];
|
|
2351
|
+
}
|
|
2352
|
+
await this.writeWorkflowFile(existing.id, workflow);
|
|
2353
|
+
await this.persist();
|
|
2354
|
+
return existing.id;
|
|
2355
|
+
}
|
|
2065
2356
|
const id = generateUUID();
|
|
2357
|
+
await this.writeWorkflowFile(id, workflow);
|
|
2066
2358
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
2067
|
-
const
|
|
2359
|
+
const meta = {
|
|
2068
2360
|
id,
|
|
2069
|
-
workflow,
|
|
2070
2361
|
description: metadata.description,
|
|
2071
2362
|
tags: metadata.tags ?? [],
|
|
2072
2363
|
platform: metadata.platform ?? "n8n",
|
|
2073
2364
|
deployCount: 0,
|
|
2074
2365
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2366
|
+
workflowName: workflow.name,
|
|
2367
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type),
|
|
2075
2368
|
...failurePatterns?.length ? { failurePatterns } : {},
|
|
2076
2369
|
...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
|
|
2077
2370
|
...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
|
|
@@ -2081,33 +2374,39 @@ var FileLibrary = class {
|
|
|
2081
2374
|
...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
|
|
2082
2375
|
...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
|
|
2083
2376
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
2084
|
-
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
2377
|
+
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {},
|
|
2378
|
+
...metadata.n8nWorkflowId ? { n8nWorkflowId: metadata.n8nWorkflowId } : {}
|
|
2085
2379
|
};
|
|
2086
|
-
this.
|
|
2087
|
-
if (this.
|
|
2088
|
-
this.
|
|
2089
|
-
|
|
2380
|
+
this.meta.push(meta);
|
|
2381
|
+
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
2382
|
+
this.meta.sort((a, b) => {
|
|
2383
|
+
if (a.id === id) return -1;
|
|
2384
|
+
if (b.id === id) return 1;
|
|
2385
|
+
return evictionScore(b) - evictionScore(a);
|
|
2386
|
+
});
|
|
2387
|
+
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
2090
2388
|
}
|
|
2091
2389
|
await this.persist();
|
|
2092
2390
|
return id;
|
|
2093
2391
|
}
|
|
2094
|
-
async recordDeployment(id) {
|
|
2095
|
-
const
|
|
2096
|
-
if (
|
|
2097
|
-
|
|
2098
|
-
|
|
2392
|
+
async recordDeployment(id, n8nWorkflowId) {
|
|
2393
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
2394
|
+
if (m) {
|
|
2395
|
+
m.deployCount++;
|
|
2396
|
+
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2397
|
+
if (n8nWorkflowId) m.n8nWorkflowId = n8nWorkflowId;
|
|
2099
2398
|
await this.persist();
|
|
2100
2399
|
}
|
|
2101
2400
|
}
|
|
2102
2401
|
async recordOutcome(id, outcome) {
|
|
2103
|
-
const
|
|
2104
|
-
if (!
|
|
2402
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
2403
|
+
if (!m) return;
|
|
2105
2404
|
if (outcome.mode === "direct") {
|
|
2106
|
-
|
|
2405
|
+
m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
|
|
2107
2406
|
} else {
|
|
2108
|
-
|
|
2407
|
+
m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
|
|
2109
2408
|
}
|
|
2110
|
-
const stats =
|
|
2409
|
+
const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
|
|
2111
2410
|
stats.totalUses++;
|
|
2112
2411
|
stats.totalAttempts += outcome.attempts;
|
|
2113
2412
|
if (outcome.firstTryPass) stats.firstTryPasses++;
|
|
@@ -2115,24 +2414,35 @@ var FileLibrary = class {
|
|
|
2115
2414
|
const key = String(rule);
|
|
2116
2415
|
stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
|
|
2117
2416
|
}
|
|
2118
|
-
|
|
2417
|
+
m.outcomeStats = stats;
|
|
2119
2418
|
await this.persist();
|
|
2120
2419
|
}
|
|
2121
2420
|
async drain() {
|
|
2122
2421
|
await this.writeQueue;
|
|
2123
2422
|
}
|
|
2124
2423
|
async get(id) {
|
|
2125
|
-
|
|
2424
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
2425
|
+
if (!m) return null;
|
|
2426
|
+
const workflow = await this.loadWorkflowFile(id);
|
|
2427
|
+
if (!workflow) return null;
|
|
2428
|
+
return { ...m, workflow };
|
|
2126
2429
|
}
|
|
2127
2430
|
async list(filters) {
|
|
2128
|
-
let
|
|
2431
|
+
let filtered = this.meta;
|
|
2129
2432
|
if (filters?.platform) {
|
|
2130
|
-
|
|
2433
|
+
filtered = filtered.filter((m) => m.platform === filters.platform);
|
|
2131
2434
|
}
|
|
2132
2435
|
if (filters?.tags && filters.tags.length > 0) {
|
|
2133
|
-
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2436
|
+
filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
|
|
2437
|
+
}
|
|
2438
|
+
const results = await Promise.all(
|
|
2439
|
+
filtered.map(async (m) => {
|
|
2440
|
+
const workflow = await this.loadWorkflowFile(m.id);
|
|
2441
|
+
if (!workflow) return null;
|
|
2442
|
+
return { ...m, workflow };
|
|
2443
|
+
})
|
|
2444
|
+
);
|
|
2445
|
+
return results.filter((r) => r !== null);
|
|
2136
2446
|
}
|
|
2137
2447
|
deduplicateFailurePatterns(patterns) {
|
|
2138
2448
|
if (!patterns?.length) return void 0;
|
|
@@ -2147,12 +2457,98 @@ var FileLibrary = class {
|
|
|
2147
2457
|
}
|
|
2148
2458
|
return [...map.values()];
|
|
2149
2459
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2460
|
+
// ── Cross-process file locking ────────────────────────────────────────────
|
|
2461
|
+
// Uses O_EXCL (exclusive create) which is atomic on POSIX and Windows NTFS.
|
|
2462
|
+
// Protects the read-modify-write cycle in persist() from concurrent writers
|
|
2463
|
+
// in separate OS processes (e.g. MCP server + CLI running simultaneously).
|
|
2464
|
+
get lockPath() {
|
|
2465
|
+
return join5(this.dir, ".index.lock");
|
|
2466
|
+
}
|
|
2467
|
+
async acquireLock(timeoutMs = 3e3) {
|
|
2468
|
+
const deadline = Date.now() + timeoutMs;
|
|
2469
|
+
let delayMs = 10;
|
|
2470
|
+
while (true) {
|
|
2471
|
+
try {
|
|
2472
|
+
const fh = await open(this.lockPath, "wx");
|
|
2473
|
+
await fh.writeFile(String(process.pid));
|
|
2474
|
+
await fh.close();
|
|
2475
|
+
return async () => {
|
|
2476
|
+
await unlink(this.lockPath).catch(() => {
|
|
2477
|
+
});
|
|
2478
|
+
};
|
|
2479
|
+
} catch {
|
|
2480
|
+
try {
|
|
2481
|
+
const content = await readFile(this.lockPath, "utf-8");
|
|
2482
|
+
const lockPid = parseInt(content.trim(), 10);
|
|
2483
|
+
const fileStat = await stat(this.lockPath);
|
|
2484
|
+
const ageMs = Date.now() - fileStat.mtimeMs;
|
|
2485
|
+
if (ageMs > 1e4) {
|
|
2486
|
+
await unlink(this.lockPath).catch(() => {
|
|
2487
|
+
});
|
|
2488
|
+
continue;
|
|
2489
|
+
}
|
|
2490
|
+
if (!isNaN(lockPid)) {
|
|
2491
|
+
try {
|
|
2492
|
+
process.kill(lockPid, 0);
|
|
2493
|
+
} catch {
|
|
2494
|
+
await unlink(this.lockPath).catch(() => {
|
|
2495
|
+
});
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
} catch {
|
|
2500
|
+
continue;
|
|
2501
|
+
}
|
|
2502
|
+
if (Date.now() > deadline) {
|
|
2503
|
+
return async () => {
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
2507
|
+
delayMs = Math.min(delayMs * 1.5, 200);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Direct write used only during migration (before writeQueue is needed).
|
|
2513
|
+
*/
|
|
2514
|
+
async persistNow() {
|
|
2515
|
+
const releaseLock = await this.acquireLock();
|
|
2516
|
+
try {
|
|
2152
2517
|
const indexPath = join5(this.dir, "index.json");
|
|
2153
2518
|
const tmpPath = `${indexPath}.tmp`;
|
|
2154
|
-
await writeFile2(tmpPath, JSON.stringify(this.
|
|
2519
|
+
await writeFile2(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
2155
2520
|
await rename2(tmpPath, indexPath);
|
|
2521
|
+
} finally {
|
|
2522
|
+
await releaseLock();
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
persist() {
|
|
2526
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
2527
|
+
const releaseLock = await this.acquireLock();
|
|
2528
|
+
try {
|
|
2529
|
+
const indexPath = join5(this.dir, "index.json");
|
|
2530
|
+
let onDisk = [];
|
|
2531
|
+
try {
|
|
2532
|
+
const raw = await readFile(indexPath, "utf-8");
|
|
2533
|
+
const parsed = JSON.parse(raw);
|
|
2534
|
+
if (Array.isArray(parsed)) {
|
|
2535
|
+
onDisk = parsed.filter(isValidMeta);
|
|
2536
|
+
}
|
|
2537
|
+
} catch {
|
|
2538
|
+
}
|
|
2539
|
+
const ourIds = new Set(this.meta.map((m) => m.id));
|
|
2540
|
+
const external = onDisk.filter((m) => !ourIds.has(m.id));
|
|
2541
|
+
let merged = [...this.meta, ...external];
|
|
2542
|
+
if (merged.length > MAX_LIBRARY_SIZE) {
|
|
2543
|
+
merged.sort((a, b) => evictionScore(b) - evictionScore(a));
|
|
2544
|
+
merged = merged.slice(0, MAX_LIBRARY_SIZE);
|
|
2545
|
+
}
|
|
2546
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
2547
|
+
await writeFile2(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
2548
|
+
await rename2(tmpPath, indexPath);
|
|
2549
|
+
} finally {
|
|
2550
|
+
await releaseLock();
|
|
2551
|
+
}
|
|
2156
2552
|
});
|
|
2157
2553
|
return this.writeQueue;
|
|
2158
2554
|
}
|
|
@@ -2163,13 +2559,16 @@ export {
|
|
|
2163
2559
|
KairosError,
|
|
2164
2560
|
ApiError,
|
|
2165
2561
|
ProviderError,
|
|
2562
|
+
GuardError,
|
|
2166
2563
|
N8nApiClient,
|
|
2167
2564
|
N8nFieldStripper,
|
|
2168
2565
|
DEFAULT_REGISTRY,
|
|
2169
2566
|
NodeRegistry,
|
|
2170
2567
|
N8nValidator,
|
|
2171
2568
|
scoreToMode,
|
|
2172
|
-
|
|
2569
|
+
RULE_EXAMPLES,
|
|
2570
|
+
RULE_MITIGATIONS,
|
|
2571
|
+
TelemetryCollector,
|
|
2173
2572
|
TelemetryReader,
|
|
2174
2573
|
PatternAnalyzer,
|
|
2175
2574
|
nullLogger,
|
|
@@ -2180,4 +2579,4 @@ export {
|
|
|
2180
2579
|
buildSearchCorpus,
|
|
2181
2580
|
FileLibrary
|
|
2182
2581
|
};
|
|
2183
|
-
//# sourceMappingURL=chunk-
|
|
2582
|
+
//# sourceMappingURL=chunk-KIFT5LA7.js.map
|