@kairos-sdk/core 0.4.5 → 0.5.1
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 +28 -11
- package/dist/{chunk-4TS6GW6O.js → chunk-GVZKMS53.js} +27 -16
- package/dist/chunk-GVZKMS53.js.map +1 -0
- package/dist/{chunk-6CLI43FI.js → chunk-MYAGTDQ2.js} +109 -13
- package/dist/chunk-MYAGTDQ2.js.map +1 -0
- package/dist/{chunk-CR2NHLOH.js → chunk-V2IZBZGB.js} +57 -11
- package/dist/chunk-V2IZBZGB.js.map +1 -0
- package/dist/{chunk-6IXW3WCC.js → chunk-VPPWTMRJ.js} +533 -77
- package/dist/chunk-VPPWTMRJ.js.map +1 -0
- package/dist/cli.cjs +790 -145
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +54 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +740 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.js +5 -5
- package/dist/mcp-server.cjs +960 -347
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +374 -261
- 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 +603 -85
- 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 +15 -5
- 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
package/dist/index.cjs
CHANGED
|
@@ -63,7 +63,13 @@ var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
|
63
63
|
|
|
64
64
|
// src/utils/uuid.ts
|
|
65
65
|
function generateUUID() {
|
|
66
|
-
|
|
66
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
67
|
+
return crypto.randomUUID();
|
|
68
|
+
}
|
|
69
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
70
|
+
const r = Math.random() * 16 | 0;
|
|
71
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
72
|
+
});
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
// src/library/null-library.ts
|
|
@@ -119,7 +125,26 @@ var ProviderError = class extends KairosError {
|
|
|
119
125
|
}
|
|
120
126
|
};
|
|
121
127
|
|
|
128
|
+
// src/errors/guard-error.ts
|
|
129
|
+
var GuardError = class extends KairosError {
|
|
130
|
+
constructor(message) {
|
|
131
|
+
super(message);
|
|
132
|
+
this.name = "GuardError";
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
122
136
|
// src/utils/retry.ts
|
|
137
|
+
function isTransientNetworkError(err) {
|
|
138
|
+
const TRANSIENT_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "ENOTFOUND", "ECONNABORTED"]);
|
|
139
|
+
let current = err;
|
|
140
|
+
for (let i = 0; i < 4; i++) {
|
|
141
|
+
if (current === null || typeof current !== "object") break;
|
|
142
|
+
const code = current.code;
|
|
143
|
+
if (typeof code === "string" && TRANSIENT_CODES.has(code)) return true;
|
|
144
|
+
current = current.cause;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
123
148
|
async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
|
|
124
149
|
let lastError;
|
|
125
150
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
@@ -144,6 +169,7 @@ function fetchWithTimeout(url, init, timeoutMs) {
|
|
|
144
169
|
|
|
145
170
|
// src/providers/n8n/api-client.ts
|
|
146
171
|
var EXECUTION_LIMIT_CAP = 100;
|
|
172
|
+
var N8N_API_PAGE_SIZE = 250;
|
|
147
173
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
148
174
|
var RETRY_ATTEMPTS = 3;
|
|
149
175
|
var RETRY_DELAY_MS = 1e3;
|
|
@@ -152,6 +178,17 @@ var N8nApiClient = class {
|
|
|
152
178
|
this.baseUrl = baseUrl;
|
|
153
179
|
this.apiKey = apiKey;
|
|
154
180
|
this.logger = logger;
|
|
181
|
+
if (!baseUrl || typeof baseUrl !== "string") {
|
|
182
|
+
throw new GuardError("N8nApiClient: baseUrl must be a non-empty string");
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
new URL(baseUrl);
|
|
186
|
+
} catch {
|
|
187
|
+
throw new GuardError(`N8nApiClient: baseUrl is not a valid URL: "${baseUrl}"`);
|
|
188
|
+
}
|
|
189
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
190
|
+
throw new GuardError("N8nApiClient: apiKey must be a non-empty string");
|
|
191
|
+
}
|
|
155
192
|
}
|
|
156
193
|
baseUrl;
|
|
157
194
|
apiKey;
|
|
@@ -161,7 +198,12 @@ var N8nApiClient = class {
|
|
|
161
198
|
this.logger.debug(`n8n ${method} ${path}`);
|
|
162
199
|
const isSafe = method === "GET";
|
|
163
200
|
if (!isSafe) {
|
|
164
|
-
return
|
|
201
|
+
return withRetry(
|
|
202
|
+
() => this.singleRequest(url, method, path, body),
|
|
203
|
+
2,
|
|
204
|
+
RETRY_DELAY_MS,
|
|
205
|
+
isTransientNetworkError
|
|
206
|
+
);
|
|
165
207
|
}
|
|
166
208
|
return withRetry(
|
|
167
209
|
() => this.singleRequest(url, method, path, body),
|
|
@@ -216,7 +258,7 @@ var N8nApiClient = class {
|
|
|
216
258
|
}
|
|
217
259
|
async listWorkflows() {
|
|
218
260
|
const all = [];
|
|
219
|
-
let path =
|
|
261
|
+
let path = `/workflows?limit=${N8N_API_PAGE_SIZE}`;
|
|
220
262
|
for (; ; ) {
|
|
221
263
|
const response = await this.request("GET", path);
|
|
222
264
|
for (const w of response.data) {
|
|
@@ -230,7 +272,7 @@ var N8nApiClient = class {
|
|
|
230
272
|
});
|
|
231
273
|
}
|
|
232
274
|
if (!response.nextCursor) break;
|
|
233
|
-
path = `/workflows?limit
|
|
275
|
+
path = `/workflows?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
234
276
|
}
|
|
235
277
|
return all;
|
|
236
278
|
}
|
|
@@ -260,14 +302,14 @@ var N8nApiClient = class {
|
|
|
260
302
|
}
|
|
261
303
|
async listTags() {
|
|
262
304
|
const all = [];
|
|
263
|
-
let path =
|
|
305
|
+
let path = `/tags?limit=${N8N_API_PAGE_SIZE}`;
|
|
264
306
|
for (; ; ) {
|
|
265
307
|
const response = await this.request("GET", path);
|
|
266
308
|
for (const t of response.data) {
|
|
267
309
|
all.push({ id: t.id, name: t.name });
|
|
268
310
|
}
|
|
269
311
|
if (!response.nextCursor) break;
|
|
270
|
-
path = `/tags?limit
|
|
312
|
+
path = `/tags?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
271
313
|
}
|
|
272
314
|
return all;
|
|
273
315
|
}
|
|
@@ -291,6 +333,32 @@ var N8nApiClient = class {
|
|
|
291
333
|
return [];
|
|
292
334
|
}
|
|
293
335
|
}
|
|
336
|
+
async triggerManual(workflowId) {
|
|
337
|
+
const raw = await this.request("POST", `/workflows/${workflowId}/run`);
|
|
338
|
+
const inner = raw["data"];
|
|
339
|
+
const execId = inner?.["executionId"] ?? raw["executionId"];
|
|
340
|
+
if (execId === void 0 || execId === null) {
|
|
341
|
+
throw new ProviderError(
|
|
342
|
+
`n8n trigger response missing executionId \u2014 got: ${JSON.stringify(raw)}`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return String(execId);
|
|
346
|
+
}
|
|
347
|
+
async triggerWebhookTest(path) {
|
|
348
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
349
|
+
const url = `${this.baseUrl.replace(/\/$/, "")}/webhook-test${cleanPath}`;
|
|
350
|
+
this.logger.debug(`n8n POST webhook-test ${cleanPath}`);
|
|
351
|
+
try {
|
|
352
|
+
const response = await fetchWithTimeout(
|
|
353
|
+
url,
|
|
354
|
+
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) },
|
|
355
|
+
REQUEST_TIMEOUT_MS
|
|
356
|
+
);
|
|
357
|
+
return response.status;
|
|
358
|
+
} catch (err) {
|
|
359
|
+
throw new ProviderError(`Webhook test request failed for path "${path}"`, err);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
294
362
|
mapExecution(e) {
|
|
295
363
|
return {
|
|
296
364
|
id: e.id,
|
|
@@ -338,15 +406,9 @@ var N8nFieldStripper = class {
|
|
|
338
406
|
}
|
|
339
407
|
};
|
|
340
408
|
|
|
341
|
-
// src/errors/guard-error.ts
|
|
342
|
-
var GuardError = class extends KairosError {
|
|
343
|
-
constructor(message) {
|
|
344
|
-
super(message);
|
|
345
|
-
this.name = "GuardError";
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
|
|
349
409
|
// src/providers/n8n/provider.ts
|
|
410
|
+
var SMOKE_TEST_TIMEOUT_MS = 3e4;
|
|
411
|
+
var SMOKE_TEST_POLL_INTERVAL_MS = 1e3;
|
|
350
412
|
var N8nProvider = class {
|
|
351
413
|
constructor(client, stripper) {
|
|
352
414
|
this.client = client;
|
|
@@ -408,6 +470,101 @@ var N8nProvider = class {
|
|
|
408
470
|
async untag(workflowId, tagIds) {
|
|
409
471
|
await this.client.untagWorkflow(workflowId, tagIds);
|
|
410
472
|
}
|
|
473
|
+
async smokeTest(workflowId, workflow) {
|
|
474
|
+
const start = Date.now();
|
|
475
|
+
const trigger = this.detectTrigger(workflow);
|
|
476
|
+
if (trigger.type === "unsupported") {
|
|
477
|
+
return { status: "not-applicable", triggerType: "not-applicable" };
|
|
478
|
+
}
|
|
479
|
+
if (trigger.type === "manual") {
|
|
480
|
+
let executionId;
|
|
481
|
+
try {
|
|
482
|
+
executionId = await this.client.triggerManual(workflowId);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
return { status: "error", triggerType: "manual", durationMs: Date.now() - start, error: String(err) };
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const execution = await this.pollExecution(executionId);
|
|
488
|
+
const durationMs = Date.now() - start;
|
|
489
|
+
if (execution.status === "success") {
|
|
490
|
+
return { status: "passed", triggerType: "manual", executionId, durationMs };
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
status: "failed",
|
|
494
|
+
triggerType: "manual",
|
|
495
|
+
executionId,
|
|
496
|
+
durationMs,
|
|
497
|
+
error: `Execution ended with status: ${execution.status}`
|
|
498
|
+
};
|
|
499
|
+
} catch (err) {
|
|
500
|
+
return { status: "error", triggerType: "manual", executionId, durationMs: Date.now() - start, error: String(err) };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
const statusCode = await this.client.triggerWebhookTest(trigger.path);
|
|
505
|
+
const durationMs = Date.now() - start;
|
|
506
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
507
|
+
return { status: "passed", triggerType: "webhook", durationMs };
|
|
508
|
+
}
|
|
509
|
+
return { status: "failed", triggerType: "webhook", durationMs, error: `Webhook returned HTTP ${statusCode}` };
|
|
510
|
+
} catch (err) {
|
|
511
|
+
return { status: "error", triggerType: "webhook", durationMs: Date.now() - start, error: String(err) };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
detectTrigger(workflow) {
|
|
515
|
+
for (const node of workflow.nodes) {
|
|
516
|
+
if (node.type === "n8n-nodes-base.manualTrigger") return { type: "manual" };
|
|
517
|
+
if (node.type === "n8n-nodes-base.webhook") {
|
|
518
|
+
const params = node.parameters;
|
|
519
|
+
const path = typeof params?.["path"] === "string" ? params["path"] : "webhook";
|
|
520
|
+
return { type: "webhook", path };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return { type: "unsupported" };
|
|
524
|
+
}
|
|
525
|
+
async pollExecution(executionId) {
|
|
526
|
+
const deadline = Date.now() + SMOKE_TEST_TIMEOUT_MS;
|
|
527
|
+
for (; ; ) {
|
|
528
|
+
const execution = await this.client.getExecution(executionId);
|
|
529
|
+
if (execution.status !== "running" && execution.status !== "waiting") {
|
|
530
|
+
return execution;
|
|
531
|
+
}
|
|
532
|
+
const remaining = deadline - Date.now();
|
|
533
|
+
if (remaining <= 0) break;
|
|
534
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(SMOKE_TEST_POLL_INTERVAL_MS, remaining)));
|
|
535
|
+
}
|
|
536
|
+
throw new ProviderError(`Smoke test: execution ${executionId} did not complete within ${SMOKE_TEST_TIMEOUT_MS}ms`);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// src/errors/generation-error.ts
|
|
541
|
+
var GenerationError = class extends KairosError {
|
|
542
|
+
constructor(message, cause) {
|
|
543
|
+
super(message, cause);
|
|
544
|
+
this.name = "GenerationError";
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// src/errors/response-parse-error.ts
|
|
549
|
+
var ResponseParseError = class extends KairosError {
|
|
550
|
+
constructor(message, cause) {
|
|
551
|
+
super(message, cause);
|
|
552
|
+
this.name = "ResponseParseError";
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/errors/validation-error.ts
|
|
557
|
+
var ValidationError = class extends KairosError {
|
|
558
|
+
constructor(message, issues, attemptMetadata, warnedRules) {
|
|
559
|
+
super(message);
|
|
560
|
+
this.issues = issues;
|
|
561
|
+
this.attemptMetadata = attemptMetadata;
|
|
562
|
+
this.warnedRules = warnedRules;
|
|
563
|
+
this.name = "ValidationError";
|
|
564
|
+
}
|
|
565
|
+
issues;
|
|
566
|
+
attemptMetadata;
|
|
567
|
+
warnedRules;
|
|
411
568
|
};
|
|
412
569
|
|
|
413
570
|
// src/validation/registry.ts
|
|
@@ -510,6 +667,14 @@ var NodeRegistry = class {
|
|
|
510
667
|
if (!def) return true;
|
|
511
668
|
return def.safeTypeVersions.includes(version);
|
|
512
669
|
}
|
|
670
|
+
// Returns true when the version is a positive integer greater than the highest
|
|
671
|
+
// known safe version — indicates a newer release rather than a bad value.
|
|
672
|
+
isVersionNewer(type, version) {
|
|
673
|
+
const def = this.byType.get(type);
|
|
674
|
+
if (!def || def.safeTypeVersions.length === 0) return false;
|
|
675
|
+
const max = Math.max(...def.safeTypeVersions);
|
|
676
|
+
return Number.isInteger(version) && version > max;
|
|
677
|
+
}
|
|
513
678
|
getRequiredParams(type) {
|
|
514
679
|
return this.byType.get(type)?.requiredParams ?? [];
|
|
515
680
|
}
|
|
@@ -562,6 +727,14 @@ var N8nValidator = class {
|
|
|
562
727
|
this.checkRule24(workflow, issues);
|
|
563
728
|
this.checkRule25(workflow, issues);
|
|
564
729
|
this.checkRule26(workflow, issues);
|
|
730
|
+
this.checkRule27(workflow, issues);
|
|
731
|
+
this.checkRule28(workflow, issues);
|
|
732
|
+
this.checkRule29(workflow, issues);
|
|
733
|
+
this.checkRule30(workflow, issues);
|
|
734
|
+
this.checkRule31(workflow, issues);
|
|
735
|
+
this.checkRule32(workflow, issues);
|
|
736
|
+
this.checkRule33(workflow, issues);
|
|
737
|
+
this.checkRule34(workflow, issues);
|
|
565
738
|
if (Array.isArray(workflow.nodes)) {
|
|
566
739
|
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
567
740
|
for (const issue of issues) {
|
|
@@ -813,19 +986,22 @@ var N8nValidator = class {
|
|
|
813
986
|
}
|
|
814
987
|
}
|
|
815
988
|
}
|
|
816
|
-
// Rule 19 (WARN): typeVersion is within known safe range for registered node types
|
|
989
|
+
// Rule 19 (WARN): typeVersion is within known safe range for registered node types.
|
|
990
|
+
// In lenient mode (KAIROS_REGISTRY_STRICT != 'true'), versions higher than the known
|
|
991
|
+
// max are allowed — they likely represent newer n8n releases Kairos hasn't catalogued yet.
|
|
817
992
|
checkRule19(w, issues) {
|
|
818
993
|
if (!Array.isArray(w.nodes)) return;
|
|
994
|
+
const strict = process.env["KAIROS_REGISTRY_STRICT"] === "true";
|
|
819
995
|
for (const node of w.nodes) {
|
|
820
996
|
if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
997
|
+
if (this.registry.isVersionSafe(node.type, node.typeVersion)) continue;
|
|
998
|
+
if (!strict && this.registry.isVersionNewer(node.type, node.typeVersion)) continue;
|
|
999
|
+
this.warn(
|
|
1000
|
+
issues,
|
|
1001
|
+
19,
|
|
1002
|
+
`Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
|
|
1003
|
+
node.id
|
|
1004
|
+
);
|
|
829
1005
|
}
|
|
830
1006
|
}
|
|
831
1007
|
// Rule 20 (WARN): cycle detection — no node should be reachable from itself
|
|
@@ -874,6 +1050,27 @@ var N8nValidator = class {
|
|
|
874
1050
|
}
|
|
875
1051
|
}
|
|
876
1052
|
}
|
|
1053
|
+
// Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
|
|
1054
|
+
checkRule21(w, issues) {
|
|
1055
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1056
|
+
const webhooksNeedingResponse = w.nodes.filter((n) => {
|
|
1057
|
+
if (!n.type.includes("webhook")) return false;
|
|
1058
|
+
const params = n.parameters;
|
|
1059
|
+
return params?.responseMode === "responseNode";
|
|
1060
|
+
});
|
|
1061
|
+
if (webhooksNeedingResponse.length === 0) return;
|
|
1062
|
+
const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
|
|
1063
|
+
if (!hasRespondNode) {
|
|
1064
|
+
for (const wh of webhooksNeedingResponse) {
|
|
1065
|
+
this.warn(
|
|
1066
|
+
issues,
|
|
1067
|
+
21,
|
|
1068
|
+
`Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
|
|
1069
|
+
wh.id
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
877
1074
|
// Rule 22 (WARN): check requiredParams from registry
|
|
878
1075
|
checkRule22(w, issues) {
|
|
879
1076
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -982,57 +1179,166 @@ var N8nValidator = class {
|
|
|
982
1179
|
walk(params);
|
|
983
1180
|
return expressions;
|
|
984
1181
|
}
|
|
985
|
-
// Rule
|
|
986
|
-
|
|
1182
|
+
// Rule 27 (WARN): httpRequest URL is a placeholder
|
|
1183
|
+
checkRule27(w, issues) {
|
|
987
1184
|
if (!Array.isArray(w.nodes)) return;
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1185
|
+
const PLACEHOLDER_RE = [
|
|
1186
|
+
/^https?:\/\/example\.com/i,
|
|
1187
|
+
/your[-_]?(api[-_]?)?url/i,
|
|
1188
|
+
/^https?:\/\/$/,
|
|
1189
|
+
/^<.+>$/,
|
|
1190
|
+
/placeholder/i
|
|
1191
|
+
];
|
|
1192
|
+
for (const node of w.nodes) {
|
|
1193
|
+
if (node.type !== "n8n-nodes-base.httpRequest") continue;
|
|
1194
|
+
const params = node.parameters;
|
|
1195
|
+
const url = params?.["url"];
|
|
1196
|
+
if (typeof url !== "string" || url.trim() === "") continue;
|
|
1197
|
+
if (PLACEHOLDER_RE.some((re) => re.test(url.trim()))) {
|
|
997
1198
|
this.warn(
|
|
998
1199
|
issues,
|
|
999
|
-
|
|
1000
|
-
`
|
|
1001
|
-
|
|
1200
|
+
27,
|
|
1201
|
+
`Node "${node.name}" httpRequest URL appears to be a placeholder: "${url}" \u2014 replace with your actual endpoint`,
|
|
1202
|
+
node.id
|
|
1002
1203
|
);
|
|
1003
1204
|
}
|
|
1004
1205
|
}
|
|
1005
1206
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1207
|
+
// Rule 28 (WARN): code node with empty or comment-only code
|
|
1208
|
+
checkRule28(w, issues) {
|
|
1209
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1210
|
+
for (const node of w.nodes) {
|
|
1211
|
+
if (node.type !== "n8n-nodes-base.code") continue;
|
|
1212
|
+
const params = node.parameters;
|
|
1213
|
+
const jsCode = typeof params?.["jsCode"] === "string" ? params["jsCode"] : "";
|
|
1214
|
+
const pythonCode = typeof params?.["pythonCode"] === "string" ? params["pythonCode"] : "";
|
|
1215
|
+
const code = jsCode || pythonCode;
|
|
1216
|
+
const stripped = code.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/#[^\n]*/g, "").trim();
|
|
1217
|
+
if (!stripped) {
|
|
1218
|
+
this.warn(issues, 28, `Node "${node.name}" code node has no executable code`, node.id);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1013
1221
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1222
|
+
// Rule 29 (WARN): slack node message operation missing channel
|
|
1223
|
+
checkRule29(w, issues) {
|
|
1224
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1225
|
+
for (const node of w.nodes) {
|
|
1226
|
+
if (node.type !== "n8n-nodes-base.slack") continue;
|
|
1227
|
+
const params = node.parameters;
|
|
1228
|
+
const resource = params?.["resource"];
|
|
1229
|
+
const operation = params?.["operation"];
|
|
1230
|
+
const isMessageOp = resource === "message" || operation === "sendMessage" || operation === "post";
|
|
1231
|
+
if (!isMessageOp) continue;
|
|
1232
|
+
const channel = params?.["channel"] ?? params?.["channelId"];
|
|
1233
|
+
const rlValue = typeof channel === "object" && channel !== null ? channel["value"] : void 0;
|
|
1234
|
+
const isEmpty = channel === void 0 || channel === null || typeof channel === "string" && channel.trim() === "" || typeof channel === "object" && (!rlValue || typeof rlValue === "string" && rlValue.trim() === "");
|
|
1235
|
+
if (isEmpty) {
|
|
1236
|
+
this.warn(issues, 29, `Node "${node.name}" Slack message has no channel specified`, node.id);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1021
1239
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1240
|
+
// Rule 30 (WARN): gmail node send operation missing recipient
|
|
1241
|
+
checkRule30(w, issues) {
|
|
1242
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1243
|
+
for (const node of w.nodes) {
|
|
1244
|
+
if (node.type !== "n8n-nodes-base.gmail") continue;
|
|
1245
|
+
const params = node.parameters;
|
|
1246
|
+
const operation = params?.["operation"];
|
|
1247
|
+
if (operation !== "send") continue;
|
|
1248
|
+
const to = params?.["to"] ?? params?.["toList"];
|
|
1249
|
+
const isEmpty = to === void 0 || to === null || typeof to === "string" && to.trim() === "" || Array.isArray(to) && to.length === 0;
|
|
1250
|
+
if (isEmpty) {
|
|
1251
|
+
this.warn(issues, 30, `Node "${node.name}" gmail send has no recipient (to) specified`, node.id);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
// Rule 31 (WARN): if node with empty conditions
|
|
1256
|
+
checkRule31(w, issues) {
|
|
1257
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1258
|
+
for (const node of w.nodes) {
|
|
1259
|
+
if (node.type !== "n8n-nodes-base.if") continue;
|
|
1260
|
+
const params = node.parameters;
|
|
1261
|
+
const conditions = params?.["conditions"];
|
|
1262
|
+
if (conditions === void 0 || conditions === null) {
|
|
1263
|
+
this.warn(issues, 31, `Node "${node.name}" if node has no conditions defined`, node.id);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (typeof conditions === "object" && !Array.isArray(conditions)) {
|
|
1267
|
+
const conds = conditions["conditions"];
|
|
1268
|
+
if (!Array.isArray(conds) || conds.length === 0) {
|
|
1269
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1270
|
+
}
|
|
1271
|
+
} else if (Array.isArray(conditions) && conditions.length === 0) {
|
|
1272
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
// Rule 32 (WARN): set node with no assignments
|
|
1277
|
+
checkRule32(w, issues) {
|
|
1278
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1279
|
+
for (const node of w.nodes) {
|
|
1280
|
+
if (node.type !== "n8n-nodes-base.set") continue;
|
|
1281
|
+
const params = node.parameters;
|
|
1282
|
+
const assignmentsObj = params?.["assignments"];
|
|
1283
|
+
const assignmentsArr = assignmentsObj?.["assignments"];
|
|
1284
|
+
const valuesObj = params?.["values"];
|
|
1285
|
+
const hasV1 = valuesObj && Object.values(valuesObj).some((v) => Array.isArray(v) && v.length > 0);
|
|
1286
|
+
const hasV3 = Array.isArray(assignmentsArr) && assignmentsArr.length > 0;
|
|
1287
|
+
if (!hasV1 && !hasV3) {
|
|
1288
|
+
this.warn(
|
|
1289
|
+
issues,
|
|
1290
|
+
32,
|
|
1291
|
+
`Node "${node.name}" set node has no fields defined \u2014 it will pass data through unchanged`,
|
|
1292
|
+
node.id
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
// Rule 33 (WARN): scheduleTrigger with no schedule rules
|
|
1298
|
+
checkRule33(w, issues) {
|
|
1299
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1300
|
+
for (const node of w.nodes) {
|
|
1301
|
+
if (node.type !== "n8n-nodes-base.scheduleTrigger") continue;
|
|
1302
|
+
const params = node.parameters;
|
|
1303
|
+
const rule = params?.["rule"];
|
|
1304
|
+
const intervals = rule?.["interval"];
|
|
1305
|
+
if (!Array.isArray(intervals) || intervals.length === 0) {
|
|
1306
|
+
this.warn(issues, 33, `Node "${node.name}" scheduleTrigger has no schedule rules defined`, node.id);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
// Rule 34 (WARN): webhook path contains spaces, starts with slash, or looks like a full URL
|
|
1311
|
+
checkRule34(w, issues) {
|
|
1312
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1313
|
+
for (const node of w.nodes) {
|
|
1314
|
+
if (node.type !== "n8n-nodes-base.webhook") continue;
|
|
1315
|
+
const params = node.parameters;
|
|
1316
|
+
const path = params?.["path"];
|
|
1317
|
+
if (typeof path !== "string") continue;
|
|
1318
|
+
if (/\s/.test(path)) {
|
|
1319
|
+
this.warn(
|
|
1320
|
+
issues,
|
|
1321
|
+
34,
|
|
1322
|
+
`Node "${node.name}" webhook path contains spaces: "${path}" \u2014 use hyphens or underscores instead`,
|
|
1323
|
+
node.id
|
|
1324
|
+
);
|
|
1325
|
+
} else if (/^https?:\/\//i.test(path)) {
|
|
1326
|
+
this.warn(
|
|
1327
|
+
issues,
|
|
1328
|
+
34,
|
|
1329
|
+
`Node "${node.name}" webhook path looks like a full URL \u2014 it should be a relative path (e.g. "my-hook")`,
|
|
1330
|
+
node.id
|
|
1331
|
+
);
|
|
1332
|
+
} else if (path.startsWith("/")) {
|
|
1333
|
+
this.warn(
|
|
1334
|
+
issues,
|
|
1335
|
+
34,
|
|
1336
|
+
`Node "${node.name}" webhook path starts with "/" \u2014 n8n adds the leading slash automatically`,
|
|
1337
|
+
node.id
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1032
1341
|
}
|
|
1033
|
-
issues;
|
|
1034
|
-
attemptMetadata;
|
|
1035
|
-
warnedRules;
|
|
1036
1342
|
};
|
|
1037
1343
|
|
|
1038
1344
|
// src/generation/prompt-builder.ts
|
|
@@ -1239,6 +1545,14 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
|
|
|
1239
1545
|
8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
|
|
1240
1546
|
9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
|
|
1241
1547
|
10. No bare $('NodeName').json \u2014 always use .first().json.field or .all()
|
|
1548
|
+
11. httpRequest URL is a real endpoint (not "example.com" or "YOUR_URL")
|
|
1549
|
+
12. code nodes contain actual logic \u2014 not empty or comment-only
|
|
1550
|
+
13. Slack message nodes have a channel specified (channelId or channel)
|
|
1551
|
+
14. Gmail send nodes have a recipient (to field non-empty)
|
|
1552
|
+
15. if nodes have at least one condition in conditions.conditions[]
|
|
1553
|
+
16. set nodes have at least one entry in assignments.assignments[]
|
|
1554
|
+
17. scheduleTrigger has at least one rule in rule.interval[]
|
|
1555
|
+
18. webhook path is relative (no spaces, no leading slash, no http://)
|
|
1242
1556
|
|
|
1243
1557
|
---
|
|
1244
1558
|
|
|
@@ -1255,7 +1569,7 @@ function scoreToMode(score) {
|
|
|
1255
1569
|
}
|
|
1256
1570
|
|
|
1257
1571
|
// src/validation/rule-metadata.ts
|
|
1258
|
-
var VALIDATOR_RULE_IDS = Array.from({ length:
|
|
1572
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 34 }, (_, i) => i + 1);
|
|
1259
1573
|
var RULE_PIPELINE_STAGES = {
|
|
1260
1574
|
1: "node_generation",
|
|
1261
1575
|
2: "node_generation",
|
|
@@ -1282,7 +1596,15 @@ var RULE_PIPELINE_STAGES = {
|
|
|
1282
1596
|
23: "node_generation",
|
|
1283
1597
|
24: "expression_syntax",
|
|
1284
1598
|
25: "expression_syntax",
|
|
1285
|
-
26: "expression_syntax"
|
|
1599
|
+
26: "expression_syntax",
|
|
1600
|
+
27: "node_generation",
|
|
1601
|
+
28: "node_generation",
|
|
1602
|
+
29: "node_generation",
|
|
1603
|
+
30: "node_generation",
|
|
1604
|
+
31: "node_generation",
|
|
1605
|
+
32: "node_generation",
|
|
1606
|
+
33: "node_generation",
|
|
1607
|
+
34: "node_generation"
|
|
1286
1608
|
};
|
|
1287
1609
|
var RULE_EXAMPLES = {
|
|
1288
1610
|
17: {
|
|
@@ -1300,6 +1622,38 @@ var RULE_EXAMPLES = {
|
|
|
1300
1622
|
26: {
|
|
1301
1623
|
bad: "$('Fetch Data').json.email",
|
|
1302
1624
|
good: "$('Fetch Data').first().json.email"
|
|
1625
|
+
},
|
|
1626
|
+
27: {
|
|
1627
|
+
bad: '"url": "https://example.com/api/data"',
|
|
1628
|
+
good: '"url": "https://api.yourservice.com/v1/endpoint"'
|
|
1629
|
+
},
|
|
1630
|
+
28: {
|
|
1631
|
+
bad: '"jsCode": "// TODO: implement this"',
|
|
1632
|
+
good: '"jsCode": "return items.map(item => ({ json: { result: item.json.value * 2 } }))"'
|
|
1633
|
+
},
|
|
1634
|
+
29: {
|
|
1635
|
+
bad: '"channelId": ""',
|
|
1636
|
+
good: '"channelId": { "__rl": true, "value": "C0123456789", "mode": "id" }'
|
|
1637
|
+
},
|
|
1638
|
+
30: {
|
|
1639
|
+
bad: '"operation": "send", "to": ""',
|
|
1640
|
+
good: '"operation": "send", "to": "recipient@example.com"'
|
|
1641
|
+
},
|
|
1642
|
+
31: {
|
|
1643
|
+
bad: '"conditions": { "combinator": "and", "conditions": [] }',
|
|
1644
|
+
good: '"conditions": { "combinator": "and", "conditions": [{ "leftValue": "={{ $json.status }}", "rightValue": "active", "operator": { "type": "string", "operation": "equals" } }] }'
|
|
1645
|
+
},
|
|
1646
|
+
32: {
|
|
1647
|
+
bad: '"assignments": { "assignments": [] }',
|
|
1648
|
+
good: '"assignments": { "assignments": [{ "id": "f1", "name": "status", "value": "processed", "type": "string" }] }'
|
|
1649
|
+
},
|
|
1650
|
+
33: {
|
|
1651
|
+
bad: '"rule": { "interval": [] }',
|
|
1652
|
+
good: '"rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] }'
|
|
1653
|
+
},
|
|
1654
|
+
34: {
|
|
1655
|
+
bad: '"path": "/my webhook"',
|
|
1656
|
+
good: '"path": "my-webhook"'
|
|
1303
1657
|
}
|
|
1304
1658
|
};
|
|
1305
1659
|
var RULE_MITIGATIONS = {
|
|
@@ -1328,7 +1682,15 @@ var RULE_MITIGATIONS = {
|
|
|
1328
1682
|
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
1329
1683
|
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
1330
1684
|
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
1331
|
-
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
|
|
1685
|
+
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime',
|
|
1686
|
+
27: 'Replace placeholder URLs with your actual API endpoint \u2014 do not use "example.com" or "YOUR_URL" patterns',
|
|
1687
|
+
28: "Add executable code to the code node \u2014 empty or comment-only code nodes do nothing at runtime",
|
|
1688
|
+
29: "Set the channel parameter for Slack message operations (channelId with __rl object, or channel as string)",
|
|
1689
|
+
30: "Set the to parameter for Gmail send operations with at least one recipient email address",
|
|
1690
|
+
31: "Add at least one condition to the if node \u2014 conditions.conditions array must be non-empty",
|
|
1691
|
+
32: "Add field assignments to the set node \u2014 assignments.assignments array must be non-empty for typeVersion 3.x",
|
|
1692
|
+
33: "Add at least one schedule rule to scheduleTrigger \u2014 rule.interval array must have at least one entry",
|
|
1693
|
+
34: 'Webhook path must be a relative path without spaces, leading slashes, or protocol prefixes (e.g. "my-hook")'
|
|
1332
1694
|
};
|
|
1333
1695
|
|
|
1334
1696
|
// src/generation/prompt-builder.ts
|
|
@@ -1360,18 +1722,37 @@ var PromptBuilder = class {
|
|
|
1360
1722
|
}
|
|
1361
1723
|
build(request, matches, globalFailureRates = [], dynamicCatalog) {
|
|
1362
1724
|
const mode = this.resolveMode(matches);
|
|
1363
|
-
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
|
|
1725
|
+
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog, request.description);
|
|
1364
1726
|
const userMessage = this.buildUserMessage(request, matches, mode);
|
|
1365
1727
|
return { system, userMessage, mode, matches };
|
|
1366
1728
|
}
|
|
1367
|
-
buildCorrectionMessage(request, matches, allIssues, attempt) {
|
|
1729
|
+
buildCorrectionMessage(request, matches, allIssues, attempt, failingRuleIds) {
|
|
1368
1730
|
const base = this.buildUserMessage(request, matches, this.resolveMode(matches));
|
|
1731
|
+
let examplesSection = "";
|
|
1732
|
+
if (failingRuleIds && failingRuleIds.length > 0) {
|
|
1733
|
+
const uniqueRules = [...new Set(failingRuleIds)];
|
|
1734
|
+
const exampleLines = [];
|
|
1735
|
+
for (const rule of uniqueRules) {
|
|
1736
|
+
const ex = RULE_EXAMPLES[rule];
|
|
1737
|
+
if (ex) {
|
|
1738
|
+
exampleLines.push(`Rule ${rule}:
|
|
1739
|
+
Bad: ${ex.bad}
|
|
1740
|
+
Good: ${ex.good}`);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
if (exampleLines.length > 0) {
|
|
1744
|
+
examplesSection = `
|
|
1745
|
+
|
|
1746
|
+
## Concrete Fix Examples
|
|
1747
|
+
${exampleLines.join("\n\n")}`;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1369
1750
|
return `${base}
|
|
1370
1751
|
|
|
1371
1752
|
IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
|
|
1372
1753
|
${allIssues.join("\n")}
|
|
1373
1754
|
|
|
1374
|
-
Fix ALL of the above issues in your new response. Do not repeat any of these mistakes
|
|
1755
|
+
Fix ALL of the above issues in your new response. Do not repeat any of these mistakes.${examplesSection}`;
|
|
1375
1756
|
}
|
|
1376
1757
|
resolveMode(matches) {
|
|
1377
1758
|
if (matches.length === 0) return "scratch";
|
|
@@ -1379,7 +1760,7 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1379
1760
|
if (!top) return "scratch";
|
|
1380
1761
|
return scoreToMode(top.score);
|
|
1381
1762
|
}
|
|
1382
|
-
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog) {
|
|
1763
|
+
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog, description) {
|
|
1383
1764
|
let basePrompt = SYSTEM_PROMPT_V1;
|
|
1384
1765
|
if (dynamicCatalog) {
|
|
1385
1766
|
basePrompt = basePrompt.replace(
|
|
@@ -1439,7 +1820,7 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1439
1820
|
});
|
|
1440
1821
|
}
|
|
1441
1822
|
}
|
|
1442
|
-
const warnings = this.buildFailureWarnings(matches, globalFailureRates);
|
|
1823
|
+
const warnings = this.buildFailureWarnings(matches, globalFailureRates, description);
|
|
1443
1824
|
if (warnings) {
|
|
1444
1825
|
blocks.push({ type: "text", text: warnings });
|
|
1445
1826
|
}
|
|
@@ -1466,15 +1847,34 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1466
1847
|
const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
|
|
1467
1848
|
return patterns.map((p) => p.rule);
|
|
1468
1849
|
}
|
|
1469
|
-
getActivePatterns(maxCount = 10) {
|
|
1850
|
+
getActivePatterns(maxCount = 10, description) {
|
|
1470
1851
|
const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
|
|
1471
1852
|
const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1472
1853
|
const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1473
1854
|
const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1855
|
+
const ordered = [...regressed, ...confirmed, ...drafts];
|
|
1856
|
+
if (this.profile === "minimal" && description) {
|
|
1857
|
+
return this.rankByRelevance(ordered, description).slice(0, maxCount);
|
|
1858
|
+
}
|
|
1859
|
+
return ordered.slice(0, maxCount);
|
|
1860
|
+
}
|
|
1861
|
+
rankByRelevance(patterns, description) {
|
|
1862
|
+
const lower = description.toLowerCase();
|
|
1863
|
+
const STAGE_KEYWORDS = {
|
|
1864
|
+
credential_injection: ["credential", "auth", "api key", "token", "oauth", "smtp", "imap", "password", "secret"],
|
|
1865
|
+
connection_wiring: ["connect", "link", "wire", "chain", "merge", "branch", "join"],
|
|
1866
|
+
expression_syntax: ["expression", "variable", "json", "field", "data", "$json", "item"],
|
|
1867
|
+
workflow_structure: ["trigger", "webhook", "schedule", "structure", "workflow"],
|
|
1868
|
+
node_generation: ["node", "generate", "create", "build", "send", "fetch", "email", "slack", "http"]
|
|
1869
|
+
};
|
|
1870
|
+
return patterns.map((p) => {
|
|
1871
|
+
const keywords = STAGE_KEYWORDS[p.pipelineStage] ?? [];
|
|
1872
|
+
const relevanceBoost = keywords.some((kw) => lower.includes(kw)) ? 1 : 0;
|
|
1873
|
+
return { pattern: p, sort: relevanceBoost * 10 + p.compositeScore };
|
|
1874
|
+
}).sort((a, b) => b.sort - a.sort).map((x) => x.pattern);
|
|
1875
|
+
}
|
|
1876
|
+
buildFailureWarnings(matches, globalFailureRates, description) {
|
|
1877
|
+
const richPatterns = this.getActivePatterns(this.resolveMaxPatterns(), description);
|
|
1478
1878
|
this._lastActivePatterns = richPatterns;
|
|
1479
1879
|
if (richPatterns.length > 0) {
|
|
1480
1880
|
return this.buildStageGroupedWarnings(richPatterns, matches);
|
|
@@ -1654,7 +2054,8 @@ var WorkflowDesigner = class {
|
|
|
1654
2054
|
const issueLines = lastErrors.map(
|
|
1655
2055
|
(i) => `- [Rule ${i.rule}] ${i.message}${i.nodeId ? ` (node: ${i.nodeId})` : ""}`
|
|
1656
2056
|
);
|
|
1657
|
-
|
|
2057
|
+
const failingRuleIds = lastErrors.map((i) => i.rule);
|
|
2058
|
+
userMessage = this.promptBuilder.buildCorrectionMessage(request, matches, issueLines, attempt - 1, failingRuleIds);
|
|
1658
2059
|
this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: lastErrors.length });
|
|
1659
2060
|
}
|
|
1660
2061
|
const start = Date.now();
|
|
@@ -1840,19 +2241,20 @@ var TelemetryReader = class {
|
|
|
1840
2241
|
}
|
|
1841
2242
|
const events = await this.readRecentEvents(days);
|
|
1842
2243
|
const buildSessions = new Set(
|
|
1843
|
-
events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
|
|
2244
|
+
events.filter((e) => e.eventType === "build_complete").map((e) => e.runId ?? e.sessionId)
|
|
1844
2245
|
);
|
|
1845
2246
|
const MIN_BUILDS_FOR_RATES = 3;
|
|
1846
2247
|
if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
|
|
1847
2248
|
const ruleSessions = /* @__PURE__ */ new Map();
|
|
1848
2249
|
for (const event of events) {
|
|
1849
2250
|
if (event.eventType !== "generation_attempt") continue;
|
|
1850
|
-
|
|
2251
|
+
const eventKey = event.runId ?? event.sessionId;
|
|
2252
|
+
if (!buildSessions.has(eventKey)) continue;
|
|
1851
2253
|
const data = event.data;
|
|
1852
2254
|
if (data.validationPassed || !data.issues) continue;
|
|
1853
2255
|
for (const issue of data.issues) {
|
|
1854
2256
|
const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
|
|
1855
|
-
entry.sessions.add(
|
|
2257
|
+
entry.sessions.add(eventKey);
|
|
1856
2258
|
entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
|
|
1857
2259
|
ruleSessions.set(issue.rule, entry);
|
|
1858
2260
|
}
|
|
@@ -1894,22 +2296,24 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1894
2296
|
telemetryDir;
|
|
1895
2297
|
outputDir;
|
|
1896
2298
|
_cachedEvents = null;
|
|
2299
|
+
_cachedPreviousPatterns = null;
|
|
1897
2300
|
constructor(telemetryDir) {
|
|
1898
2301
|
const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
|
|
1899
2302
|
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
1900
2303
|
this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
|
|
1901
2304
|
}
|
|
1902
2305
|
async loadPreviousPatterns() {
|
|
2306
|
+
if (this._cachedPreviousPatterns !== null) return this._cachedPreviousPatterns;
|
|
1903
2307
|
try {
|
|
1904
2308
|
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
|
|
1905
2309
|
const prev = JSON.parse(raw);
|
|
1906
2310
|
const version = prev.schemaVersion ?? 0;
|
|
1907
2311
|
const patterns = prev.topFailureRules ?? [];
|
|
1908
|
-
|
|
1909
|
-
return this.migratePatterns(patterns, version);
|
|
2312
|
+
this._cachedPreviousPatterns = version === PATTERN_SCHEMA_VERSION ? patterns : this.migratePatterns(patterns, version);
|
|
1910
2313
|
} catch {
|
|
1911
|
-
|
|
2314
|
+
this._cachedPreviousPatterns = [];
|
|
1912
2315
|
}
|
|
2316
|
+
return this._cachedPreviousPatterns;
|
|
1913
2317
|
}
|
|
1914
2318
|
migratePatterns(patterns, fromVersion) {
|
|
1915
2319
|
let migrated = patterns;
|
|
@@ -1941,7 +2345,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1941
2345
|
this._cachedEvents = events;
|
|
1942
2346
|
const starts = events.filter((e) => e.eventType === "build_start");
|
|
1943
2347
|
const attempts = events.filter((e) => e.eventType === "generation_attempt");
|
|
1944
|
-
const
|
|
2348
|
+
const _passed = attempts.filter(
|
|
1945
2349
|
(a) => a.data.validationPassed === true
|
|
1946
2350
|
);
|
|
1947
2351
|
const failed = attempts.filter(
|
|
@@ -2209,6 +2613,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2209
2613
|
const tmpPath = `${outputPath}.tmp`;
|
|
2210
2614
|
await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
2211
2615
|
await (0, import_promises3.rename)(tmpPath, outputPath);
|
|
2616
|
+
this._cachedPreviousPatterns = null;
|
|
2212
2617
|
const historySummary = {
|
|
2213
2618
|
timestamp: analysis.generatedAt,
|
|
2214
2619
|
totalBuilds: analysis.summary.totalBuilds,
|
|
@@ -2257,7 +2662,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2257
2662
|
})
|
|
2258
2663
|
));
|
|
2259
2664
|
return {
|
|
2260
|
-
sessionId: bc.sessionId,
|
|
2665
|
+
sessionId: bc.runId ?? bc.sessionId,
|
|
2261
2666
|
date: bc.fileDate,
|
|
2262
2667
|
description: data.description ?? "",
|
|
2263
2668
|
workflowType: data.workflowType ?? null,
|
|
@@ -2290,7 +2695,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2290
2695
|
alerts.push({
|
|
2291
2696
|
type: "stale_pattern",
|
|
2292
2697
|
rule: p.rule,
|
|
2293
|
-
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-
|
|
2698
|
+
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-34)`
|
|
2294
2699
|
});
|
|
2295
2700
|
}
|
|
2296
2701
|
}
|
|
@@ -2430,11 +2835,10 @@ function inferWorkflowType(description) {
|
|
|
2430
2835
|
// src/client.ts
|
|
2431
2836
|
var import_node_os5 = require("os");
|
|
2432
2837
|
var import_node_path6 = require("path");
|
|
2433
|
-
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
2838
|
+
var DEFAULT_MODEL = process.env["KAIROS_MODEL"] ?? "claude-sonnet-4-6";
|
|
2434
2839
|
var Kairos = class {
|
|
2435
2840
|
provider;
|
|
2436
2841
|
designer;
|
|
2437
|
-
validator;
|
|
2438
2842
|
library;
|
|
2439
2843
|
logger;
|
|
2440
2844
|
telemetry;
|
|
@@ -2460,7 +2864,6 @@ var Kairos = class {
|
|
|
2460
2864
|
const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
|
|
2461
2865
|
const patternsPath = typeof options.telemetry === "string" ? (0, import_node_path6.join)(options.telemetry, "..", "patterns.json") : (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".kairos", "patterns.json");
|
|
2462
2866
|
this.designer = new WorkflowDesigner(anthropic, this.model, logger, patternsPath);
|
|
2463
|
-
this.validator = new N8nValidator();
|
|
2464
2867
|
this.library = options.library ?? new NullLibrary();
|
|
2465
2868
|
this.logger = logger;
|
|
2466
2869
|
if (options.telemetry === true) {
|
|
@@ -2557,7 +2960,6 @@ var Kairos = class {
|
|
|
2557
2960
|
}
|
|
2558
2961
|
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
2559
2962
|
const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
|
|
2560
|
-
this.saveToLibrary(workflow, description, designResult, matches);
|
|
2561
2963
|
if (options?.dryRun) {
|
|
2562
2964
|
const totalTokensInput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
2563
2965
|
const totalTokensOutput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
@@ -2588,10 +2990,20 @@ var Kairos = class {
|
|
|
2588
2990
|
}
|
|
2589
2991
|
const provider = this.requireProvider();
|
|
2590
2992
|
const deployed = await provider.deploy(workflow);
|
|
2591
|
-
this.
|
|
2993
|
+
this.logger.info("Workflow deployed to n8n", { workflowId: deployed.workflowId, name: deployed.name });
|
|
2994
|
+
this.recordDeploy(deployed.workflowId);
|
|
2592
2995
|
if (options?.activate) {
|
|
2593
2996
|
await provider.activate(deployed.workflowId);
|
|
2594
2997
|
}
|
|
2998
|
+
this.saveToLibrary(workflow, description, designResult, matches, deployed.workflowId);
|
|
2999
|
+
let smokeTestResult;
|
|
3000
|
+
if (options?.smokeTest) {
|
|
3001
|
+
smokeTestResult = await provider.smokeTest(deployed.workflowId, workflow).catch((err) => {
|
|
3002
|
+
this.logger.warn("Smoke test threw unexpectedly", { err: String(err) });
|
|
3003
|
+
return { status: "error", triggerType: "manual", error: String(err) };
|
|
3004
|
+
});
|
|
3005
|
+
this.logger.info("Smoke test complete", { status: smokeTestResult.status, triggerType: smokeTestResult.triggerType });
|
|
3006
|
+
}
|
|
2595
3007
|
const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
2596
3008
|
const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
2597
3009
|
await this.telemetry?.emit("build_complete", {
|
|
@@ -2616,7 +3028,8 @@ var Kairos = class {
|
|
|
2616
3028
|
credentialsNeeded: designResult.credentialsNeeded,
|
|
2617
3029
|
activationRequired: !options?.activate,
|
|
2618
3030
|
generationAttempts: designResult.attempts,
|
|
2619
|
-
dryRun: false
|
|
3031
|
+
dryRun: false,
|
|
3032
|
+
...smokeTestResult !== void 0 ? { smokeTest: smokeTestResult } : {}
|
|
2620
3033
|
};
|
|
2621
3034
|
}
|
|
2622
3035
|
async replace(id, description) {
|
|
@@ -2673,7 +3086,8 @@ var Kairos = class {
|
|
|
2673
3086
|
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
2674
3087
|
const provider = this.requireProvider();
|
|
2675
3088
|
const deployed = await provider.update(id, designResult.workflow);
|
|
2676
|
-
this.
|
|
3089
|
+
this.logger.info("Workflow updated in n8n", { workflowId: deployed.workflowId, name: deployed.name });
|
|
3090
|
+
this.saveToLibrary(designResult.workflow, description, designResult, matches, deployed.workflowId);
|
|
2677
3091
|
this.recordDeploy();
|
|
2678
3092
|
const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
2679
3093
|
const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
@@ -2729,10 +3143,10 @@ var Kairos = class {
|
|
|
2729
3143
|
}, runId);
|
|
2730
3144
|
}
|
|
2731
3145
|
}
|
|
2732
|
-
recordDeploy() {
|
|
3146
|
+
recordDeploy(n8nWorkflowId) {
|
|
2733
3147
|
this.saveQueue = this.saveQueue.then(async (savedId) => {
|
|
2734
3148
|
if (savedId) {
|
|
2735
|
-
await this.library.recordDeployment(savedId);
|
|
3149
|
+
await this.library.recordDeployment(savedId, n8nWorkflowId);
|
|
2736
3150
|
}
|
|
2737
3151
|
return savedId;
|
|
2738
3152
|
}).catch((err) => {
|
|
@@ -2740,7 +3154,7 @@ var Kairos = class {
|
|
|
2740
3154
|
return null;
|
|
2741
3155
|
});
|
|
2742
3156
|
}
|
|
2743
|
-
saveToLibrary(workflow, description, designResult, matches) {
|
|
3157
|
+
saveToLibrary(workflow, description, designResult, matches, n8nWorkflowId) {
|
|
2744
3158
|
const failedAttempts = designResult.attemptMetadata.filter((m) => !m.validationPassed);
|
|
2745
3159
|
const failurePatterns = failedAttempts.flatMap(
|
|
2746
3160
|
(m) => m.issues.map((i) => ({ rule: i.rule, message: i.message }))
|
|
@@ -2766,6 +3180,7 @@ var Kairos = class {
|
|
|
2766
3180
|
if (matches.length > 0) metadata.sourceWorkflowIds = matches.map((m) => m.workflow.id);
|
|
2767
3181
|
if (topMatch) metadata.topMatchScore = topMatch.score;
|
|
2768
3182
|
if (designResult.credentialsNeeded.length > 0) metadata.credentialsNeeded = designResult.credentialsNeeded;
|
|
3183
|
+
if (n8nWorkflowId) metadata.n8nWorkflowId = n8nWorkflowId;
|
|
2769
3184
|
const firstTryPass = designResult.attemptMetadata.length > 0 && designResult.attemptMetadata[0].validationPassed;
|
|
2770
3185
|
const failedRules = Array.from(new Set(
|
|
2771
3186
|
designResult.attemptMetadata.filter((m) => !m.validationPassed).flatMap((m) => m.issues.map((i) => i.rule))
|
|
@@ -2829,12 +3244,32 @@ var import_node_path7 = require("path");
|
|
|
2829
3244
|
var import_node_os6 = require("os");
|
|
2830
3245
|
|
|
2831
3246
|
// src/library/scorer.ts
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
3247
|
+
function loadWeights() {
|
|
3248
|
+
const raw = {
|
|
3249
|
+
tfidf: parseFloat(process.env["KAIROS_WEIGHT_TFIDF"] ?? ""),
|
|
3250
|
+
nodeFingerprint: parseFloat(process.env["KAIROS_WEIGHT_JACCARD"] ?? ""),
|
|
3251
|
+
outcome: parseFloat(process.env["KAIROS_WEIGHT_OUTCOME"] ?? ""),
|
|
3252
|
+
deploy: parseFloat(process.env["KAIROS_WEIGHT_DEPLOY"] ?? "")
|
|
3253
|
+
};
|
|
3254
|
+
const defaults = { tfidf: 0.35, nodeFingerprint: 0.3, outcome: 0.2, deploy: 0.15 };
|
|
3255
|
+
const anySet = Object.values(raw).some((v) => !isNaN(v) && v >= 0);
|
|
3256
|
+
if (!anySet) return defaults;
|
|
3257
|
+
const w = {
|
|
3258
|
+
tfidf: !isNaN(raw.tfidf) && raw.tfidf >= 0 ? raw.tfidf : defaults.tfidf,
|
|
3259
|
+
nodeFingerprint: !isNaN(raw.nodeFingerprint) && raw.nodeFingerprint >= 0 ? raw.nodeFingerprint : defaults.nodeFingerprint,
|
|
3260
|
+
outcome: !isNaN(raw.outcome) && raw.outcome >= 0 ? raw.outcome : defaults.outcome,
|
|
3261
|
+
deploy: !isNaN(raw.deploy) && raw.deploy >= 0 ? raw.deploy : defaults.deploy
|
|
3262
|
+
};
|
|
3263
|
+
const total = w.tfidf + w.nodeFingerprint + w.outcome + w.deploy;
|
|
3264
|
+
if (total <= 0) return defaults;
|
|
3265
|
+
return {
|
|
3266
|
+
tfidf: w.tfidf / total,
|
|
3267
|
+
nodeFingerprint: w.nodeFingerprint / total,
|
|
3268
|
+
outcome: w.outcome / total,
|
|
3269
|
+
deploy: w.deploy / total
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
var WEIGHTS = loadWeights();
|
|
2838
3273
|
var NODE_KEYWORDS = {
|
|
2839
3274
|
slack: ["slack", "slackApi"],
|
|
2840
3275
|
email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
|
|
@@ -3019,6 +3454,8 @@ function clusterWorkflows(workflows) {
|
|
|
3019
3454
|
}
|
|
3020
3455
|
return clusters.sort((a, b) => b.members.length - a.members.length);
|
|
3021
3456
|
}
|
|
3457
|
+
var NOVELTY_BOOST = 0.05;
|
|
3458
|
+
var NOVELTY_PENALTY = 0.03;
|
|
3022
3459
|
function rerank(candidates, clusters) {
|
|
3023
3460
|
const clusterMap = /* @__PURE__ */ new Map();
|
|
3024
3461
|
for (const cluster of clusters) {
|
|
@@ -3026,7 +3463,7 @@ function rerank(candidates, clusters) {
|
|
|
3026
3463
|
clusterMap.set(member.id, cluster);
|
|
3027
3464
|
}
|
|
3028
3465
|
}
|
|
3029
|
-
|
|
3466
|
+
const pass1 = candidates.map((c) => {
|
|
3030
3467
|
const cluster = clusterMap.get(c.workflow.id);
|
|
3031
3468
|
let boost = 0;
|
|
3032
3469
|
if (cluster && cluster.avgFirstTryPassRate > 0) {
|
|
@@ -3038,7 +3475,25 @@ function rerank(candidates, clusters) {
|
|
|
3038
3475
|
return {
|
|
3039
3476
|
workflow: c.workflow,
|
|
3040
3477
|
score: Math.max(0, Math.min(1, c.score + boost)),
|
|
3041
|
-
|
|
3478
|
+
cluster
|
|
3479
|
+
};
|
|
3480
|
+
}).sort((a, b) => b.score - a.score);
|
|
3481
|
+
const seenFingerprints = /* @__PURE__ */ new Set();
|
|
3482
|
+
return pass1.map((c) => {
|
|
3483
|
+
const fpKey = c.cluster ? fingerprintKey(c.cluster.fingerprint) : null;
|
|
3484
|
+
let noveltyAdjust = 0;
|
|
3485
|
+
if (fpKey !== null) {
|
|
3486
|
+
if (!seenFingerprints.has(fpKey)) {
|
|
3487
|
+
seenFingerprints.add(fpKey);
|
|
3488
|
+
noveltyAdjust = NOVELTY_BOOST;
|
|
3489
|
+
} else {
|
|
3490
|
+
noveltyAdjust = -NOVELTY_PENALTY;
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
return {
|
|
3494
|
+
workflow: c.workflow,
|
|
3495
|
+
score: Math.max(0, Math.min(1, c.score + noveltyAdjust)),
|
|
3496
|
+
...c.cluster ? { clusterPattern: c.cluster.pattern } : {}
|
|
3042
3497
|
};
|
|
3043
3498
|
}).sort((a, b) => b.score - a.score);
|
|
3044
3499
|
}
|
|
@@ -3055,7 +3510,11 @@ function buildSearchCorpus(w) {
|
|
|
3055
3510
|
});
|
|
3056
3511
|
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
3057
3512
|
}
|
|
3058
|
-
var
|
|
3513
|
+
var _rawSize = parseInt(process.env["KAIROS_LIBRARY_SIZE"] ?? "500", 10);
|
|
3514
|
+
var MAX_LIBRARY_SIZE = Number.isFinite(_rawSize) && _rawSize >= 10 ? _rawSize : 500;
|
|
3515
|
+
function evictionScore(m) {
|
|
3516
|
+
return (m.deployCount ?? 0) * 3 + (m.timesRetrieved ?? 0) + (m.outcomeStats?.totalUses ?? 0);
|
|
3517
|
+
}
|
|
3059
3518
|
function isValidMeta(item) {
|
|
3060
3519
|
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
3061
3520
|
}
|
|
@@ -3103,6 +3562,7 @@ var FileLibrary = class {
|
|
|
3103
3562
|
} catch {
|
|
3104
3563
|
this.meta = [];
|
|
3105
3564
|
}
|
|
3565
|
+
await this.scanForOrphansAndCleanup();
|
|
3106
3566
|
} else {
|
|
3107
3567
|
try {
|
|
3108
3568
|
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
@@ -3117,6 +3577,31 @@ var FileLibrary = class {
|
|
|
3117
3577
|
await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
|
|
3118
3578
|
}
|
|
3119
3579
|
}
|
|
3580
|
+
async scanForOrphansAndCleanup() {
|
|
3581
|
+
let entries;
|
|
3582
|
+
try {
|
|
3583
|
+
entries = await (0, import_promises4.readdir)(this.workflowsDir);
|
|
3584
|
+
} catch {
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
const indexedIds = new Set(this.meta.map((m) => m.id));
|
|
3588
|
+
const orphanIds = [];
|
|
3589
|
+
for (const filename of entries) {
|
|
3590
|
+
if (filename.endsWith(".tmp")) {
|
|
3591
|
+
await (0, import_promises4.unlink)((0, import_node_path7.join)(this.workflowsDir, filename)).catch(() => {
|
|
3592
|
+
});
|
|
3593
|
+
continue;
|
|
3594
|
+
}
|
|
3595
|
+
if (!filename.endsWith(".json")) continue;
|
|
3596
|
+
const id = filename.slice(0, -5);
|
|
3597
|
+
if (!indexedIds.has(id)) {
|
|
3598
|
+
orphanIds.push(id);
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
if (orphanIds.length > 0) {
|
|
3602
|
+
console.warn(`[FileLibrary] Found ${orphanIds.length} orphaned workflow file(s) not in index: ${orphanIds.join(", ")}`);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3120
3605
|
/**
|
|
3121
3606
|
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
3122
3607
|
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
@@ -3187,10 +3672,12 @@ var FileLibrary = class {
|
|
|
3187
3672
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
3188
3673
|
const docCount = shells.length;
|
|
3189
3674
|
const idf = /* @__PURE__ */ new Map();
|
|
3675
|
+
const idfCeiling = Math.log(docCount + 1) + 1;
|
|
3190
3676
|
const allTokens = new Set(queryTokens);
|
|
3191
3677
|
for (const token of allTokens) {
|
|
3192
3678
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
3193
|
-
|
|
3679
|
+
const rawIdf = Math.log((docCount + 1) / (docsWithToken + 1)) + 1;
|
|
3680
|
+
idf.set(token, rawIdf / idfCeiling);
|
|
3194
3681
|
}
|
|
3195
3682
|
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
3196
3683
|
const clusters = clusterWorkflows(shells);
|
|
@@ -3216,6 +3703,27 @@ var FileLibrary = class {
|
|
|
3216
3703
|
return results.filter((r) => r !== null);
|
|
3217
3704
|
}
|
|
3218
3705
|
async save(workflow, metadata) {
|
|
3706
|
+
const existingByN8nId = metadata.n8nWorkflowId ? this.meta.find((m) => m.n8nWorkflowId === metadata.n8nWorkflowId) : void 0;
|
|
3707
|
+
const normalizedDesc = metadata.description.trim().toLowerCase();
|
|
3708
|
+
const existing = existingByN8nId ?? this.meta.find((m) => m.description.trim().toLowerCase() === normalizedDesc);
|
|
3709
|
+
if (existing) {
|
|
3710
|
+
existing.description = metadata.description;
|
|
3711
|
+
existing.workflowName = workflow.name;
|
|
3712
|
+
existing.cachedNodeTypes = workflow.nodes.map((n) => n.type);
|
|
3713
|
+
if (metadata.n8nWorkflowId) existing.n8nWorkflowId = metadata.n8nWorkflowId;
|
|
3714
|
+
if (metadata.generationAttempts != null) {
|
|
3715
|
+
existing.generationAttempts = metadata.generationAttempts;
|
|
3716
|
+
}
|
|
3717
|
+
if (metadata.failurePatterns?.length) {
|
|
3718
|
+
existing.failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
3719
|
+
}
|
|
3720
|
+
if (metadata.tags?.length) {
|
|
3721
|
+
existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...metadata.tags])];
|
|
3722
|
+
}
|
|
3723
|
+
await this.writeWorkflowFile(existing.id, workflow);
|
|
3724
|
+
await this.persist();
|
|
3725
|
+
return existing.id;
|
|
3726
|
+
}
|
|
3219
3727
|
const id = generateUUID();
|
|
3220
3728
|
await this.writeWorkflowFile(id, workflow);
|
|
3221
3729
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
@@ -3237,25 +3745,27 @@ var FileLibrary = class {
|
|
|
3237
3745
|
...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
|
|
3238
3746
|
...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
|
|
3239
3747
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
3240
|
-
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
3748
|
+
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {},
|
|
3749
|
+
...metadata.n8nWorkflowId ? { n8nWorkflowId: metadata.n8nWorkflowId } : {}
|
|
3241
3750
|
};
|
|
3242
3751
|
this.meta.push(meta);
|
|
3243
3752
|
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
3244
3753
|
this.meta.sort((a, b) => {
|
|
3245
3754
|
if (a.id === id) return -1;
|
|
3246
3755
|
if (b.id === id) return 1;
|
|
3247
|
-
return (b
|
|
3756
|
+
return evictionScore(b) - evictionScore(a);
|
|
3248
3757
|
});
|
|
3249
3758
|
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
3250
3759
|
}
|
|
3251
3760
|
await this.persist();
|
|
3252
3761
|
return id;
|
|
3253
3762
|
}
|
|
3254
|
-
async recordDeployment(id) {
|
|
3763
|
+
async recordDeployment(id, n8nWorkflowId) {
|
|
3255
3764
|
const m = this.meta.find((m2) => m2.id === id);
|
|
3256
3765
|
if (m) {
|
|
3257
3766
|
m.deployCount++;
|
|
3258
3767
|
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3768
|
+
if (n8nWorkflowId) m.n8nWorkflowId = n8nWorkflowId;
|
|
3259
3769
|
await this.persist();
|
|
3260
3770
|
}
|
|
3261
3771
|
}
|
|
@@ -3318,37 +3828,98 @@ var FileLibrary = class {
|
|
|
3318
3828
|
}
|
|
3319
3829
|
return [...map.values()];
|
|
3320
3830
|
}
|
|
3831
|
+
// ── Cross-process file locking ────────────────────────────────────────────
|
|
3832
|
+
// Uses O_EXCL (exclusive create) which is atomic on POSIX and Windows NTFS.
|
|
3833
|
+
// Protects the read-modify-write cycle in persist() from concurrent writers
|
|
3834
|
+
// in separate OS processes (e.g. MCP server + CLI running simultaneously).
|
|
3835
|
+
get lockPath() {
|
|
3836
|
+
return (0, import_node_path7.join)(this.dir, ".index.lock");
|
|
3837
|
+
}
|
|
3838
|
+
async acquireLock(timeoutMs = 3e3) {
|
|
3839
|
+
const deadline = Date.now() + timeoutMs;
|
|
3840
|
+
let delayMs = 10;
|
|
3841
|
+
while (true) {
|
|
3842
|
+
try {
|
|
3843
|
+
const fh = await (0, import_promises4.open)(this.lockPath, "wx");
|
|
3844
|
+
await fh.writeFile(String(process.pid));
|
|
3845
|
+
await fh.close();
|
|
3846
|
+
return async () => {
|
|
3847
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3848
|
+
});
|
|
3849
|
+
};
|
|
3850
|
+
} catch {
|
|
3851
|
+
try {
|
|
3852
|
+
const content = await (0, import_promises4.readFile)(this.lockPath, "utf-8");
|
|
3853
|
+
const lockPid = parseInt(content.trim(), 10);
|
|
3854
|
+
const fileStat = await (0, import_promises4.stat)(this.lockPath);
|
|
3855
|
+
const ageMs = Date.now() - fileStat.mtimeMs;
|
|
3856
|
+
if (ageMs > 1e4) {
|
|
3857
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3858
|
+
});
|
|
3859
|
+
continue;
|
|
3860
|
+
}
|
|
3861
|
+
if (!isNaN(lockPid)) {
|
|
3862
|
+
try {
|
|
3863
|
+
process.kill(lockPid, 0);
|
|
3864
|
+
} catch {
|
|
3865
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3866
|
+
});
|
|
3867
|
+
continue;
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
} catch {
|
|
3871
|
+
continue;
|
|
3872
|
+
}
|
|
3873
|
+
if (Date.now() > deadline) {
|
|
3874
|
+
return async () => {
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
3878
|
+
delayMs = Math.min(delayMs * 1.5, 200);
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3321
3882
|
/**
|
|
3322
3883
|
* Direct write used only during migration (before writeQueue is needed).
|
|
3323
3884
|
*/
|
|
3324
3885
|
async persistNow() {
|
|
3325
|
-
const
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3886
|
+
const releaseLock = await this.acquireLock();
|
|
3887
|
+
try {
|
|
3888
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3889
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
3890
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
3891
|
+
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3892
|
+
} finally {
|
|
3893
|
+
await releaseLock();
|
|
3894
|
+
}
|
|
3329
3895
|
}
|
|
3330
3896
|
persist() {
|
|
3331
3897
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
3332
|
-
const
|
|
3333
|
-
let onDisk = [];
|
|
3898
|
+
const releaseLock = await this.acquireLock();
|
|
3334
3899
|
try {
|
|
3335
|
-
const
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3900
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3901
|
+
let onDisk = [];
|
|
3902
|
+
try {
|
|
3903
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3904
|
+
const parsed = JSON.parse(raw);
|
|
3905
|
+
if (Array.isArray(parsed)) {
|
|
3906
|
+
onDisk = parsed.filter(isValidMeta);
|
|
3907
|
+
}
|
|
3908
|
+
} catch {
|
|
3339
3909
|
}
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3910
|
+
const ourIds = new Set(this.meta.map((m) => m.id));
|
|
3911
|
+
const external = onDisk.filter((m) => !ourIds.has(m.id));
|
|
3912
|
+
let merged = [...this.meta, ...external];
|
|
3913
|
+
if (merged.length > MAX_LIBRARY_SIZE) {
|
|
3914
|
+
merged.sort((a, b) => evictionScore(b) - evictionScore(a));
|
|
3915
|
+
merged = merged.slice(0, MAX_LIBRARY_SIZE);
|
|
3916
|
+
}
|
|
3917
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
3918
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
3919
|
+
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3920
|
+
} finally {
|
|
3921
|
+
await releaseLock();
|
|
3348
3922
|
}
|
|
3349
|
-
const tmpPath = `${indexPath}.tmp`;
|
|
3350
|
-
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
3351
|
-
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3352
3923
|
});
|
|
3353
3924
|
return this.writeQueue;
|
|
3354
3925
|
}
|
|
@@ -3370,6 +3941,19 @@ var SECRET_PATTERNS = [
|
|
|
3370
3941
|
/AIza[a-zA-Z0-9_-]{35}/,
|
|
3371
3942
|
/AKIA[A-Z0-9]{16}/
|
|
3372
3943
|
];
|
|
3944
|
+
var SECRET_PREFIXES = ["sk-", "ghp_", "xoxb-", "AIza", "AKIA"];
|
|
3945
|
+
function collectExpressionStrings(obj, out = []) {
|
|
3946
|
+
if (typeof obj === "string") {
|
|
3947
|
+
if (obj.includes("={{")) out.push(obj);
|
|
3948
|
+
} else if (Array.isArray(obj)) {
|
|
3949
|
+
for (const item of obj) collectExpressionStrings(item, out);
|
|
3950
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
3951
|
+
for (const val of Object.values(obj)) {
|
|
3952
|
+
collectExpressionStrings(val, out);
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
return out;
|
|
3956
|
+
}
|
|
3373
3957
|
function assessTemplateSafety(workflow) {
|
|
3374
3958
|
const reasons = [];
|
|
3375
3959
|
let worst = "safe";
|
|
@@ -3392,6 +3976,15 @@ function assessTemplateSafety(workflow) {
|
|
|
3392
3976
|
break;
|
|
3393
3977
|
}
|
|
3394
3978
|
}
|
|
3979
|
+
const expressions = collectExpressionStrings(node.parameters);
|
|
3980
|
+
for (const expr of expressions) {
|
|
3981
|
+
for (const prefix of SECRET_PREFIXES) {
|
|
3982
|
+
if (expr.includes(prefix)) {
|
|
3983
|
+
escalate("review", `Node "${node.name}" has an expression containing credential-like prefix "${prefix}"`);
|
|
3984
|
+
break;
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3395
3988
|
}
|
|
3396
3989
|
return { trustLevel: worst, reasons };
|
|
3397
3990
|
}
|
|
@@ -3449,12 +4042,26 @@ var TemplateSyncer = class {
|
|
|
3449
4042
|
}
|
|
3450
4043
|
return progress;
|
|
3451
4044
|
}
|
|
4045
|
+
async fetchWithBackoff(url, maxRetries = 3) {
|
|
4046
|
+
let delayMs = DELAY_BETWEEN_FETCHES_MS;
|
|
4047
|
+
let lastResponse;
|
|
4048
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
4049
|
+
lastResponse = await fetch(url);
|
|
4050
|
+
if (lastResponse.status !== 429 && lastResponse.status !== 503) return lastResponse;
|
|
4051
|
+
if (attempt === maxRetries) break;
|
|
4052
|
+
const retryAfterHeader = lastResponse.headers.get("Retry-After");
|
|
4053
|
+
const waitMs = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : delayMs * Math.pow(2, attempt);
|
|
4054
|
+
this.logger.warn(`HTTP ${lastResponse.status} from template API, retrying in ${waitMs}ms`, { url, attempt });
|
|
4055
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
4056
|
+
}
|
|
4057
|
+
return lastResponse;
|
|
4058
|
+
}
|
|
3452
4059
|
async fetchTemplateIds(max, progress) {
|
|
3453
4060
|
const ids = [];
|
|
3454
4061
|
let page = 1;
|
|
3455
4062
|
while (ids.length < max) {
|
|
3456
4063
|
const url = `${N8N_TEMPLATE_API}/search?page=${page}&rows=${PAGE_SIZE}`;
|
|
3457
|
-
const response = await
|
|
4064
|
+
const response = await this.fetchWithBackoff(url);
|
|
3458
4065
|
if (!response.ok) break;
|
|
3459
4066
|
const data = await response.json();
|
|
3460
4067
|
progress.total = Math.min(data.totalWorkflows, max);
|
|
@@ -3474,7 +4081,7 @@ var TemplateSyncer = class {
|
|
|
3474
4081
|
}
|
|
3475
4082
|
async processTemplate(id, progress) {
|
|
3476
4083
|
const url = `${N8N_TEMPLATE_API}/workflows/${id}`;
|
|
3477
|
-
const response = await
|
|
4084
|
+
const response = await this.fetchWithBackoff(url);
|
|
3478
4085
|
if (!response.ok) return;
|
|
3479
4086
|
const data = await response.json();
|
|
3480
4087
|
const templateMeta = data.workflow;
|