@kairos-sdk/core 0.4.5 → 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 +18 -10
- package/dist/{chunk-6CLI43FI.js → chunk-5GAY7CSJ.js} +109 -13
- package/dist/chunk-5GAY7CSJ.js.map +1 -0
- package/dist/{chunk-CR2NHLOH.js → chunk-EVOAYH2K.js} +57 -11
- package/dist/chunk-EVOAYH2K.js.map +1 -0
- package/dist/{chunk-4TS6GW6O.js → chunk-HBGZTUUZ.js} +26 -13
- package/dist/chunk-HBGZTUUZ.js.map +1 -0
- package/dist/{chunk-6IXW3WCC.js → chunk-KIFT5LA7.js} +532 -76
- package/dist/chunk-KIFT5LA7.js.map +1 -0
- package/dist/cli.cjs +738 -111
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +31 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +711 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -5
- package/dist/mcp-server.cjs +875 -333
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +293 -251
- package/dist/mcp-server.js.map +1 -1
- package/dist/{reader-CpUcHhKW.d.ts → reader-B5mV20H6.d.cts} +34 -4
- package/dist/{reader-CpUcHhKW.d.cts → reader-B5mV20H6.d.ts} +34 -4
- package/dist/standalone.cjs +602 -84
- package/dist/standalone.cjs.map +1 -1
- package/dist/standalone.d.cts +2 -1
- package/dist/standalone.d.ts +2 -1
- package/dist/standalone.js +3 -3
- package/package.json +4 -1
- package/dist/chunk-4TS6GW6O.js.map +0 -1
- package/dist/chunk-6CLI43FI.js.map +0 -1
- package/dist/chunk-6IXW3WCC.js.map +0 -1
- package/dist/chunk-CR2NHLOH.js.map +0 -1
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// src/utils/uuid.ts
|
|
2
2
|
function generateUUID() {
|
|
3
|
-
|
|
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
|
+
});
|
|
4
10
|
}
|
|
5
11
|
|
|
6
12
|
// src/errors/base.ts
|
|
@@ -34,7 +40,26 @@ var ProviderError = class extends KairosError {
|
|
|
34
40
|
}
|
|
35
41
|
};
|
|
36
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
|
+
|
|
37
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
|
+
}
|
|
38
63
|
async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
|
|
39
64
|
let lastError;
|
|
40
65
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
@@ -59,6 +84,7 @@ function fetchWithTimeout(url, init, timeoutMs) {
|
|
|
59
84
|
|
|
60
85
|
// src/providers/n8n/api-client.ts
|
|
61
86
|
var EXECUTION_LIMIT_CAP = 100;
|
|
87
|
+
var N8N_API_PAGE_SIZE = 250;
|
|
62
88
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
63
89
|
var RETRY_ATTEMPTS = 3;
|
|
64
90
|
var RETRY_DELAY_MS = 1e3;
|
|
@@ -67,6 +93,17 @@ var N8nApiClient = class {
|
|
|
67
93
|
this.baseUrl = baseUrl;
|
|
68
94
|
this.apiKey = apiKey;
|
|
69
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
|
+
}
|
|
70
107
|
}
|
|
71
108
|
baseUrl;
|
|
72
109
|
apiKey;
|
|
@@ -76,7 +113,12 @@ var N8nApiClient = class {
|
|
|
76
113
|
this.logger.debug(`n8n ${method} ${path}`);
|
|
77
114
|
const isSafe = method === "GET";
|
|
78
115
|
if (!isSafe) {
|
|
79
|
-
return
|
|
116
|
+
return withRetry(
|
|
117
|
+
() => this.singleRequest(url, method, path, body),
|
|
118
|
+
2,
|
|
119
|
+
RETRY_DELAY_MS,
|
|
120
|
+
isTransientNetworkError
|
|
121
|
+
);
|
|
80
122
|
}
|
|
81
123
|
return withRetry(
|
|
82
124
|
() => this.singleRequest(url, method, path, body),
|
|
@@ -131,7 +173,7 @@ var N8nApiClient = class {
|
|
|
131
173
|
}
|
|
132
174
|
async listWorkflows() {
|
|
133
175
|
const all = [];
|
|
134
|
-
let path =
|
|
176
|
+
let path = `/workflows?limit=${N8N_API_PAGE_SIZE}`;
|
|
135
177
|
for (; ; ) {
|
|
136
178
|
const response = await this.request("GET", path);
|
|
137
179
|
for (const w of response.data) {
|
|
@@ -145,7 +187,7 @@ var N8nApiClient = class {
|
|
|
145
187
|
});
|
|
146
188
|
}
|
|
147
189
|
if (!response.nextCursor) break;
|
|
148
|
-
path = `/workflows?limit
|
|
190
|
+
path = `/workflows?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
149
191
|
}
|
|
150
192
|
return all;
|
|
151
193
|
}
|
|
@@ -175,14 +217,14 @@ var N8nApiClient = class {
|
|
|
175
217
|
}
|
|
176
218
|
async listTags() {
|
|
177
219
|
const all = [];
|
|
178
|
-
let path =
|
|
220
|
+
let path = `/tags?limit=${N8N_API_PAGE_SIZE}`;
|
|
179
221
|
for (; ; ) {
|
|
180
222
|
const response = await this.request("GET", path);
|
|
181
223
|
for (const t of response.data) {
|
|
182
224
|
all.push({ id: t.id, name: t.name });
|
|
183
225
|
}
|
|
184
226
|
if (!response.nextCursor) break;
|
|
185
|
-
path = `/tags?limit
|
|
227
|
+
path = `/tags?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
186
228
|
}
|
|
187
229
|
return all;
|
|
188
230
|
}
|
|
@@ -206,6 +248,32 @@ var N8nApiClient = class {
|
|
|
206
248
|
return [];
|
|
207
249
|
}
|
|
208
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
|
+
}
|
|
209
277
|
mapExecution(e) {
|
|
210
278
|
return {
|
|
211
279
|
id: e.id,
|
|
@@ -353,6 +421,14 @@ var NodeRegistry = class {
|
|
|
353
421
|
if (!def) return true;
|
|
354
422
|
return def.safeTypeVersions.includes(version);
|
|
355
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
|
+
}
|
|
356
432
|
getRequiredParams(type) {
|
|
357
433
|
return this.byType.get(type)?.requiredParams ?? [];
|
|
358
434
|
}
|
|
@@ -405,6 +481,14 @@ var N8nValidator = class {
|
|
|
405
481
|
this.checkRule24(workflow, issues);
|
|
406
482
|
this.checkRule25(workflow, issues);
|
|
407
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);
|
|
408
492
|
if (Array.isArray(workflow.nodes)) {
|
|
409
493
|
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
410
494
|
for (const issue of issues) {
|
|
@@ -656,19 +740,22 @@ var N8nValidator = class {
|
|
|
656
740
|
}
|
|
657
741
|
}
|
|
658
742
|
}
|
|
659
|
-
// 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.
|
|
660
746
|
checkRule19(w, issues) {
|
|
661
747
|
if (!Array.isArray(w.nodes)) return;
|
|
748
|
+
const strict = process.env["KAIROS_REGISTRY_STRICT"] === "true";
|
|
662
749
|
for (const node of w.nodes) {
|
|
663
750
|
if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
|
|
664
|
-
if (
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
+
);
|
|
672
759
|
}
|
|
673
760
|
}
|
|
674
761
|
// Rule 20 (WARN): cycle detection — no node should be reachable from itself
|
|
@@ -717,6 +804,27 @@ var N8nValidator = class {
|
|
|
717
804
|
}
|
|
718
805
|
}
|
|
719
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
|
+
}
|
|
720
828
|
// Rule 22 (WARN): check requiredParams from registry
|
|
721
829
|
checkRule22(w, issues) {
|
|
722
830
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -825,23 +933,162 @@ var N8nValidator = class {
|
|
|
825
933
|
walk(params);
|
|
826
934
|
return expressions;
|
|
827
935
|
}
|
|
828
|
-
// Rule
|
|
829
|
-
|
|
936
|
+
// Rule 27 (WARN): httpRequest URL is a placeholder
|
|
937
|
+
checkRule27(w, issues) {
|
|
830
938
|
if (!Array.isArray(w.nodes)) return;
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
939
|
+
const PLACEHOLDER_RE = [
|
|
940
|
+
/^https?:\/\/example\.com/i,
|
|
941
|
+
/your[-_]?(api[-_]?)?url/i,
|
|
942
|
+
/^https?:\/\/$/,
|
|
943
|
+
/^<.+>$/,
|
|
944
|
+
/placeholder/i
|
|
945
|
+
];
|
|
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()))) {
|
|
840
952
|
this.warn(
|
|
841
953
|
issues,
|
|
842
|
-
|
|
843
|
-
`
|
|
844
|
-
|
|
954
|
+
27,
|
|
955
|
+
`Node "${node.name}" httpRequest URL appears to be a placeholder: "${url}" \u2014 replace with your actual endpoint`,
|
|
956
|
+
node.id
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
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
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
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
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
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
|
+
}
|
|
1008
|
+
}
|
|
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
|
+
}
|
|
1029
|
+
}
|
|
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
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
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);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
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
|
|
845
1092
|
);
|
|
846
1093
|
}
|
|
847
1094
|
}
|
|
@@ -943,19 +1190,20 @@ var TelemetryReader = class {
|
|
|
943
1190
|
}
|
|
944
1191
|
const events = await this.readRecentEvents(days);
|
|
945
1192
|
const buildSessions = new Set(
|
|
946
|
-
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)
|
|
947
1194
|
);
|
|
948
1195
|
const MIN_BUILDS_FOR_RATES = 3;
|
|
949
1196
|
if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
|
|
950
1197
|
const ruleSessions = /* @__PURE__ */ new Map();
|
|
951
1198
|
for (const event of events) {
|
|
952
1199
|
if (event.eventType !== "generation_attempt") continue;
|
|
953
|
-
|
|
1200
|
+
const eventKey = event.runId ?? event.sessionId;
|
|
1201
|
+
if (!buildSessions.has(eventKey)) continue;
|
|
954
1202
|
const data = event.data;
|
|
955
1203
|
if (data.validationPassed || !data.issues) continue;
|
|
956
1204
|
for (const issue of data.issues) {
|
|
957
1205
|
const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
|
|
958
|
-
entry.sessions.add(
|
|
1206
|
+
entry.sessions.add(eventKey);
|
|
959
1207
|
entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
|
|
960
1208
|
ruleSessions.set(issue.rule, entry);
|
|
961
1209
|
}
|
|
@@ -994,7 +1242,7 @@ import { join as join4 } from "path";
|
|
|
994
1242
|
import { homedir as homedir3 } from "os";
|
|
995
1243
|
|
|
996
1244
|
// src/validation/rule-metadata.ts
|
|
997
|
-
var VALIDATOR_RULE_IDS = Array.from({ length:
|
|
1245
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 34 }, (_, i) => i + 1);
|
|
998
1246
|
var RULE_PIPELINE_STAGES = {
|
|
999
1247
|
1: "node_generation",
|
|
1000
1248
|
2: "node_generation",
|
|
@@ -1021,7 +1269,15 @@ var RULE_PIPELINE_STAGES = {
|
|
|
1021
1269
|
23: "node_generation",
|
|
1022
1270
|
24: "expression_syntax",
|
|
1023
1271
|
25: "expression_syntax",
|
|
1024
|
-
26: "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"
|
|
1025
1281
|
};
|
|
1026
1282
|
var RULE_EXAMPLES = {
|
|
1027
1283
|
17: {
|
|
@@ -1039,6 +1295,38 @@ var RULE_EXAMPLES = {
|
|
|
1039
1295
|
26: {
|
|
1040
1296
|
bad: "$('Fetch Data').json.email",
|
|
1041
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"'
|
|
1042
1330
|
}
|
|
1043
1331
|
};
|
|
1044
1332
|
var RULE_MITIGATIONS = {
|
|
@@ -1067,7 +1355,15 @@ var RULE_MITIGATIONS = {
|
|
|
1067
1355
|
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
1068
1356
|
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
1069
1357
|
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
1070
|
-
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
|
|
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")'
|
|
1071
1367
|
};
|
|
1072
1368
|
|
|
1073
1369
|
// src/telemetry/pattern-analyzer.ts
|
|
@@ -1076,22 +1372,24 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1076
1372
|
telemetryDir;
|
|
1077
1373
|
outputDir;
|
|
1078
1374
|
_cachedEvents = null;
|
|
1375
|
+
_cachedPreviousPatterns = null;
|
|
1079
1376
|
constructor(telemetryDir) {
|
|
1080
1377
|
const defaultDir = join4(homedir3(), ".kairos", "telemetry");
|
|
1081
1378
|
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
1082
1379
|
this.outputDir = telemetryDir ? join4(telemetryDir, "..") : join4(homedir3(), ".kairos");
|
|
1083
1380
|
}
|
|
1084
1381
|
async loadPreviousPatterns() {
|
|
1382
|
+
if (this._cachedPreviousPatterns !== null) return this._cachedPreviousPatterns;
|
|
1085
1383
|
try {
|
|
1086
1384
|
const raw = await fsReadFile(join4(this.outputDir, "patterns.json"), "utf-8");
|
|
1087
1385
|
const prev = JSON.parse(raw);
|
|
1088
1386
|
const version = prev.schemaVersion ?? 0;
|
|
1089
1387
|
const patterns = prev.topFailureRules ?? [];
|
|
1090
|
-
|
|
1091
|
-
return this.migratePatterns(patterns, version);
|
|
1388
|
+
this._cachedPreviousPatterns = version === PATTERN_SCHEMA_VERSION ? patterns : this.migratePatterns(patterns, version);
|
|
1092
1389
|
} catch {
|
|
1093
|
-
|
|
1390
|
+
this._cachedPreviousPatterns = [];
|
|
1094
1391
|
}
|
|
1392
|
+
return this._cachedPreviousPatterns;
|
|
1095
1393
|
}
|
|
1096
1394
|
migratePatterns(patterns, fromVersion) {
|
|
1097
1395
|
let migrated = patterns;
|
|
@@ -1391,6 +1689,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1391
1689
|
const tmpPath = `${outputPath}.tmp`;
|
|
1392
1690
|
await writeFile(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
1393
1691
|
await rename(tmpPath, outputPath);
|
|
1692
|
+
this._cachedPreviousPatterns = null;
|
|
1394
1693
|
const historySummary = {
|
|
1395
1694
|
timestamp: analysis.generatedAt,
|
|
1396
1695
|
totalBuilds: analysis.summary.totalBuilds,
|
|
@@ -1439,7 +1738,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1439
1738
|
})
|
|
1440
1739
|
));
|
|
1441
1740
|
return {
|
|
1442
|
-
sessionId: bc.sessionId,
|
|
1741
|
+
sessionId: bc.runId ?? bc.sessionId,
|
|
1443
1742
|
date: bc.fileDate,
|
|
1444
1743
|
description: data.description ?? "",
|
|
1445
1744
|
workflowType: data.workflowType ?? null,
|
|
@@ -1472,7 +1771,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1472
1771
|
alerts.push({
|
|
1473
1772
|
type: "stale_pattern",
|
|
1474
1773
|
rule: p.rule,
|
|
1475
|
-
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)`
|
|
1476
1775
|
});
|
|
1477
1776
|
}
|
|
1478
1777
|
}
|
|
@@ -1560,12 +1859,32 @@ var nullLogger = {
|
|
|
1560
1859
|
};
|
|
1561
1860
|
|
|
1562
1861
|
// src/library/scorer.ts
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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();
|
|
1569
1888
|
var NODE_KEYWORDS = {
|
|
1570
1889
|
slack: ["slack", "slackApi"],
|
|
1571
1890
|
email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
|
|
@@ -1750,6 +2069,8 @@ function clusterWorkflows(workflows) {
|
|
|
1750
2069
|
}
|
|
1751
2070
|
return clusters.sort((a, b) => b.members.length - a.members.length);
|
|
1752
2071
|
}
|
|
2072
|
+
var NOVELTY_BOOST = 0.05;
|
|
2073
|
+
var NOVELTY_PENALTY = 0.03;
|
|
1753
2074
|
function rerank(candidates, clusters) {
|
|
1754
2075
|
const clusterMap = /* @__PURE__ */ new Map();
|
|
1755
2076
|
for (const cluster of clusters) {
|
|
@@ -1757,7 +2078,7 @@ function rerank(candidates, clusters) {
|
|
|
1757
2078
|
clusterMap.set(member.id, cluster);
|
|
1758
2079
|
}
|
|
1759
2080
|
}
|
|
1760
|
-
|
|
2081
|
+
const pass1 = candidates.map((c) => {
|
|
1761
2082
|
const cluster = clusterMap.get(c.workflow.id);
|
|
1762
2083
|
let boost = 0;
|
|
1763
2084
|
if (cluster && cluster.avgFirstTryPassRate > 0) {
|
|
@@ -1769,13 +2090,31 @@ function rerank(candidates, clusters) {
|
|
|
1769
2090
|
return {
|
|
1770
2091
|
workflow: c.workflow,
|
|
1771
2092
|
score: Math.max(0, Math.min(1, c.score + boost)),
|
|
1772
|
-
|
|
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 } : {}
|
|
1773
2112
|
};
|
|
1774
2113
|
}).sort((a, b) => b.score - a.score);
|
|
1775
2114
|
}
|
|
1776
2115
|
|
|
1777
2116
|
// src/library/file-library.ts
|
|
1778
|
-
import { readFile, writeFile as writeFile2, rename as rename2, mkdir as mkdir3, stat } from "fs/promises";
|
|
2117
|
+
import { readFile, writeFile as writeFile2, rename as rename2, mkdir as mkdir3, stat, readdir as readdir2, unlink, open } from "fs/promises";
|
|
1779
2118
|
import { join as join5 } from "path";
|
|
1780
2119
|
import { homedir as homedir4 } from "os";
|
|
1781
2120
|
|
|
@@ -1800,7 +2139,11 @@ function buildSearchCorpus(w) {
|
|
|
1800
2139
|
});
|
|
1801
2140
|
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
1802
2141
|
}
|
|
1803
|
-
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
|
+
}
|
|
1804
2147
|
function isValidMeta(item) {
|
|
1805
2148
|
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
1806
2149
|
}
|
|
@@ -1848,6 +2191,7 @@ var FileLibrary = class {
|
|
|
1848
2191
|
} catch {
|
|
1849
2192
|
this.meta = [];
|
|
1850
2193
|
}
|
|
2194
|
+
await this.scanForOrphansAndCleanup();
|
|
1851
2195
|
} else {
|
|
1852
2196
|
try {
|
|
1853
2197
|
const raw = await readFile(indexPath, "utf-8");
|
|
@@ -1862,6 +2206,31 @@ var FileLibrary = class {
|
|
|
1862
2206
|
await mkdir3(this.workflowsDir, { recursive: true });
|
|
1863
2207
|
}
|
|
1864
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);
|
|
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
|
+
}
|
|
1865
2234
|
/**
|
|
1866
2235
|
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
1867
2236
|
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
@@ -1932,10 +2301,12 @@ var FileLibrary = class {
|
|
|
1932
2301
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
1933
2302
|
const docCount = shells.length;
|
|
1934
2303
|
const idf = /* @__PURE__ */ new Map();
|
|
2304
|
+
const idfCeiling = Math.log(docCount + 1) + 1;
|
|
1935
2305
|
const allTokens = new Set(queryTokens);
|
|
1936
2306
|
for (const token of allTokens) {
|
|
1937
2307
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
1938
|
-
|
|
2308
|
+
const rawIdf = Math.log((docCount + 1) / (docsWithToken + 1)) + 1;
|
|
2309
|
+
idf.set(token, rawIdf / idfCeiling);
|
|
1939
2310
|
}
|
|
1940
2311
|
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
1941
2312
|
const clusters = clusterWorkflows(shells);
|
|
@@ -1961,6 +2332,27 @@ var FileLibrary = class {
|
|
|
1961
2332
|
return results.filter((r) => r !== null);
|
|
1962
2333
|
}
|
|
1963
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
|
+
}
|
|
1964
2356
|
const id = generateUUID();
|
|
1965
2357
|
await this.writeWorkflowFile(id, workflow);
|
|
1966
2358
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
@@ -1982,25 +2374,27 @@ var FileLibrary = class {
|
|
|
1982
2374
|
...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
|
|
1983
2375
|
...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
|
|
1984
2376
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
1985
|
-
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
2377
|
+
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {},
|
|
2378
|
+
...metadata.n8nWorkflowId ? { n8nWorkflowId: metadata.n8nWorkflowId } : {}
|
|
1986
2379
|
};
|
|
1987
2380
|
this.meta.push(meta);
|
|
1988
2381
|
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
1989
2382
|
this.meta.sort((a, b) => {
|
|
1990
2383
|
if (a.id === id) return -1;
|
|
1991
2384
|
if (b.id === id) return 1;
|
|
1992
|
-
return (b
|
|
2385
|
+
return evictionScore(b) - evictionScore(a);
|
|
1993
2386
|
});
|
|
1994
2387
|
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
1995
2388
|
}
|
|
1996
2389
|
await this.persist();
|
|
1997
2390
|
return id;
|
|
1998
2391
|
}
|
|
1999
|
-
async recordDeployment(id) {
|
|
2392
|
+
async recordDeployment(id, n8nWorkflowId) {
|
|
2000
2393
|
const m = this.meta.find((m2) => m2.id === id);
|
|
2001
2394
|
if (m) {
|
|
2002
2395
|
m.deployCount++;
|
|
2003
2396
|
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2397
|
+
if (n8nWorkflowId) m.n8nWorkflowId = n8nWorkflowId;
|
|
2004
2398
|
await this.persist();
|
|
2005
2399
|
}
|
|
2006
2400
|
}
|
|
@@ -2063,37 +2457,98 @@ var FileLibrary = class {
|
|
|
2063
2457
|
}
|
|
2064
2458
|
return [...map.values()];
|
|
2065
2459
|
}
|
|
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
|
+
}
|
|
2066
2511
|
/**
|
|
2067
2512
|
* Direct write used only during migration (before writeQueue is needed).
|
|
2068
2513
|
*/
|
|
2069
2514
|
async persistNow() {
|
|
2070
|
-
const
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2515
|
+
const releaseLock = await this.acquireLock();
|
|
2516
|
+
try {
|
|
2517
|
+
const indexPath = join5(this.dir, "index.json");
|
|
2518
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
2519
|
+
await writeFile2(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
2520
|
+
await rename2(tmpPath, indexPath);
|
|
2521
|
+
} finally {
|
|
2522
|
+
await releaseLock();
|
|
2523
|
+
}
|
|
2074
2524
|
}
|
|
2075
2525
|
persist() {
|
|
2076
2526
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
2077
|
-
const
|
|
2078
|
-
let onDisk = [];
|
|
2527
|
+
const releaseLock = await this.acquireLock();
|
|
2079
2528
|
try {
|
|
2080
|
-
const
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
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 {
|
|
2084
2538
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
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();
|
|
2093
2551
|
}
|
|
2094
|
-
const tmpPath = `${indexPath}.tmp`;
|
|
2095
|
-
await writeFile2(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
2096
|
-
await rename2(tmpPath, indexPath);
|
|
2097
2552
|
});
|
|
2098
2553
|
return this.writeQueue;
|
|
2099
2554
|
}
|
|
@@ -2104,6 +2559,7 @@ export {
|
|
|
2104
2559
|
KairosError,
|
|
2105
2560
|
ApiError,
|
|
2106
2561
|
ProviderError,
|
|
2562
|
+
GuardError,
|
|
2107
2563
|
N8nApiClient,
|
|
2108
2564
|
N8nFieldStripper,
|
|
2109
2565
|
DEFAULT_REGISTRY,
|
|
@@ -2123,4 +2579,4 @@ export {
|
|
|
2123
2579
|
buildSearchCorpus,
|
|
2124
2580
|
FileLibrary
|
|
2125
2581
|
};
|
|
2126
|
-
//# sourceMappingURL=chunk-
|
|
2582
|
+
//# sourceMappingURL=chunk-KIFT5LA7.js.map
|