@kairos-sdk/core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -10
- package/dist/chunk-5GAY7CSJ.js +411 -0
- package/dist/chunk-5GAY7CSJ.js.map +1 -0
- package/dist/chunk-6FOFWVMG.js +1 -0
- package/dist/chunk-6FOFWVMG.js.map +1 -0
- package/dist/chunk-EVOAYH2K.js +569 -0
- package/dist/chunk-EVOAYH2K.js.map +1 -0
- package/dist/{chunk-N6LRD2FN.js → chunk-HBGZTUUZ.js} +81 -380
- package/dist/chunk-HBGZTUUZ.js.map +1 -0
- package/dist/{chunk-NJ6QZBIC.js → chunk-KIFT5LA7.js} +971 -572
- package/dist/chunk-KIFT5LA7.js.map +1 -0
- package/dist/cli.cjs +1341 -236
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +83 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1259 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -540
- package/dist/index.d.ts +3 -540
- package/dist/index.js +9 -5
- package/dist/mcp-server.cjs +1473 -402
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +357 -232
- package/dist/mcp-server.js.map +1 -1
- package/dist/reader-B5mV20H6.d.cts +596 -0
- package/dist/reader-B5mV20H6.d.ts +596 -0
- package/dist/standalone.cjs +2978 -0
- package/dist/standalone.cjs.map +1 -0
- package/dist/standalone.d.cts +106 -0
- package/dist/standalone.d.ts +106 -0
- package/dist/standalone.js +58 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +9 -1
- package/dist/chunk-N6LRD2FN.js.map +0 -1
- package/dist/chunk-NJ6QZBIC.js.map +0 -1
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,71 @@ 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
|
+
}
|
|
411
538
|
};
|
|
412
539
|
|
|
413
540
|
// src/validation/registry.ts
|
|
@@ -510,6 +637,14 @@ var NodeRegistry = class {
|
|
|
510
637
|
if (!def) return true;
|
|
511
638
|
return def.safeTypeVersions.includes(version);
|
|
512
639
|
}
|
|
640
|
+
// Returns true when the version is a positive integer greater than the highest
|
|
641
|
+
// known safe version — indicates a newer release rather than a bad value.
|
|
642
|
+
isVersionNewer(type, version) {
|
|
643
|
+
const def = this.byType.get(type);
|
|
644
|
+
if (!def || def.safeTypeVersions.length === 0) return false;
|
|
645
|
+
const max = Math.max(...def.safeTypeVersions);
|
|
646
|
+
return Number.isInteger(version) && version > max;
|
|
647
|
+
}
|
|
513
648
|
getRequiredParams(type) {
|
|
514
649
|
return this.byType.get(type)?.requiredParams ?? [];
|
|
515
650
|
}
|
|
@@ -559,6 +694,17 @@ var N8nValidator = class {
|
|
|
559
694
|
this.checkRule21(workflow, issues);
|
|
560
695
|
this.checkRule22(workflow, issues);
|
|
561
696
|
this.checkRule23(workflow, issues);
|
|
697
|
+
this.checkRule24(workflow, issues);
|
|
698
|
+
this.checkRule25(workflow, issues);
|
|
699
|
+
this.checkRule26(workflow, issues);
|
|
700
|
+
this.checkRule27(workflow, issues);
|
|
701
|
+
this.checkRule28(workflow, issues);
|
|
702
|
+
this.checkRule29(workflow, issues);
|
|
703
|
+
this.checkRule30(workflow, issues);
|
|
704
|
+
this.checkRule31(workflow, issues);
|
|
705
|
+
this.checkRule32(workflow, issues);
|
|
706
|
+
this.checkRule33(workflow, issues);
|
|
707
|
+
this.checkRule34(workflow, issues);
|
|
562
708
|
if (Array.isArray(workflow.nodes)) {
|
|
563
709
|
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
564
710
|
for (const issue of issues) {
|
|
@@ -691,10 +837,14 @@ var N8nValidator = class {
|
|
|
691
837
|
checkRule11(w, issues) {
|
|
692
838
|
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
693
839
|
const reachable = /* @__PURE__ */ new Set();
|
|
694
|
-
|
|
840
|
+
const aiSubNodeSources = /* @__PURE__ */ new Set();
|
|
841
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
695
842
|
if (typeof outputs !== "object" || outputs === null) continue;
|
|
696
|
-
|
|
843
|
+
let hasAiPort = false;
|
|
844
|
+
for (const [portName, portGroup] of Object.entries(outputs)) {
|
|
697
845
|
if (!Array.isArray(portGroup)) continue;
|
|
846
|
+
const isAiPort = portName.startsWith("ai_");
|
|
847
|
+
if (isAiPort) hasAiPort = true;
|
|
698
848
|
for (const targets of portGroup) {
|
|
699
849
|
if (!Array.isArray(targets)) continue;
|
|
700
850
|
for (const target of targets) {
|
|
@@ -703,10 +853,13 @@ var N8nValidator = class {
|
|
|
703
853
|
}
|
|
704
854
|
}
|
|
705
855
|
}
|
|
856
|
+
if (hasAiPort) aiSubNodeSources.add(sourceName);
|
|
706
857
|
}
|
|
707
858
|
for (const node of w.nodes) {
|
|
708
859
|
if (node.type.includes("stickyNote")) continue;
|
|
709
|
-
if (
|
|
860
|
+
if (this.isTriggerNode(node)) continue;
|
|
861
|
+
if (aiSubNodeSources.has(node.name)) continue;
|
|
862
|
+
if (!reachable.has(node.name)) {
|
|
710
863
|
this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
|
|
711
864
|
}
|
|
712
865
|
}
|
|
@@ -803,19 +956,22 @@ var N8nValidator = class {
|
|
|
803
956
|
}
|
|
804
957
|
}
|
|
805
958
|
}
|
|
806
|
-
// Rule 19 (WARN): typeVersion is within known safe range for registered node types
|
|
959
|
+
// Rule 19 (WARN): typeVersion is within known safe range for registered node types.
|
|
960
|
+
// In lenient mode (KAIROS_REGISTRY_STRICT != 'true'), versions higher than the known
|
|
961
|
+
// max are allowed — they likely represent newer n8n releases Kairos hasn't catalogued yet.
|
|
807
962
|
checkRule19(w, issues) {
|
|
808
963
|
if (!Array.isArray(w.nodes)) return;
|
|
964
|
+
const strict = process.env["KAIROS_REGISTRY_STRICT"] === "true";
|
|
809
965
|
for (const node of w.nodes) {
|
|
810
966
|
if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
|
|
811
|
-
if (
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
967
|
+
if (this.registry.isVersionSafe(node.type, node.typeVersion)) continue;
|
|
968
|
+
if (!strict && this.registry.isVersionNewer(node.type, node.typeVersion)) continue;
|
|
969
|
+
this.warn(
|
|
970
|
+
issues,
|
|
971
|
+
19,
|
|
972
|
+
`Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
|
|
973
|
+
node.id
|
|
974
|
+
);
|
|
819
975
|
}
|
|
820
976
|
}
|
|
821
977
|
// Rule 20 (WARN): cycle detection — no node should be reachable from itself
|
|
@@ -864,6 +1020,27 @@ var N8nValidator = class {
|
|
|
864
1020
|
}
|
|
865
1021
|
}
|
|
866
1022
|
}
|
|
1023
|
+
// Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
|
|
1024
|
+
checkRule21(w, issues) {
|
|
1025
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1026
|
+
const webhooksNeedingResponse = w.nodes.filter((n) => {
|
|
1027
|
+
if (!n.type.includes("webhook")) return false;
|
|
1028
|
+
const params = n.parameters;
|
|
1029
|
+
return params?.responseMode === "responseNode";
|
|
1030
|
+
});
|
|
1031
|
+
if (webhooksNeedingResponse.length === 0) return;
|
|
1032
|
+
const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
|
|
1033
|
+
if (!hasRespondNode) {
|
|
1034
|
+
for (const wh of webhooksNeedingResponse) {
|
|
1035
|
+
this.warn(
|
|
1036
|
+
issues,
|
|
1037
|
+
21,
|
|
1038
|
+
`Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
|
|
1039
|
+
wh.id
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
867
1044
|
// Rule 22 (WARN): check requiredParams from registry
|
|
868
1045
|
checkRule22(w, issues) {
|
|
869
1046
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -902,23 +1079,232 @@ var N8nValidator = class {
|
|
|
902
1079
|
}
|
|
903
1080
|
}
|
|
904
1081
|
}
|
|
905
|
-
// Rule
|
|
906
|
-
|
|
1082
|
+
// Rule 24 (WARN): deprecated accessor syntax in expressions
|
|
1083
|
+
checkRule24(w, issues) {
|
|
907
1084
|
if (!Array.isArray(w.nodes)) return;
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1085
|
+
const deprecated = /\$node\s*\[/;
|
|
1086
|
+
for (const node of w.nodes) {
|
|
1087
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1088
|
+
if (deprecated.test(expr)) {
|
|
1089
|
+
this.warn(
|
|
1090
|
+
issues,
|
|
1091
|
+
24,
|
|
1092
|
+
`Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
|
|
1093
|
+
node.id
|
|
1094
|
+
);
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
// Rule 25 (WARN): wrong item index assumptions in expressions
|
|
1101
|
+
checkRule25(w, issues) {
|
|
1102
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1103
|
+
const itemIndex = /\$json\s*\.\s*items\s*\[/;
|
|
1104
|
+
for (const node of w.nodes) {
|
|
1105
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1106
|
+
if (itemIndex.test(expr)) {
|
|
1107
|
+
this.warn(
|
|
1108
|
+
issues,
|
|
1109
|
+
25,
|
|
1110
|
+
`Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
|
|
1111
|
+
node.id
|
|
1112
|
+
);
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// Rule 26 (WARN): missing .first() or .all() on node references
|
|
1119
|
+
checkRule26(w, issues) {
|
|
1120
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1121
|
+
const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
|
|
1122
|
+
for (const node of w.nodes) {
|
|
1123
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1124
|
+
if (bareRef.test(expr)) {
|
|
1125
|
+
this.warn(
|
|
1126
|
+
issues,
|
|
1127
|
+
26,
|
|
1128
|
+
`Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
|
|
1129
|
+
node.id
|
|
1130
|
+
);
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
extractExpressions(params) {
|
|
1137
|
+
const expressions = [];
|
|
1138
|
+
const walk = (val) => {
|
|
1139
|
+
if (typeof val === "string") {
|
|
1140
|
+
if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
|
|
1141
|
+
expressions.push(val);
|
|
1142
|
+
}
|
|
1143
|
+
} else if (Array.isArray(val)) {
|
|
1144
|
+
for (const item of val) walk(item);
|
|
1145
|
+
} else if (val !== null && typeof val === "object") {
|
|
1146
|
+
for (const v of Object.values(val)) walk(v);
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
walk(params);
|
|
1150
|
+
return expressions;
|
|
1151
|
+
}
|
|
1152
|
+
// Rule 27 (WARN): httpRequest URL is a placeholder
|
|
1153
|
+
checkRule27(w, issues) {
|
|
1154
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1155
|
+
const PLACEHOLDER_RE = [
|
|
1156
|
+
/^https?:\/\/example\.com/i,
|
|
1157
|
+
/your[-_]?(api[-_]?)?url/i,
|
|
1158
|
+
/^https?:\/\/$/,
|
|
1159
|
+
/^<.+>$/,
|
|
1160
|
+
/placeholder/i
|
|
1161
|
+
];
|
|
1162
|
+
for (const node of w.nodes) {
|
|
1163
|
+
if (node.type !== "n8n-nodes-base.httpRequest") continue;
|
|
1164
|
+
const params = node.parameters;
|
|
1165
|
+
const url = params?.["url"];
|
|
1166
|
+
if (typeof url !== "string" || url.trim() === "") continue;
|
|
1167
|
+
if (PLACEHOLDER_RE.some((re) => re.test(url.trim()))) {
|
|
917
1168
|
this.warn(
|
|
918
1169
|
issues,
|
|
919
|
-
|
|
920
|
-
`
|
|
921
|
-
|
|
1170
|
+
27,
|
|
1171
|
+
`Node "${node.name}" httpRequest URL appears to be a placeholder: "${url}" \u2014 replace with your actual endpoint`,
|
|
1172
|
+
node.id
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
// Rule 28 (WARN): code node with empty or comment-only code
|
|
1178
|
+
checkRule28(w, issues) {
|
|
1179
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1180
|
+
for (const node of w.nodes) {
|
|
1181
|
+
if (node.type !== "n8n-nodes-base.code") continue;
|
|
1182
|
+
const params = node.parameters;
|
|
1183
|
+
const jsCode = typeof params?.["jsCode"] === "string" ? params["jsCode"] : "";
|
|
1184
|
+
const pythonCode = typeof params?.["pythonCode"] === "string" ? params["pythonCode"] : "";
|
|
1185
|
+
const code = jsCode || pythonCode;
|
|
1186
|
+
const stripped = code.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/#[^\n]*/g, "").trim();
|
|
1187
|
+
if (!stripped) {
|
|
1188
|
+
this.warn(issues, 28, `Node "${node.name}" code node has no executable code`, node.id);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
// Rule 29 (WARN): slack node message operation missing channel
|
|
1193
|
+
checkRule29(w, issues) {
|
|
1194
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1195
|
+
for (const node of w.nodes) {
|
|
1196
|
+
if (node.type !== "n8n-nodes-base.slack") continue;
|
|
1197
|
+
const params = node.parameters;
|
|
1198
|
+
const resource = params?.["resource"];
|
|
1199
|
+
const operation = params?.["operation"];
|
|
1200
|
+
const isMessageOp = resource === "message" || operation === "sendMessage" || operation === "post";
|
|
1201
|
+
if (!isMessageOp) continue;
|
|
1202
|
+
const channel = params?.["channel"] ?? params?.["channelId"];
|
|
1203
|
+
const rlValue = typeof channel === "object" && channel !== null ? channel["value"] : void 0;
|
|
1204
|
+
const isEmpty = channel === void 0 || channel === null || typeof channel === "string" && channel.trim() === "" || typeof channel === "object" && (!rlValue || typeof rlValue === "string" && rlValue.trim() === "");
|
|
1205
|
+
if (isEmpty) {
|
|
1206
|
+
this.warn(issues, 29, `Node "${node.name}" Slack message has no channel specified`, node.id);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// Rule 30 (WARN): gmail node send operation missing recipient
|
|
1211
|
+
checkRule30(w, issues) {
|
|
1212
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1213
|
+
for (const node of w.nodes) {
|
|
1214
|
+
if (node.type !== "n8n-nodes-base.gmail") continue;
|
|
1215
|
+
const params = node.parameters;
|
|
1216
|
+
const operation = params?.["operation"];
|
|
1217
|
+
if (operation !== "send") continue;
|
|
1218
|
+
const to = params?.["to"] ?? params?.["toList"];
|
|
1219
|
+
const isEmpty = to === void 0 || to === null || typeof to === "string" && to.trim() === "" || Array.isArray(to) && to.length === 0;
|
|
1220
|
+
if (isEmpty) {
|
|
1221
|
+
this.warn(issues, 30, `Node "${node.name}" gmail send has no recipient (to) specified`, node.id);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
// Rule 31 (WARN): if node with empty conditions
|
|
1226
|
+
checkRule31(w, issues) {
|
|
1227
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1228
|
+
for (const node of w.nodes) {
|
|
1229
|
+
if (node.type !== "n8n-nodes-base.if") continue;
|
|
1230
|
+
const params = node.parameters;
|
|
1231
|
+
const conditions = params?.["conditions"];
|
|
1232
|
+
if (conditions === void 0 || conditions === null) {
|
|
1233
|
+
this.warn(issues, 31, `Node "${node.name}" if node has no conditions defined`, node.id);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (typeof conditions === "object" && !Array.isArray(conditions)) {
|
|
1237
|
+
const conds = conditions["conditions"];
|
|
1238
|
+
if (!Array.isArray(conds) || conds.length === 0) {
|
|
1239
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1240
|
+
}
|
|
1241
|
+
} else if (Array.isArray(conditions) && conditions.length === 0) {
|
|
1242
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
// Rule 32 (WARN): set node with no assignments
|
|
1247
|
+
checkRule32(w, issues) {
|
|
1248
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1249
|
+
for (const node of w.nodes) {
|
|
1250
|
+
if (node.type !== "n8n-nodes-base.set") continue;
|
|
1251
|
+
const params = node.parameters;
|
|
1252
|
+
const assignmentsObj = params?.["assignments"];
|
|
1253
|
+
const assignmentsArr = assignmentsObj?.["assignments"];
|
|
1254
|
+
const valuesObj = params?.["values"];
|
|
1255
|
+
const hasV1 = valuesObj && Object.values(valuesObj).some((v) => Array.isArray(v) && v.length > 0);
|
|
1256
|
+
const hasV3 = Array.isArray(assignmentsArr) && assignmentsArr.length > 0;
|
|
1257
|
+
if (!hasV1 && !hasV3) {
|
|
1258
|
+
this.warn(
|
|
1259
|
+
issues,
|
|
1260
|
+
32,
|
|
1261
|
+
`Node "${node.name}" set node has no fields defined \u2014 it will pass data through unchanged`,
|
|
1262
|
+
node.id
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
// Rule 33 (WARN): scheduleTrigger with no schedule rules
|
|
1268
|
+
checkRule33(w, issues) {
|
|
1269
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1270
|
+
for (const node of w.nodes) {
|
|
1271
|
+
if (node.type !== "n8n-nodes-base.scheduleTrigger") continue;
|
|
1272
|
+
const params = node.parameters;
|
|
1273
|
+
const rule = params?.["rule"];
|
|
1274
|
+
const intervals = rule?.["interval"];
|
|
1275
|
+
if (!Array.isArray(intervals) || intervals.length === 0) {
|
|
1276
|
+
this.warn(issues, 33, `Node "${node.name}" scheduleTrigger has no schedule rules defined`, node.id);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
// Rule 34 (WARN): webhook path contains spaces, starts with slash, or looks like a full URL
|
|
1281
|
+
checkRule34(w, issues) {
|
|
1282
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1283
|
+
for (const node of w.nodes) {
|
|
1284
|
+
if (node.type !== "n8n-nodes-base.webhook") continue;
|
|
1285
|
+
const params = node.parameters;
|
|
1286
|
+
const path = params?.["path"];
|
|
1287
|
+
if (typeof path !== "string") continue;
|
|
1288
|
+
if (/\s/.test(path)) {
|
|
1289
|
+
this.warn(
|
|
1290
|
+
issues,
|
|
1291
|
+
34,
|
|
1292
|
+
`Node "${node.name}" webhook path contains spaces: "${path}" \u2014 use hyphens or underscores instead`,
|
|
1293
|
+
node.id
|
|
1294
|
+
);
|
|
1295
|
+
} else if (/^https?:\/\//i.test(path)) {
|
|
1296
|
+
this.warn(
|
|
1297
|
+
issues,
|
|
1298
|
+
34,
|
|
1299
|
+
`Node "${node.name}" webhook path looks like a full URL \u2014 it should be a relative path (e.g. "my-hook")`,
|
|
1300
|
+
node.id
|
|
1301
|
+
);
|
|
1302
|
+
} else if (path.startsWith("/")) {
|
|
1303
|
+
this.warn(
|
|
1304
|
+
issues,
|
|
1305
|
+
34,
|
|
1306
|
+
`Node "${node.name}" webhook path starts with "/" \u2014 n8n adds the leading slash automatically`,
|
|
1307
|
+
node.id
|
|
922
1308
|
);
|
|
923
1309
|
}
|
|
924
1310
|
}
|
|
@@ -989,9 +1375,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
|
|
|
989
1375
|
- Never reuse IDs, never use sequential fake IDs like "node-1"
|
|
990
1376
|
|
|
991
1377
|
### Credentials:
|
|
992
|
-
-
|
|
993
|
-
|
|
994
|
-
-
|
|
1378
|
+
- Each credential is keyed by its type string, with an object value containing id and name:
|
|
1379
|
+
"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack Credential" } }
|
|
1380
|
+
- Use "placeholder-id" as the id \u2014 users replace this with their real credential ID from n8n after deployment
|
|
1381
|
+
- The credentialsNeeded field in your response declares what credentials the user must configure
|
|
1382
|
+
- Never put API keys or tokens directly in node parameters when a credential type exists
|
|
995
1383
|
|
|
996
1384
|
### Node names:
|
|
997
1385
|
- All node names must be unique within the workflow
|
|
@@ -1038,6 +1426,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
|
|
|
1038
1426
|
|
|
1039
1427
|
---
|
|
1040
1428
|
|
|
1429
|
+
## EXPRESSION SYNTAX \u2014 how to reference upstream node data
|
|
1430
|
+
|
|
1431
|
+
### Accessing a field from an upstream node:
|
|
1432
|
+
- CORRECT: $('NodeName').item.json.field
|
|
1433
|
+
- WRONG: $node["NodeName"].json.field \u2190 deprecated accessor, fails at runtime (Rule 24)
|
|
1434
|
+
|
|
1435
|
+
### Accessing array items from $json:
|
|
1436
|
+
- CORRECT: $json.field \u2190 n8n auto-flattens items; each item is already a flat object
|
|
1437
|
+
- WRONG: $json.items[0].field \u2190 do not index into items[] (Rule 25)
|
|
1438
|
+
|
|
1439
|
+
### Calling node data \u2014 always qualify with .first() or .all():
|
|
1440
|
+
- CORRECT: $('NodeName').first().json.field \u2190 single item
|
|
1441
|
+
- CORRECT: $('NodeName').all() \u2190 array of all items
|
|
1442
|
+
- WRONG: $('NodeName').json \u2190 throws at runtime without .first() or .all() (Rule 26)
|
|
1443
|
+
|
|
1444
|
+
---
|
|
1445
|
+
|
|
1041
1446
|
## NODE CATALOG \u2014 exact type strings and safe typeVersions
|
|
1042
1447
|
|
|
1043
1448
|
### Triggers (always at least one required):
|
|
@@ -1137,6 +1542,17 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
|
|
|
1137
1542
|
5. At least one trigger node present
|
|
1138
1543
|
6. Every AI Agent has an ai_languageModel sub-node
|
|
1139
1544
|
7. settings block is complete with executionOrder: "v1"
|
|
1545
|
+
8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
|
|
1546
|
+
9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
|
|
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://)
|
|
1140
1556
|
|
|
1141
1557
|
---
|
|
1142
1558
|
|
|
@@ -1153,7 +1569,7 @@ function scoreToMode(score) {
|
|
|
1153
1569
|
}
|
|
1154
1570
|
|
|
1155
1571
|
// src/validation/rule-metadata.ts
|
|
1156
|
-
var VALIDATOR_RULE_IDS = Array.from({ length:
|
|
1572
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 34 }, (_, i) => i + 1);
|
|
1157
1573
|
var RULE_PIPELINE_STAGES = {
|
|
1158
1574
|
1: "node_generation",
|
|
1159
1575
|
2: "node_generation",
|
|
@@ -1177,7 +1593,68 @@ var RULE_PIPELINE_STAGES = {
|
|
|
1177
1593
|
20: "connection_wiring",
|
|
1178
1594
|
21: "workflow_structure",
|
|
1179
1595
|
22: "workflow_structure",
|
|
1180
|
-
23: "node_generation"
|
|
1596
|
+
23: "node_generation",
|
|
1597
|
+
24: "expression_syntax",
|
|
1598
|
+
25: "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"
|
|
1608
|
+
};
|
|
1609
|
+
var RULE_EXAMPLES = {
|
|
1610
|
+
17: {
|
|
1611
|
+
bad: '"credentials": { "slackOAuth2Api": "my-token" }',
|
|
1612
|
+
good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
|
|
1613
|
+
},
|
|
1614
|
+
24: {
|
|
1615
|
+
bad: '$node["Fetch Data"].json.email',
|
|
1616
|
+
good: "$('Fetch Data').item.json.email"
|
|
1617
|
+
},
|
|
1618
|
+
25: {
|
|
1619
|
+
bad: "$json.items[0].email",
|
|
1620
|
+
good: "$json.email"
|
|
1621
|
+
},
|
|
1622
|
+
26: {
|
|
1623
|
+
bad: "$('Fetch Data').json.email",
|
|
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"'
|
|
1657
|
+
}
|
|
1181
1658
|
};
|
|
1182
1659
|
var RULE_MITIGATIONS = {
|
|
1183
1660
|
1: "Provide a non-empty workflow name string",
|
|
@@ -1196,36 +1673,86 @@ var RULE_MITIGATIONS = {
|
|
|
1196
1673
|
14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
|
|
1197
1674
|
15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
|
|
1198
1675
|
16: "All node names must be unique within the workflow",
|
|
1199
|
-
17: '
|
|
1676
|
+
17: 'Each credential entry must be keyed by credential type with an object value: { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Credential" } } \u2014 the key is the credential type, the value has id and name strings',
|
|
1200
1677
|
18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
|
|
1201
1678
|
19: "Use known safe typeVersion values for each node type",
|
|
1202
1679
|
20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
|
|
1203
1680
|
21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
|
|
1204
1681
|
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
|
|
1205
|
-
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync"
|
|
1682
|
+
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
1683
|
+
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
1684
|
+
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
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")'
|
|
1206
1694
|
};
|
|
1207
1695
|
|
|
1208
1696
|
// src/generation/prompt-builder.ts
|
|
1209
1697
|
var CRITICAL_SCORE_THRESHOLD = 0.15;
|
|
1698
|
+
function resolveProfile() {
|
|
1699
|
+
const env = process.env["KAIROS_PROMPT_PROFILE"];
|
|
1700
|
+
if (env === "minimal" || env === "standard" || env === "rich") return env;
|
|
1701
|
+
return "standard";
|
|
1702
|
+
}
|
|
1703
|
+
var PROACTIVE_EXPRESSION_GUIDANCE = `## Expression Syntax Quick Reference
|
|
1704
|
+
|
|
1705
|
+
Always use these patterns in expressions:
|
|
1706
|
+
- Access node data: $('NodeName').item.json.field (not $node["NodeName"].json)
|
|
1707
|
+
- Access JSON field: $json.field (not $json.items[0].field)
|
|
1708
|
+
- Single item: $('NodeName').first().json.field
|
|
1709
|
+
- All items: $('NodeName').all()`;
|
|
1210
1710
|
var PromptBuilder = class {
|
|
1211
1711
|
patternsPath;
|
|
1212
|
-
|
|
1712
|
+
profile;
|
|
1713
|
+
_lastActivePatterns = null;
|
|
1714
|
+
constructor(patternsPath, profile) {
|
|
1213
1715
|
this.patternsPath = patternsPath ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "patterns.json");
|
|
1716
|
+
this.profile = profile ?? resolveProfile();
|
|
1717
|
+
}
|
|
1718
|
+
resolveMaxPatterns() {
|
|
1719
|
+
if (this.profile === "minimal") return 3;
|
|
1720
|
+
if (this.profile === "rich") return 15;
|
|
1721
|
+
return 10;
|
|
1214
1722
|
}
|
|
1215
1723
|
build(request, matches, globalFailureRates = [], dynamicCatalog) {
|
|
1216
1724
|
const mode = this.resolveMode(matches);
|
|
1217
|
-
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
|
|
1725
|
+
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog, request.description);
|
|
1218
1726
|
const userMessage = this.buildUserMessage(request, matches, mode);
|
|
1219
1727
|
return { system, userMessage, mode, matches };
|
|
1220
1728
|
}
|
|
1221
|
-
buildCorrectionMessage(request, matches, allIssues, attempt) {
|
|
1729
|
+
buildCorrectionMessage(request, matches, allIssues, attempt, failingRuleIds) {
|
|
1222
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
|
+
}
|
|
1223
1750
|
return `${base}
|
|
1224
1751
|
|
|
1225
1752
|
IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
|
|
1226
1753
|
${allIssues.join("\n")}
|
|
1227
1754
|
|
|
1228
|
-
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}`;
|
|
1229
1756
|
}
|
|
1230
1757
|
resolveMode(matches) {
|
|
1231
1758
|
if (matches.length === 0) return "scratch";
|
|
@@ -1233,7 +1760,7 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1233
1760
|
if (!top) return "scratch";
|
|
1234
1761
|
return scoreToMode(top.score);
|
|
1235
1762
|
}
|
|
1236
|
-
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog) {
|
|
1763
|
+
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog, description) {
|
|
1237
1764
|
let basePrompt = SYSTEM_PROMPT_V1;
|
|
1238
1765
|
if (dynamicCatalog) {
|
|
1239
1766
|
basePrompt = basePrompt.replace(
|
|
@@ -1248,53 +1775,62 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1248
1775
|
cache_control: { type: "ephemeral" }
|
|
1249
1776
|
}
|
|
1250
1777
|
];
|
|
1251
|
-
if (
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1778
|
+
if (this.profile !== "minimal") {
|
|
1779
|
+
if (mode === "reference" && matches.length > 0) {
|
|
1780
|
+
const refText = matches.slice(0, 3).map((m) => {
|
|
1781
|
+
const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1782
|
+
return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
|
|
1255
1783
|
Nodes:
|
|
1256
1784
|
${nodes}`;
|
|
1257
|
-
|
|
1258
|
-
blocks.push({
|
|
1259
|
-
type: "text",
|
|
1260
|
-
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1261
|
-
|
|
1262
|
-
${refText}`
|
|
1263
|
-
});
|
|
1264
|
-
}
|
|
1265
|
-
if (mode === "direct" && matches[0]) {
|
|
1266
|
-
const match = matches[0];
|
|
1267
|
-
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1268
|
-
if (json.length > 3e4) {
|
|
1269
|
-
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1785
|
+
}).join("\n\n");
|
|
1270
1786
|
blocks.push({
|
|
1271
1787
|
type: "text",
|
|
1272
|
-
text: `##
|
|
1788
|
+
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1789
|
+
|
|
1790
|
+
${refText}`
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
if (mode === "direct" && matches[0]) {
|
|
1794
|
+
const match = matches[0];
|
|
1795
|
+
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1796
|
+
if (json.length > 3e4) {
|
|
1797
|
+
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1798
|
+
blocks.push({
|
|
1799
|
+
type: "text",
|
|
1800
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
|
|
1273
1801
|
Nodes:
|
|
1274
1802
|
${nodes}`
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1803
|
+
});
|
|
1804
|
+
} else {
|
|
1805
|
+
blocks.push({
|
|
1806
|
+
type: "text",
|
|
1807
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
|
|
1280
1808
|
|
|
1281
1809
|
${json}`
|
|
1282
|
-
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1283
1812
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
text: `## Weak Structural Hint
|
|
1813
|
+
if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
|
|
1814
|
+
const hint = matches[0];
|
|
1815
|
+
const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
|
|
1816
|
+
blocks.push({
|
|
1817
|
+
type: "text",
|
|
1818
|
+
text: `## Weak Structural Hint
|
|
1291
1819
|
A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
|
|
1292
|
-
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1293
1822
|
}
|
|
1294
|
-
const warnings = this.buildFailureWarnings(matches, globalFailureRates);
|
|
1823
|
+
const warnings = this.buildFailureWarnings(matches, globalFailureRates, description);
|
|
1295
1824
|
if (warnings) {
|
|
1296
1825
|
blocks.push({ type: "text", text: warnings });
|
|
1297
1826
|
}
|
|
1827
|
+
if (this.profile === "rich") {
|
|
1828
|
+
const expressionRules = /* @__PURE__ */ new Set([24, 25, 26]);
|
|
1829
|
+
const expressionAlreadyCovered = (this._lastActivePatterns ?? []).some((p) => expressionRules.has(p.rule));
|
|
1830
|
+
if (!expressionAlreadyCovered) {
|
|
1831
|
+
blocks.push({ type: "text", text: PROACTIVE_EXPRESSION_GUIDANCE });
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1298
1834
|
return blocks;
|
|
1299
1835
|
}
|
|
1300
1836
|
loadPatterns() {
|
|
@@ -1308,18 +1844,38 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1308
1844
|
}
|
|
1309
1845
|
}
|
|
1310
1846
|
getWarnedRules() {
|
|
1311
|
-
|
|
1847
|
+
const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
|
|
1848
|
+
return patterns.map((p) => p.rule);
|
|
1312
1849
|
}
|
|
1313
|
-
getActivePatterns() {
|
|
1314
|
-
const MAX_WARNED = 10;
|
|
1850
|
+
getActivePatterns(maxCount = 10, description) {
|
|
1315
1851
|
const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
|
|
1316
1852
|
const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1317
1853
|
const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1318
1854
|
const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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);
|
|
1878
|
+
this._lastActivePatterns = richPatterns;
|
|
1323
1879
|
if (richPatterns.length > 0) {
|
|
1324
1880
|
return this.buildStageGroupedWarnings(richPatterns, matches);
|
|
1325
1881
|
}
|
|
@@ -1330,7 +1886,8 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1330
1886
|
credential_injection: "CREDENTIAL FORMATTING",
|
|
1331
1887
|
connection_wiring: "CONNECTION WIRING",
|
|
1332
1888
|
node_generation: "NODE GENERATION",
|
|
1333
|
-
workflow_structure: "WORKFLOW STRUCTURE"
|
|
1889
|
+
workflow_structure: "WORKFLOW STRUCTURE",
|
|
1890
|
+
expression_syntax: "EXPRESSION SYNTAX"
|
|
1334
1891
|
};
|
|
1335
1892
|
const byStage = /* @__PURE__ */ new Map();
|
|
1336
1893
|
for (const p of patterns) {
|
|
@@ -1358,7 +1915,11 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1358
1915
|
const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
|
|
1359
1916
|
const remedyStr = remedy ? `
|
|
1360
1917
|
Fix: ${remedy}` : "";
|
|
1361
|
-
|
|
1918
|
+
const ex = RULE_EXAMPLES[p.rule];
|
|
1919
|
+
const exampleStr = ex ? `
|
|
1920
|
+
Bad: ${ex.bad}
|
|
1921
|
+
Good: ${ex.good}` : "";
|
|
1922
|
+
lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}${exampleStr}`);
|
|
1362
1923
|
} else {
|
|
1363
1924
|
const ruleNums = group.map((p) => p.rule).join(", ");
|
|
1364
1925
|
const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
|
|
@@ -1465,12 +2026,12 @@ var GENERATE_WORKFLOW_TOOL = {
|
|
|
1465
2026
|
}
|
|
1466
2027
|
};
|
|
1467
2028
|
var WorkflowDesigner = class {
|
|
1468
|
-
constructor(anthropic, model, logger) {
|
|
2029
|
+
constructor(anthropic, model, logger, patternsPath) {
|
|
1469
2030
|
this.anthropic = anthropic;
|
|
1470
2031
|
this.model = model;
|
|
1471
2032
|
this.logger = logger;
|
|
1472
2033
|
this.validator = new N8nValidator();
|
|
1473
|
-
this.promptBuilder = new PromptBuilder();
|
|
2034
|
+
this.promptBuilder = new PromptBuilder(patternsPath);
|
|
1474
2035
|
}
|
|
1475
2036
|
anthropic;
|
|
1476
2037
|
model;
|
|
@@ -1493,7 +2054,8 @@ var WorkflowDesigner = class {
|
|
|
1493
2054
|
const issueLines = lastErrors.map(
|
|
1494
2055
|
(i) => `- [Rule ${i.rule}] ${i.message}${i.nodeId ? ` (node: ${i.nodeId})` : ""}`
|
|
1495
2056
|
);
|
|
1496
|
-
|
|
2057
|
+
const failingRuleIds = lastErrors.map((i) => i.rule);
|
|
2058
|
+
userMessage = this.promptBuilder.buildCorrectionMessage(request, matches, issueLines, attempt - 1, failingRuleIds);
|
|
1497
2059
|
this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: lastErrors.length });
|
|
1498
2060
|
}
|
|
1499
2061
|
const start = Date.now();
|
|
@@ -1554,6 +2116,11 @@ var WorkflowDesigner = class {
|
|
|
1554
2116
|
}
|
|
1555
2117
|
}
|
|
1556
2118
|
extractToolUse(message) {
|
|
2119
|
+
if (message.stop_reason === "max_tokens") {
|
|
2120
|
+
throw new GenerationError(
|
|
2121
|
+
"Claude response was truncated (max_tokens reached) \u2014 the workflow may be too large. Try a simpler description or break it into smaller workflows."
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
1557
2124
|
const toolUseBlock = message.content.find(
|
|
1558
2125
|
(block) => block.type === "tool_use"
|
|
1559
2126
|
);
|
|
@@ -1596,11 +2163,12 @@ var TelemetryCollector = class {
|
|
|
1596
2163
|
this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
|
|
1597
2164
|
this.sessionId = generateUUID();
|
|
1598
2165
|
}
|
|
1599
|
-
async emit(eventType, data) {
|
|
2166
|
+
async emit(eventType, data, runId) {
|
|
1600
2167
|
const event = {
|
|
1601
2168
|
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
1602
2169
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1603
2170
|
sessionId: this.sessionId,
|
|
2171
|
+
...runId ? { runId } : {},
|
|
1604
2172
|
eventType,
|
|
1605
2173
|
data
|
|
1606
2174
|
};
|
|
@@ -1673,19 +2241,20 @@ var TelemetryReader = class {
|
|
|
1673
2241
|
}
|
|
1674
2242
|
const events = await this.readRecentEvents(days);
|
|
1675
2243
|
const buildSessions = new Set(
|
|
1676
|
-
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)
|
|
1677
2245
|
);
|
|
1678
2246
|
const MIN_BUILDS_FOR_RATES = 3;
|
|
1679
2247
|
if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
|
|
1680
2248
|
const ruleSessions = /* @__PURE__ */ new Map();
|
|
1681
2249
|
for (const event of events) {
|
|
1682
2250
|
if (event.eventType !== "generation_attempt") continue;
|
|
1683
|
-
|
|
2251
|
+
const eventKey = event.runId ?? event.sessionId;
|
|
2252
|
+
if (!buildSessions.has(eventKey)) continue;
|
|
1684
2253
|
const data = event.data;
|
|
1685
2254
|
if (data.validationPassed || !data.issues) continue;
|
|
1686
2255
|
for (const issue of data.issues) {
|
|
1687
2256
|
const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
|
|
1688
|
-
entry.sessions.add(
|
|
2257
|
+
entry.sessions.add(eventKey);
|
|
1689
2258
|
entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
|
|
1690
2259
|
ruleSessions.set(issue.rule, entry);
|
|
1691
2260
|
}
|
|
@@ -1726,22 +2295,25 @@ var PATTERN_SCHEMA_VERSION = 2;
|
|
|
1726
2295
|
var PatternAnalyzer = class _PatternAnalyzer {
|
|
1727
2296
|
telemetryDir;
|
|
1728
2297
|
outputDir;
|
|
2298
|
+
_cachedEvents = null;
|
|
2299
|
+
_cachedPreviousPatterns = null;
|
|
1729
2300
|
constructor(telemetryDir) {
|
|
1730
2301
|
const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
|
|
1731
2302
|
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
1732
2303
|
this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
|
|
1733
2304
|
}
|
|
1734
2305
|
async loadPreviousPatterns() {
|
|
2306
|
+
if (this._cachedPreviousPatterns !== null) return this._cachedPreviousPatterns;
|
|
1735
2307
|
try {
|
|
1736
2308
|
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
|
|
1737
2309
|
const prev = JSON.parse(raw);
|
|
1738
2310
|
const version = prev.schemaVersion ?? 0;
|
|
1739
2311
|
const patterns = prev.topFailureRules ?? [];
|
|
1740
|
-
|
|
1741
|
-
return this.migratePatterns(patterns, version);
|
|
2312
|
+
this._cachedPreviousPatterns = version === PATTERN_SCHEMA_VERSION ? patterns : this.migratePatterns(patterns, version);
|
|
1742
2313
|
} catch {
|
|
1743
|
-
|
|
2314
|
+
this._cachedPreviousPatterns = [];
|
|
1744
2315
|
}
|
|
2316
|
+
return this._cachedPreviousPatterns;
|
|
1745
2317
|
}
|
|
1746
2318
|
migratePatterns(patterns, fromVersion) {
|
|
1747
2319
|
let migrated = patterns;
|
|
@@ -1754,19 +2326,23 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1754
2326
|
}));
|
|
1755
2327
|
}
|
|
1756
2328
|
if (fromVersion < 2) {
|
|
1757
|
-
migrated = migrated.map((p) =>
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
...p
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2329
|
+
migrated = migrated.map((p) => {
|
|
2330
|
+
const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
|
|
2331
|
+
return {
|
|
2332
|
+
...p,
|
|
2333
|
+
scoringFactors: {
|
|
2334
|
+
...sf,
|
|
2335
|
+
stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
});
|
|
1764
2339
|
}
|
|
1765
2340
|
return migrated;
|
|
1766
2341
|
}
|
|
1767
2342
|
async analyze(days = 30) {
|
|
1768
2343
|
const previousPatterns = await this.loadPreviousPatterns();
|
|
1769
2344
|
const events = await this.readAllEvents(days);
|
|
2345
|
+
this._cachedEvents = events;
|
|
1770
2346
|
const starts = events.filter((e) => e.eventType === "build_start");
|
|
1771
2347
|
const attempts = events.filter((e) => e.eventType === "generation_attempt");
|
|
1772
2348
|
const passed = attempts.filter(
|
|
@@ -1779,13 +2355,18 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1779
2355
|
const credentialFailures = /* @__PURE__ */ new Map();
|
|
1780
2356
|
for (const a of failed) {
|
|
1781
2357
|
const weight = this.recencyWeight(a.fileDate);
|
|
2358
|
+
const buildId = a.runId ?? a.sessionId;
|
|
1782
2359
|
const data = a.data;
|
|
1783
2360
|
for (const issue of data.issues ?? []) {
|
|
1784
|
-
|
|
2361
|
+
if (issue.severity === "warn") continue;
|
|
2362
|
+
const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
|
|
1785
2363
|
entry.count++;
|
|
1786
|
-
entry.sessions.add(
|
|
2364
|
+
entry.sessions.add(buildId);
|
|
1787
2365
|
entry.recencyWeights.push(weight);
|
|
1788
2366
|
entry.allMessages.push(issue.message);
|
|
2367
|
+
if (data.workflowType) {
|
|
2368
|
+
entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
|
|
2369
|
+
}
|
|
1789
2370
|
ruleFailures.set(issue.rule, entry);
|
|
1790
2371
|
if (issue.rule === 17) {
|
|
1791
2372
|
const credPatterns = [
|
|
@@ -1838,9 +2419,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1838
2419
|
}
|
|
1839
2420
|
const sessions = /* @__PURE__ */ new Map();
|
|
1840
2421
|
for (const a of attempts) {
|
|
1841
|
-
const
|
|
2422
|
+
const buildId = a.runId ?? a.sessionId;
|
|
2423
|
+
const list = sessions.get(buildId) ?? [];
|
|
1842
2424
|
list.push(a);
|
|
1843
|
-
sessions.set(
|
|
2425
|
+
sessions.set(buildId, list);
|
|
1844
2426
|
}
|
|
1845
2427
|
let firstTryPass = 0;
|
|
1846
2428
|
let correctionNeeded = 0;
|
|
@@ -1887,7 +2469,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1887
2469
|
const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
|
|
1888
2470
|
const stickiness = stickinessCount.get(rule) ?? 0;
|
|
1889
2471
|
const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
|
|
1890
|
-
|
|
2472
|
+
const pattern = {
|
|
1891
2473
|
rule,
|
|
1892
2474
|
failureCount: entry.count,
|
|
1893
2475
|
confidence: Math.round(rawConfidence * 1e3) / 1e3,
|
|
@@ -1899,6 +2481,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1899
2481
|
exampleMessages: this.deduplicateMessages(entry.allMessages),
|
|
1900
2482
|
mitigation: RULE_MITIGATIONS[rule] ?? null
|
|
1901
2483
|
};
|
|
2484
|
+
if (entry.workflowTypes.size > 0) {
|
|
2485
|
+
pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
|
|
2486
|
+
}
|
|
2487
|
+
return pattern;
|
|
1902
2488
|
}).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1903
2489
|
const activeRules = new Set(activePatterns.map((p) => p.rule));
|
|
1904
2490
|
for (const p of activePatterns) {
|
|
@@ -1955,7 +2541,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1955
2541
|
const warned = bcData.warnedRules ?? [];
|
|
1956
2542
|
if (warned.length === 0) continue;
|
|
1957
2543
|
const sessionFailedRules = /* @__PURE__ */ new Set();
|
|
1958
|
-
const sessionAttempts = sessions.get(bc.sessionId) ?? [];
|
|
2544
|
+
const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
|
|
1959
2545
|
for (const a of sessionAttempts) {
|
|
1960
2546
|
const ad = a.data;
|
|
1961
2547
|
if (ad.validationPassed === false) {
|
|
@@ -2027,6 +2613,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2027
2613
|
const tmpPath = `${outputPath}.tmp`;
|
|
2028
2614
|
await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
2029
2615
|
await (0, import_promises3.rename)(tmpPath, outputPath);
|
|
2616
|
+
this._cachedPreviousPatterns = null;
|
|
2030
2617
|
const historySummary = {
|
|
2031
2618
|
timestamp: analysis.generatedAt,
|
|
2032
2619
|
totalBuilds: analysis.summary.totalBuilds,
|
|
@@ -2038,8 +2625,55 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2038
2625
|
};
|
|
2039
2626
|
const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
|
|
2040
2627
|
await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
|
|
2628
|
+
const sessions = await this.buildSessionSummaries(days);
|
|
2629
|
+
const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
|
|
2630
|
+
const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
|
|
2631
|
+
await (0, import_promises3.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
|
|
2632
|
+
await (0, import_promises3.rename)(sessionHistoryTmp, sessionHistoryPath);
|
|
2041
2633
|
return analysis;
|
|
2042
2634
|
}
|
|
2635
|
+
async getSessions(limit = 20) {
|
|
2636
|
+
try {
|
|
2637
|
+
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
|
|
2638
|
+
const all = JSON.parse(raw);
|
|
2639
|
+
return all.slice(-limit);
|
|
2640
|
+
} catch {
|
|
2641
|
+
return [];
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
async buildSessionSummaries(days = 30) {
|
|
2645
|
+
const events = this._cachedEvents ?? await this.readAllEvents(days);
|
|
2646
|
+
const buildCompletes = events.filter((e) => e.eventType === "build_complete");
|
|
2647
|
+
const attemptsByBuild = /* @__PURE__ */ new Map();
|
|
2648
|
+
for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
|
|
2649
|
+
const buildId = e.runId ?? e.sessionId;
|
|
2650
|
+
const list = attemptsByBuild.get(buildId) ?? [];
|
|
2651
|
+
list.push(e);
|
|
2652
|
+
attemptsByBuild.set(buildId, list);
|
|
2653
|
+
}
|
|
2654
|
+
const summaries = buildCompletes.map((bc) => {
|
|
2655
|
+
const data = bc.data;
|
|
2656
|
+
const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
|
|
2657
|
+
const failedRules = Array.from(new Set(
|
|
2658
|
+
sessionAttempts.flatMap((a) => {
|
|
2659
|
+
const ad = a.data;
|
|
2660
|
+
if (ad.validationPassed !== false) return [];
|
|
2661
|
+
return (ad.issues ?? []).map((i) => i.rule);
|
|
2662
|
+
})
|
|
2663
|
+
));
|
|
2664
|
+
return {
|
|
2665
|
+
sessionId: bc.runId ?? bc.sessionId,
|
|
2666
|
+
date: bc.fileDate,
|
|
2667
|
+
description: data.description ?? "",
|
|
2668
|
+
workflowType: data.workflowType ?? null,
|
|
2669
|
+
attempts: data.totalAttempts ?? 1,
|
|
2670
|
+
success: data.success ?? false,
|
|
2671
|
+
failedRules,
|
|
2672
|
+
workflowName: data.workflowName ?? null
|
|
2673
|
+
};
|
|
2674
|
+
});
|
|
2675
|
+
return summaries.sort((a, b) => a.date.localeCompare(b.date));
|
|
2676
|
+
}
|
|
2043
2677
|
async getHistory(limit = 20) {
|
|
2044
2678
|
try {
|
|
2045
2679
|
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
|
|
@@ -2061,7 +2695,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2061
2695
|
alerts.push({
|
|
2062
2696
|
type: "stale_pattern",
|
|
2063
2697
|
rule: p.rule,
|
|
2064
|
-
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)`
|
|
2065
2699
|
});
|
|
2066
2700
|
}
|
|
2067
2701
|
}
|
|
@@ -2148,8 +2782,60 @@ var nullLogger = {
|
|
|
2148
2782
|
}
|
|
2149
2783
|
};
|
|
2150
2784
|
|
|
2785
|
+
// src/utils/workflow-type.ts
|
|
2786
|
+
var TYPE_KEYWORDS = [
|
|
2787
|
+
["gmail", "email"],
|
|
2788
|
+
["imap", "email"],
|
|
2789
|
+
["smtp", "email"],
|
|
2790
|
+
[" email", "email"],
|
|
2791
|
+
["slack", "slack"],
|
|
2792
|
+
["telegram", "messaging"],
|
|
2793
|
+
["discord", "messaging"],
|
|
2794
|
+
[" sms", "messaging"],
|
|
2795
|
+
["twilio", "messaging"],
|
|
2796
|
+
["webhook", "webhook"],
|
|
2797
|
+
["google sheets", "data"],
|
|
2798
|
+
["spreadsheet", "data"],
|
|
2799
|
+
["airtable", "data"],
|
|
2800
|
+
["notion", "data"],
|
|
2801
|
+
["github", "devops"],
|
|
2802
|
+
["gitlab", "devops"],
|
|
2803
|
+
["schedule", "schedule"],
|
|
2804
|
+
[" cron", "schedule"],
|
|
2805
|
+
["daily", "schedule"],
|
|
2806
|
+
["weekly", "schedule"],
|
|
2807
|
+
["hourly", "schedule"],
|
|
2808
|
+
["every day", "schedule"],
|
|
2809
|
+
["every hour", "schedule"],
|
|
2810
|
+
["every morning", "schedule"],
|
|
2811
|
+
["postgres", "database"],
|
|
2812
|
+
["mysql", "database"],
|
|
2813
|
+
["supabase", "database"],
|
|
2814
|
+
["redis", "database"],
|
|
2815
|
+
[" database", "database"],
|
|
2816
|
+
[" llm", "ai"],
|
|
2817
|
+
[" gpt", "ai"],
|
|
2818
|
+
["claude", "ai"],
|
|
2819
|
+
[" agent", "ai"],
|
|
2820
|
+
["langchain", "ai"],
|
|
2821
|
+
[" ai ", "ai"],
|
|
2822
|
+
[" ai", "ai"],
|
|
2823
|
+
["http request", "api"],
|
|
2824
|
+
["rest api", "api"],
|
|
2825
|
+
[" api", "api"]
|
|
2826
|
+
];
|
|
2827
|
+
function inferWorkflowType(description) {
|
|
2828
|
+
const lower = " " + description.toLowerCase();
|
|
2829
|
+
for (const [keyword, type] of TYPE_KEYWORDS) {
|
|
2830
|
+
if (lower.includes(keyword)) return type;
|
|
2831
|
+
}
|
|
2832
|
+
return null;
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2151
2835
|
// src/client.ts
|
|
2152
|
-
var
|
|
2836
|
+
var import_node_os5 = require("os");
|
|
2837
|
+
var import_node_path6 = require("path");
|
|
2838
|
+
var DEFAULT_MODEL = process.env["KAIROS_MODEL"] ?? "claude-sonnet-4-6";
|
|
2153
2839
|
var Kairos = class {
|
|
2154
2840
|
provider;
|
|
2155
2841
|
designer;
|
|
@@ -2177,7 +2863,8 @@ var Kairos = class {
|
|
|
2177
2863
|
this.provider = null;
|
|
2178
2864
|
}
|
|
2179
2865
|
const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
|
|
2180
|
-
|
|
2866
|
+
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");
|
|
2867
|
+
this.designer = new WorkflowDesigner(anthropic, this.model, logger, patternsPath);
|
|
2181
2868
|
this.validator = new N8nValidator();
|
|
2182
2869
|
this.library = options.library ?? new NullLibrary();
|
|
2183
2870
|
this.logger = logger;
|
|
@@ -2210,11 +2897,13 @@ var Kairos = class {
|
|
|
2210
2897
|
this.validateDescription(description);
|
|
2211
2898
|
this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
|
|
2212
2899
|
const buildStart = Date.now();
|
|
2900
|
+
const runId = generateUUID();
|
|
2901
|
+
const workflowType = inferWorkflowType(description);
|
|
2213
2902
|
await this.telemetry?.emit("build_start", {
|
|
2214
2903
|
description,
|
|
2215
2904
|
model: this.model,
|
|
2216
2905
|
dryRun: options?.dryRun ?? false
|
|
2217
|
-
});
|
|
2906
|
+
}, runId);
|
|
2218
2907
|
await this.library.initialize();
|
|
2219
2908
|
const matches = await this.library.search(description);
|
|
2220
2909
|
if (matches.length > 0) {
|
|
@@ -2249,8 +2938,9 @@ var Kairos = class {
|
|
|
2249
2938
|
tokensOutput: meta.tokensOutput,
|
|
2250
2939
|
validationPassed: meta.validationPassed,
|
|
2251
2940
|
issueCount: meta.issues.length,
|
|
2252
|
-
issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
|
|
2253
|
-
|
|
2941
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
2942
|
+
workflowType
|
|
2943
|
+
}, runId);
|
|
2254
2944
|
}
|
|
2255
2945
|
await this.telemetry?.emit("build_complete", {
|
|
2256
2946
|
description,
|
|
@@ -2263,13 +2953,14 @@ var Kairos = class {
|
|
|
2263
2953
|
workflowId: null,
|
|
2264
2954
|
dryRun: options?.dryRun ?? false,
|
|
2265
2955
|
credentialsNeeded: 0,
|
|
2266
|
-
warnedRules: err.warnedRules ?? []
|
|
2267
|
-
|
|
2956
|
+
warnedRules: err.warnedRules ?? [],
|
|
2957
|
+
workflowType
|
|
2958
|
+
}, runId);
|
|
2268
2959
|
this.updatePatterns();
|
|
2269
2960
|
}
|
|
2270
2961
|
throw err;
|
|
2271
2962
|
}
|
|
2272
|
-
await this.emitAttemptTelemetry(description, designResult);
|
|
2963
|
+
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
2273
2964
|
const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
|
|
2274
2965
|
this.saveToLibrary(workflow, description, designResult, matches);
|
|
2275
2966
|
if (options?.dryRun) {
|
|
@@ -2286,8 +2977,9 @@ var Kairos = class {
|
|
|
2286
2977
|
workflowId: null,
|
|
2287
2978
|
dryRun: true,
|
|
2288
2979
|
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2289
|
-
warnedRules: designResult.warnedRules
|
|
2290
|
-
|
|
2980
|
+
warnedRules: designResult.warnedRules,
|
|
2981
|
+
workflowType
|
|
2982
|
+
}, runId);
|
|
2291
2983
|
this.updatePatterns();
|
|
2292
2984
|
return {
|
|
2293
2985
|
workflowId: null,
|
|
@@ -2301,10 +2993,19 @@ var Kairos = class {
|
|
|
2301
2993
|
}
|
|
2302
2994
|
const provider = this.requireProvider();
|
|
2303
2995
|
const deployed = await provider.deploy(workflow);
|
|
2304
|
-
this.
|
|
2996
|
+
this.logger.info("Workflow deployed to n8n", { workflowId: deployed.workflowId, name: deployed.name });
|
|
2997
|
+
this.recordDeploy(deployed.workflowId);
|
|
2305
2998
|
if (options?.activate) {
|
|
2306
2999
|
await provider.activate(deployed.workflowId);
|
|
2307
3000
|
}
|
|
3001
|
+
let smokeTestResult;
|
|
3002
|
+
if (options?.smokeTest) {
|
|
3003
|
+
smokeTestResult = await provider.smokeTest(deployed.workflowId, workflow).catch((err) => {
|
|
3004
|
+
this.logger.warn("Smoke test threw unexpectedly", { err: String(err) });
|
|
3005
|
+
return { status: "error", triggerType: "manual", error: String(err) };
|
|
3006
|
+
});
|
|
3007
|
+
this.logger.info("Smoke test complete", { status: smokeTestResult.status, triggerType: smokeTestResult.triggerType });
|
|
3008
|
+
}
|
|
2308
3009
|
const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
2309
3010
|
const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
2310
3011
|
await this.telemetry?.emit("build_complete", {
|
|
@@ -2318,8 +3019,9 @@ var Kairos = class {
|
|
|
2318
3019
|
workflowId: deployed.workflowId,
|
|
2319
3020
|
dryRun: false,
|
|
2320
3021
|
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2321
|
-
warnedRules: designResult.warnedRules
|
|
2322
|
-
|
|
3022
|
+
warnedRules: designResult.warnedRules,
|
|
3023
|
+
workflowType
|
|
3024
|
+
}, runId);
|
|
2323
3025
|
this.updatePatterns();
|
|
2324
3026
|
return {
|
|
2325
3027
|
workflowId: deployed.workflowId,
|
|
@@ -2328,18 +3030,21 @@ var Kairos = class {
|
|
|
2328
3030
|
credentialsNeeded: designResult.credentialsNeeded,
|
|
2329
3031
|
activationRequired: !options?.activate,
|
|
2330
3032
|
generationAttempts: designResult.attempts,
|
|
2331
|
-
dryRun: false
|
|
3033
|
+
dryRun: false,
|
|
3034
|
+
...smokeTestResult !== void 0 ? { smokeTest: smokeTestResult } : {}
|
|
2332
3035
|
};
|
|
2333
3036
|
}
|
|
2334
3037
|
async replace(id, description) {
|
|
2335
3038
|
this.validateDescription(description);
|
|
2336
3039
|
this.logger.info("Kairos.update", { id, description });
|
|
2337
3040
|
const buildStart = Date.now();
|
|
3041
|
+
const runId = generateUUID();
|
|
3042
|
+
const workflowType = inferWorkflowType(description);
|
|
2338
3043
|
await this.telemetry?.emit("build_start", {
|
|
2339
3044
|
description,
|
|
2340
3045
|
model: this.model,
|
|
2341
3046
|
dryRun: false
|
|
2342
|
-
});
|
|
3047
|
+
}, runId);
|
|
2343
3048
|
await this.library.initialize();
|
|
2344
3049
|
const matches = await this.library.search(description);
|
|
2345
3050
|
const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
|
|
@@ -2358,8 +3063,9 @@ var Kairos = class {
|
|
|
2358
3063
|
tokensOutput: meta.tokensOutput,
|
|
2359
3064
|
validationPassed: meta.validationPassed,
|
|
2360
3065
|
issueCount: meta.issues.length,
|
|
2361
|
-
issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
|
|
2362
|
-
|
|
3066
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
3067
|
+
workflowType
|
|
3068
|
+
}, runId);
|
|
2363
3069
|
}
|
|
2364
3070
|
await this.telemetry?.emit("build_complete", {
|
|
2365
3071
|
description,
|
|
@@ -2372,16 +3078,18 @@ var Kairos = class {
|
|
|
2372
3078
|
workflowId: null,
|
|
2373
3079
|
dryRun: false,
|
|
2374
3080
|
credentialsNeeded: 0,
|
|
2375
|
-
warnedRules: err.warnedRules ?? []
|
|
2376
|
-
|
|
3081
|
+
warnedRules: err.warnedRules ?? [],
|
|
3082
|
+
workflowType
|
|
3083
|
+
}, runId);
|
|
2377
3084
|
this.updatePatterns();
|
|
2378
3085
|
}
|
|
2379
3086
|
throw err;
|
|
2380
3087
|
}
|
|
2381
|
-
await this.emitAttemptTelemetry(description, designResult);
|
|
3088
|
+
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
2382
3089
|
const provider = this.requireProvider();
|
|
2383
3090
|
const deployed = await provider.update(id, designResult.workflow);
|
|
2384
|
-
this.
|
|
3091
|
+
this.logger.info("Workflow updated in n8n", { workflowId: deployed.workflowId, name: deployed.name });
|
|
3092
|
+
this.saveToLibrary(designResult.workflow, description, designResult, matches, deployed.workflowId);
|
|
2385
3093
|
this.recordDeploy();
|
|
2386
3094
|
const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
2387
3095
|
const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
@@ -2396,8 +3104,9 @@ var Kairos = class {
|
|
|
2396
3104
|
workflowId: deployed.workflowId,
|
|
2397
3105
|
dryRun: false,
|
|
2398
3106
|
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2399
|
-
warnedRules: designResult.warnedRules
|
|
2400
|
-
|
|
3107
|
+
warnedRules: designResult.warnedRules,
|
|
3108
|
+
workflowType
|
|
3109
|
+
}, runId);
|
|
2401
3110
|
this.updatePatterns();
|
|
2402
3111
|
return {
|
|
2403
3112
|
workflowId: deployed.workflowId,
|
|
@@ -2420,7 +3129,7 @@ var Kairos = class {
|
|
|
2420
3129
|
return null;
|
|
2421
3130
|
});
|
|
2422
3131
|
}
|
|
2423
|
-
async emitAttemptTelemetry(description, designResult) {
|
|
3132
|
+
async emitAttemptTelemetry(description, designResult, workflowType, runId) {
|
|
2424
3133
|
for (const meta of designResult.attemptMetadata) {
|
|
2425
3134
|
await this.telemetry?.emit("generation_attempt", {
|
|
2426
3135
|
description,
|
|
@@ -2431,14 +3140,15 @@ var Kairos = class {
|
|
|
2431
3140
|
tokensOutput: meta.tokensOutput,
|
|
2432
3141
|
validationPassed: meta.validationPassed,
|
|
2433
3142
|
issueCount: meta.issues.length,
|
|
2434
|
-
issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
|
|
2435
|
-
|
|
3143
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
3144
|
+
workflowType
|
|
3145
|
+
}, runId);
|
|
2436
3146
|
}
|
|
2437
3147
|
}
|
|
2438
|
-
recordDeploy() {
|
|
3148
|
+
recordDeploy(n8nWorkflowId) {
|
|
2439
3149
|
this.saveQueue = this.saveQueue.then(async (savedId) => {
|
|
2440
3150
|
if (savedId) {
|
|
2441
|
-
await this.library.recordDeployment(savedId);
|
|
3151
|
+
await this.library.recordDeployment(savedId, n8nWorkflowId);
|
|
2442
3152
|
}
|
|
2443
3153
|
return savedId;
|
|
2444
3154
|
}).catch((err) => {
|
|
@@ -2446,7 +3156,7 @@ var Kairos = class {
|
|
|
2446
3156
|
return null;
|
|
2447
3157
|
});
|
|
2448
3158
|
}
|
|
2449
|
-
saveToLibrary(workflow, description, designResult, matches) {
|
|
3159
|
+
saveToLibrary(workflow, description, designResult, matches, n8nWorkflowId) {
|
|
2450
3160
|
const failedAttempts = designResult.attemptMetadata.filter((m) => !m.validationPassed);
|
|
2451
3161
|
const failurePatterns = failedAttempts.flatMap(
|
|
2452
3162
|
(m) => m.issues.map((i) => ({ rule: i.rule, message: i.message }))
|
|
@@ -2472,6 +3182,7 @@ var Kairos = class {
|
|
|
2472
3182
|
if (matches.length > 0) metadata.sourceWorkflowIds = matches.map((m) => m.workflow.id);
|
|
2473
3183
|
if (topMatch) metadata.topMatchScore = topMatch.score;
|
|
2474
3184
|
if (designResult.credentialsNeeded.length > 0) metadata.credentialsNeeded = designResult.credentialsNeeded;
|
|
3185
|
+
if (n8nWorkflowId) metadata.n8nWorkflowId = n8nWorkflowId;
|
|
2475
3186
|
const firstTryPass = designResult.attemptMetadata.length > 0 && designResult.attemptMetadata[0].validationPassed;
|
|
2476
3187
|
const failedRules = Array.from(new Set(
|
|
2477
3188
|
designResult.attemptMetadata.filter((m) => !m.validationPassed).flatMap((m) => m.issues.map((i) => i.rule))
|
|
@@ -2531,16 +3242,36 @@ var Kairos = class {
|
|
|
2531
3242
|
|
|
2532
3243
|
// src/library/file-library.ts
|
|
2533
3244
|
var import_promises4 = require("fs/promises");
|
|
2534
|
-
var
|
|
2535
|
-
var
|
|
3245
|
+
var import_node_path7 = require("path");
|
|
3246
|
+
var import_node_os6 = require("os");
|
|
2536
3247
|
|
|
2537
3248
|
// src/library/scorer.ts
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
3249
|
+
function loadWeights() {
|
|
3250
|
+
const raw = {
|
|
3251
|
+
tfidf: parseFloat(process.env["KAIROS_WEIGHT_TFIDF"] ?? ""),
|
|
3252
|
+
nodeFingerprint: parseFloat(process.env["KAIROS_WEIGHT_JACCARD"] ?? ""),
|
|
3253
|
+
outcome: parseFloat(process.env["KAIROS_WEIGHT_OUTCOME"] ?? ""),
|
|
3254
|
+
deploy: parseFloat(process.env["KAIROS_WEIGHT_DEPLOY"] ?? "")
|
|
3255
|
+
};
|
|
3256
|
+
const defaults = { tfidf: 0.35, nodeFingerprint: 0.3, outcome: 0.2, deploy: 0.15 };
|
|
3257
|
+
const anySet = Object.values(raw).some((v) => !isNaN(v) && v >= 0);
|
|
3258
|
+
if (!anySet) return defaults;
|
|
3259
|
+
const w = {
|
|
3260
|
+
tfidf: !isNaN(raw.tfidf) && raw.tfidf >= 0 ? raw.tfidf : defaults.tfidf,
|
|
3261
|
+
nodeFingerprint: !isNaN(raw.nodeFingerprint) && raw.nodeFingerprint >= 0 ? raw.nodeFingerprint : defaults.nodeFingerprint,
|
|
3262
|
+
outcome: !isNaN(raw.outcome) && raw.outcome >= 0 ? raw.outcome : defaults.outcome,
|
|
3263
|
+
deploy: !isNaN(raw.deploy) && raw.deploy >= 0 ? raw.deploy : defaults.deploy
|
|
3264
|
+
};
|
|
3265
|
+
const total = w.tfidf + w.nodeFingerprint + w.outcome + w.deploy;
|
|
3266
|
+
if (total <= 0) return defaults;
|
|
3267
|
+
return {
|
|
3268
|
+
tfidf: w.tfidf / total,
|
|
3269
|
+
nodeFingerprint: w.nodeFingerprint / total,
|
|
3270
|
+
outcome: w.outcome / total,
|
|
3271
|
+
deploy: w.deploy / total
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
var WEIGHTS = loadWeights();
|
|
2544
3275
|
var NODE_KEYWORDS = {
|
|
2545
3276
|
slack: ["slack", "slackApi"],
|
|
2546
3277
|
email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
|
|
@@ -2725,6 +3456,8 @@ function clusterWorkflows(workflows) {
|
|
|
2725
3456
|
}
|
|
2726
3457
|
return clusters.sort((a, b) => b.members.length - a.members.length);
|
|
2727
3458
|
}
|
|
3459
|
+
var NOVELTY_BOOST = 0.05;
|
|
3460
|
+
var NOVELTY_PENALTY = 0.03;
|
|
2728
3461
|
function rerank(candidates, clusters) {
|
|
2729
3462
|
const clusterMap = /* @__PURE__ */ new Map();
|
|
2730
3463
|
for (const cluster of clusters) {
|
|
@@ -2732,7 +3465,7 @@ function rerank(candidates, clusters) {
|
|
|
2732
3465
|
clusterMap.set(member.id, cluster);
|
|
2733
3466
|
}
|
|
2734
3467
|
}
|
|
2735
|
-
|
|
3468
|
+
const pass1 = candidates.map((c) => {
|
|
2736
3469
|
const cluster = clusterMap.get(c.workflow.id);
|
|
2737
3470
|
let boost = 0;
|
|
2738
3471
|
if (cluster && cluster.avgFirstTryPassRate > 0) {
|
|
@@ -2744,7 +3477,25 @@ function rerank(candidates, clusters) {
|
|
|
2744
3477
|
return {
|
|
2745
3478
|
workflow: c.workflow,
|
|
2746
3479
|
score: Math.max(0, Math.min(1, c.score + boost)),
|
|
2747
|
-
|
|
3480
|
+
cluster
|
|
3481
|
+
};
|
|
3482
|
+
}).sort((a, b) => b.score - a.score);
|
|
3483
|
+
const seenFingerprints = /* @__PURE__ */ new Set();
|
|
3484
|
+
return pass1.map((c) => {
|
|
3485
|
+
const fpKey = c.cluster ? fingerprintKey(c.cluster.fingerprint) : null;
|
|
3486
|
+
let noveltyAdjust = 0;
|
|
3487
|
+
if (fpKey !== null) {
|
|
3488
|
+
if (!seenFingerprints.has(fpKey)) {
|
|
3489
|
+
seenFingerprints.add(fpKey);
|
|
3490
|
+
noveltyAdjust = NOVELTY_BOOST;
|
|
3491
|
+
} else {
|
|
3492
|
+
noveltyAdjust = -NOVELTY_PENALTY;
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
return {
|
|
3496
|
+
workflow: c.workflow,
|
|
3497
|
+
score: Math.max(0, Math.min(1, c.score + noveltyAdjust)),
|
|
3498
|
+
...c.cluster ? { clusterPattern: c.cluster.pattern } : {}
|
|
2748
3499
|
};
|
|
2749
3500
|
}).sort((a, b) => b.score - a.score);
|
|
2750
3501
|
}
|
|
@@ -2761,14 +3512,32 @@ function buildSearchCorpus(w) {
|
|
|
2761
3512
|
});
|
|
2762
3513
|
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
2763
3514
|
}
|
|
2764
|
-
var
|
|
3515
|
+
var _rawSize = parseInt(process.env["KAIROS_LIBRARY_SIZE"] ?? "500", 10);
|
|
3516
|
+
var MAX_LIBRARY_SIZE = Number.isFinite(_rawSize) && _rawSize >= 10 ? _rawSize : 500;
|
|
3517
|
+
function evictionScore(m) {
|
|
3518
|
+
return (m.deployCount ?? 0) * 3 + (m.timesRetrieved ?? 0) + (m.outcomeStats?.totalUses ?? 0);
|
|
3519
|
+
}
|
|
3520
|
+
function isValidMeta(item) {
|
|
3521
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
3522
|
+
}
|
|
3523
|
+
function isValidOldEntry(item) {
|
|
3524
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
|
|
3525
|
+
item.workflow.nodes
|
|
3526
|
+
);
|
|
3527
|
+
}
|
|
2765
3528
|
var FileLibrary = class {
|
|
2766
3529
|
dir;
|
|
2767
|
-
|
|
3530
|
+
meta = [];
|
|
2768
3531
|
initPromise = null;
|
|
2769
3532
|
writeQueue = Promise.resolve();
|
|
2770
3533
|
constructor(dir) {
|
|
2771
|
-
this.dir = dir ?? (0,
|
|
3534
|
+
this.dir = dir ?? (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".kairos", "library");
|
|
3535
|
+
}
|
|
3536
|
+
get workflowsDir() {
|
|
3537
|
+
return (0, import_node_path7.join)(this.dir, "workflows");
|
|
3538
|
+
}
|
|
3539
|
+
workflowFilePath(id) {
|
|
3540
|
+
return (0, import_node_path7.join)(this.workflowsDir, `${id}.json`);
|
|
2772
3541
|
}
|
|
2773
3542
|
async initialize() {
|
|
2774
3543
|
if (!this.initPromise) {
|
|
@@ -2778,61 +3547,197 @@ var FileLibrary = class {
|
|
|
2778
3547
|
}
|
|
2779
3548
|
async doInitialize() {
|
|
2780
3549
|
await (0, import_promises4.mkdir)(this.dir, { recursive: true });
|
|
2781
|
-
const indexPath = (0,
|
|
3550
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3551
|
+
let workflowsDirExists = false;
|
|
2782
3552
|
try {
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
);
|
|
3553
|
+
await (0, import_promises4.stat)(this.workflowsDir);
|
|
3554
|
+
workflowsDirExists = true;
|
|
3555
|
+
} catch {
|
|
3556
|
+
}
|
|
3557
|
+
if (workflowsDirExists) {
|
|
3558
|
+
try {
|
|
3559
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3560
|
+
const parsed = JSON.parse(raw);
|
|
3561
|
+
if (Array.isArray(parsed)) {
|
|
3562
|
+
this.meta = parsed.filter(isValidMeta);
|
|
3563
|
+
}
|
|
3564
|
+
} catch {
|
|
3565
|
+
this.meta = [];
|
|
3566
|
+
}
|
|
3567
|
+
await this.scanForOrphansAndCleanup();
|
|
3568
|
+
} else {
|
|
3569
|
+
try {
|
|
3570
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3571
|
+
const parsed = JSON.parse(raw);
|
|
3572
|
+
if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
|
|
3573
|
+
await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
} catch {
|
|
3577
|
+
}
|
|
3578
|
+
this.meta = [];
|
|
3579
|
+
await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
async scanForOrphansAndCleanup() {
|
|
3583
|
+
let entries;
|
|
3584
|
+
try {
|
|
3585
|
+
entries = await (0, import_promises4.readdir)(this.workflowsDir);
|
|
3586
|
+
} catch {
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
const indexedIds = new Set(this.meta.map((m) => m.id));
|
|
3590
|
+
const orphanIds = [];
|
|
3591
|
+
for (const filename of entries) {
|
|
3592
|
+
if (filename.endsWith(".tmp")) {
|
|
3593
|
+
await (0, import_promises4.unlink)((0, import_node_path7.join)(this.workflowsDir, filename)).catch(() => {
|
|
3594
|
+
});
|
|
3595
|
+
continue;
|
|
2791
3596
|
}
|
|
3597
|
+
if (!filename.endsWith(".json")) continue;
|
|
3598
|
+
const id = filename.slice(0, -5);
|
|
3599
|
+
if (!indexedIds.has(id)) {
|
|
3600
|
+
orphanIds.push(id);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
if (orphanIds.length > 0) {
|
|
3604
|
+
console.warn(`[FileLibrary] Found ${orphanIds.length} orphaned workflow file(s) not in index: ${orphanIds.join(", ")}`);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
/**
|
|
3608
|
+
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
3609
|
+
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
3610
|
+
* meta entry. Rewrites index.json in the new format.
|
|
3611
|
+
*/
|
|
3612
|
+
async migrateFromMonolithic(oldEntries) {
|
|
3613
|
+
await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
|
|
3614
|
+
const newMeta = [];
|
|
3615
|
+
for (const entry of oldEntries) {
|
|
3616
|
+
const wfPath = this.workflowFilePath(entry.id);
|
|
3617
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
3618
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
|
|
3619
|
+
await (0, import_promises4.rename)(tmpPath, wfPath);
|
|
3620
|
+
const { workflow, ...metaFields } = entry;
|
|
3621
|
+
newMeta.push({
|
|
3622
|
+
...metaFields,
|
|
3623
|
+
workflowName: workflow.name,
|
|
3624
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type)
|
|
3625
|
+
});
|
|
3626
|
+
}
|
|
3627
|
+
this.meta = newMeta;
|
|
3628
|
+
await this.persistNow();
|
|
3629
|
+
}
|
|
3630
|
+
async loadWorkflowFile(id) {
|
|
3631
|
+
try {
|
|
3632
|
+
const raw = await (0, import_promises4.readFile)(this.workflowFilePath(id), "utf-8");
|
|
3633
|
+
return JSON.parse(raw);
|
|
2792
3634
|
} catch {
|
|
2793
|
-
|
|
3635
|
+
return null;
|
|
2794
3636
|
}
|
|
2795
3637
|
}
|
|
3638
|
+
async writeWorkflowFile(id, workflow) {
|
|
3639
|
+
const wfPath = this.workflowFilePath(id);
|
|
3640
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
3641
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
|
|
3642
|
+
await (0, import_promises4.rename)(tmpPath, wfPath);
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Build a lightweight StoredWorkflow shell from a meta entry for use in
|
|
3646
|
+
* scoring / clustering. Only node.type is populated in each node — no other
|
|
3647
|
+
* node fields are used by hybridScore or clusterWorkflows.
|
|
3648
|
+
*/
|
|
3649
|
+
makeSearchShell(m) {
|
|
3650
|
+
return {
|
|
3651
|
+
...m,
|
|
3652
|
+
workflow: {
|
|
3653
|
+
name: m.workflowName,
|
|
3654
|
+
nodes: m.cachedNodeTypes.map((type) => ({
|
|
3655
|
+
id: "",
|
|
3656
|
+
name: "",
|
|
3657
|
+
type,
|
|
3658
|
+
typeVersion: 1,
|
|
3659
|
+
position: [0, 0],
|
|
3660
|
+
parameters: {}
|
|
3661
|
+
})),
|
|
3662
|
+
connections: {}
|
|
3663
|
+
}
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
2796
3666
|
async search(description, options) {
|
|
2797
|
-
const
|
|
2798
|
-
if (
|
|
3667
|
+
const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
|
|
3668
|
+
if (filteredMeta.length === 0) return [];
|
|
2799
3669
|
const limit = options?.limit ?? 3;
|
|
2800
3670
|
const queryTokens = tokenize(description);
|
|
2801
3671
|
if (queryTokens.length === 0) return [];
|
|
2802
|
-
const
|
|
3672
|
+
const shells = filteredMeta.map((m) => this.makeSearchShell(m));
|
|
3673
|
+
const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
|
|
2803
3674
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
2804
|
-
const docCount =
|
|
3675
|
+
const docCount = shells.length;
|
|
2805
3676
|
const idf = /* @__PURE__ */ new Map();
|
|
3677
|
+
const idfCeiling = Math.log(docCount + 1) + 1;
|
|
2806
3678
|
const allTokens = new Set(queryTokens);
|
|
2807
3679
|
for (const token of allTokens) {
|
|
2808
3680
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
2809
|
-
|
|
3681
|
+
const rawIdf = Math.log((docCount + 1) / (docsWithToken + 1)) + 1;
|
|
3682
|
+
idf.set(token, rawIdf / idfCeiling);
|
|
2810
3683
|
}
|
|
2811
|
-
const scored = hybridScore(queryTokens, description,
|
|
2812
|
-
const clusters = clusterWorkflows(
|
|
3684
|
+
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
3685
|
+
const clusters = clusterWorkflows(shells);
|
|
2813
3686
|
const reranked = rerank(scored, clusters).slice(0, limit);
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
for (const r of results) {
|
|
2819
|
-
r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
|
|
2820
|
-
}
|
|
2821
|
-
this.persist();
|
|
3687
|
+
if (reranked.length === 0) return [];
|
|
3688
|
+
for (const r of reranked) {
|
|
3689
|
+
const m = this.meta.find((m2) => m2.id === r.workflow.id);
|
|
3690
|
+
if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
|
|
2822
3691
|
}
|
|
2823
|
-
|
|
3692
|
+
this.persist();
|
|
3693
|
+
const results = await Promise.all(
|
|
3694
|
+
reranked.map(async (r) => {
|
|
3695
|
+
const m = this.meta.find((meta) => meta.id === r.workflow.id);
|
|
3696
|
+
const workflow = await this.loadWorkflowFile(r.workflow.id);
|
|
3697
|
+
if (!workflow) return null;
|
|
3698
|
+
return {
|
|
3699
|
+
workflow: { ...m, workflow },
|
|
3700
|
+
score: r.score,
|
|
3701
|
+
mode: scoreToMode(r.score)
|
|
3702
|
+
};
|
|
3703
|
+
})
|
|
3704
|
+
);
|
|
3705
|
+
return results.filter((r) => r !== null);
|
|
2824
3706
|
}
|
|
2825
3707
|
async save(workflow, metadata) {
|
|
3708
|
+
const existingByN8nId = metadata.n8nWorkflowId ? this.meta.find((m) => m.n8nWorkflowId === metadata.n8nWorkflowId) : void 0;
|
|
3709
|
+
const normalizedDesc = metadata.description.trim().toLowerCase();
|
|
3710
|
+
const existing = existingByN8nId ?? this.meta.find((m) => m.description.trim().toLowerCase() === normalizedDesc);
|
|
3711
|
+
if (existing) {
|
|
3712
|
+
existing.description = metadata.description;
|
|
3713
|
+
existing.workflowName = workflow.name;
|
|
3714
|
+
existing.cachedNodeTypes = workflow.nodes.map((n) => n.type);
|
|
3715
|
+
if (metadata.n8nWorkflowId) existing.n8nWorkflowId = metadata.n8nWorkflowId;
|
|
3716
|
+
if (metadata.generationAttempts != null) {
|
|
3717
|
+
existing.generationAttempts = metadata.generationAttempts;
|
|
3718
|
+
}
|
|
3719
|
+
if (metadata.failurePatterns?.length) {
|
|
3720
|
+
existing.failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
3721
|
+
}
|
|
3722
|
+
if (metadata.tags?.length) {
|
|
3723
|
+
existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...metadata.tags])];
|
|
3724
|
+
}
|
|
3725
|
+
await this.writeWorkflowFile(existing.id, workflow);
|
|
3726
|
+
await this.persist();
|
|
3727
|
+
return existing.id;
|
|
3728
|
+
}
|
|
2826
3729
|
const id = generateUUID();
|
|
3730
|
+
await this.writeWorkflowFile(id, workflow);
|
|
2827
3731
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
2828
|
-
const
|
|
3732
|
+
const meta = {
|
|
2829
3733
|
id,
|
|
2830
|
-
workflow,
|
|
2831
3734
|
description: metadata.description,
|
|
2832
3735
|
tags: metadata.tags ?? [],
|
|
2833
3736
|
platform: metadata.platform ?? "n8n",
|
|
2834
3737
|
deployCount: 0,
|
|
2835
3738
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3739
|
+
workflowName: workflow.name,
|
|
3740
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type),
|
|
2836
3741
|
...failurePatterns?.length ? { failurePatterns } : {},
|
|
2837
3742
|
...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
|
|
2838
3743
|
...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
|
|
@@ -2842,33 +3747,39 @@ var FileLibrary = class {
|
|
|
2842
3747
|
...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
|
|
2843
3748
|
...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
|
|
2844
3749
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
2845
|
-
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
3750
|
+
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {},
|
|
3751
|
+
...metadata.n8nWorkflowId ? { n8nWorkflowId: metadata.n8nWorkflowId } : {}
|
|
2846
3752
|
};
|
|
2847
|
-
this.
|
|
2848
|
-
if (this.
|
|
2849
|
-
this.
|
|
2850
|
-
|
|
3753
|
+
this.meta.push(meta);
|
|
3754
|
+
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
3755
|
+
this.meta.sort((a, b) => {
|
|
3756
|
+
if (a.id === id) return -1;
|
|
3757
|
+
if (b.id === id) return 1;
|
|
3758
|
+
return evictionScore(b) - evictionScore(a);
|
|
3759
|
+
});
|
|
3760
|
+
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
2851
3761
|
}
|
|
2852
3762
|
await this.persist();
|
|
2853
3763
|
return id;
|
|
2854
3764
|
}
|
|
2855
|
-
async recordDeployment(id) {
|
|
2856
|
-
const
|
|
2857
|
-
if (
|
|
2858
|
-
|
|
2859
|
-
|
|
3765
|
+
async recordDeployment(id, n8nWorkflowId) {
|
|
3766
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3767
|
+
if (m) {
|
|
3768
|
+
m.deployCount++;
|
|
3769
|
+
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3770
|
+
if (n8nWorkflowId) m.n8nWorkflowId = n8nWorkflowId;
|
|
2860
3771
|
await this.persist();
|
|
2861
3772
|
}
|
|
2862
3773
|
}
|
|
2863
3774
|
async recordOutcome(id, outcome) {
|
|
2864
|
-
const
|
|
2865
|
-
if (!
|
|
3775
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3776
|
+
if (!m) return;
|
|
2866
3777
|
if (outcome.mode === "direct") {
|
|
2867
|
-
|
|
3778
|
+
m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
|
|
2868
3779
|
} else {
|
|
2869
|
-
|
|
3780
|
+
m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
|
|
2870
3781
|
}
|
|
2871
|
-
const stats =
|
|
3782
|
+
const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
|
|
2872
3783
|
stats.totalUses++;
|
|
2873
3784
|
stats.totalAttempts += outcome.attempts;
|
|
2874
3785
|
if (outcome.firstTryPass) stats.firstTryPasses++;
|
|
@@ -2876,24 +3787,35 @@ var FileLibrary = class {
|
|
|
2876
3787
|
const key = String(rule);
|
|
2877
3788
|
stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
|
|
2878
3789
|
}
|
|
2879
|
-
|
|
3790
|
+
m.outcomeStats = stats;
|
|
2880
3791
|
await this.persist();
|
|
2881
3792
|
}
|
|
2882
3793
|
async drain() {
|
|
2883
3794
|
await this.writeQueue;
|
|
2884
3795
|
}
|
|
2885
3796
|
async get(id) {
|
|
2886
|
-
|
|
3797
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3798
|
+
if (!m) return null;
|
|
3799
|
+
const workflow = await this.loadWorkflowFile(id);
|
|
3800
|
+
if (!workflow) return null;
|
|
3801
|
+
return { ...m, workflow };
|
|
2887
3802
|
}
|
|
2888
3803
|
async list(filters) {
|
|
2889
|
-
let
|
|
3804
|
+
let filtered = this.meta;
|
|
2890
3805
|
if (filters?.platform) {
|
|
2891
|
-
|
|
3806
|
+
filtered = filtered.filter((m) => m.platform === filters.platform);
|
|
2892
3807
|
}
|
|
2893
3808
|
if (filters?.tags && filters.tags.length > 0) {
|
|
2894
|
-
|
|
3809
|
+
filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
|
|
2895
3810
|
}
|
|
2896
|
-
|
|
3811
|
+
const results = await Promise.all(
|
|
3812
|
+
filtered.map(async (m) => {
|
|
3813
|
+
const workflow = await this.loadWorkflowFile(m.id);
|
|
3814
|
+
if (!workflow) return null;
|
|
3815
|
+
return { ...m, workflow };
|
|
3816
|
+
})
|
|
3817
|
+
);
|
|
3818
|
+
return results.filter((r) => r !== null);
|
|
2897
3819
|
}
|
|
2898
3820
|
deduplicateFailurePatterns(patterns) {
|
|
2899
3821
|
if (!patterns?.length) return void 0;
|
|
@@ -2908,12 +3830,98 @@ var FileLibrary = class {
|
|
|
2908
3830
|
}
|
|
2909
3831
|
return [...map.values()];
|
|
2910
3832
|
}
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
3833
|
+
// ── Cross-process file locking ────────────────────────────────────────────
|
|
3834
|
+
// Uses O_EXCL (exclusive create) which is atomic on POSIX and Windows NTFS.
|
|
3835
|
+
// Protects the read-modify-write cycle in persist() from concurrent writers
|
|
3836
|
+
// in separate OS processes (e.g. MCP server + CLI running simultaneously).
|
|
3837
|
+
get lockPath() {
|
|
3838
|
+
return (0, import_node_path7.join)(this.dir, ".index.lock");
|
|
3839
|
+
}
|
|
3840
|
+
async acquireLock(timeoutMs = 3e3) {
|
|
3841
|
+
const deadline = Date.now() + timeoutMs;
|
|
3842
|
+
let delayMs = 10;
|
|
3843
|
+
while (true) {
|
|
3844
|
+
try {
|
|
3845
|
+
const fh = await (0, import_promises4.open)(this.lockPath, "wx");
|
|
3846
|
+
await fh.writeFile(String(process.pid));
|
|
3847
|
+
await fh.close();
|
|
3848
|
+
return async () => {
|
|
3849
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3850
|
+
});
|
|
3851
|
+
};
|
|
3852
|
+
} catch {
|
|
3853
|
+
try {
|
|
3854
|
+
const content = await (0, import_promises4.readFile)(this.lockPath, "utf-8");
|
|
3855
|
+
const lockPid = parseInt(content.trim(), 10);
|
|
3856
|
+
const fileStat = await (0, import_promises4.stat)(this.lockPath);
|
|
3857
|
+
const ageMs = Date.now() - fileStat.mtimeMs;
|
|
3858
|
+
if (ageMs > 1e4) {
|
|
3859
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3860
|
+
});
|
|
3861
|
+
continue;
|
|
3862
|
+
}
|
|
3863
|
+
if (!isNaN(lockPid)) {
|
|
3864
|
+
try {
|
|
3865
|
+
process.kill(lockPid, 0);
|
|
3866
|
+
} catch {
|
|
3867
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3868
|
+
});
|
|
3869
|
+
continue;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
} catch {
|
|
3873
|
+
continue;
|
|
3874
|
+
}
|
|
3875
|
+
if (Date.now() > deadline) {
|
|
3876
|
+
return async () => {
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
3880
|
+
delayMs = Math.min(delayMs * 1.5, 200);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
/**
|
|
3885
|
+
* Direct write used only during migration (before writeQueue is needed).
|
|
3886
|
+
*/
|
|
3887
|
+
async persistNow() {
|
|
3888
|
+
const releaseLock = await this.acquireLock();
|
|
3889
|
+
try {
|
|
3890
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
2914
3891
|
const tmpPath = `${indexPath}.tmp`;
|
|
2915
|
-
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.
|
|
3892
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
2916
3893
|
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3894
|
+
} finally {
|
|
3895
|
+
await releaseLock();
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
persist() {
|
|
3899
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
3900
|
+
const releaseLock = await this.acquireLock();
|
|
3901
|
+
try {
|
|
3902
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3903
|
+
let onDisk = [];
|
|
3904
|
+
try {
|
|
3905
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3906
|
+
const parsed = JSON.parse(raw);
|
|
3907
|
+
if (Array.isArray(parsed)) {
|
|
3908
|
+
onDisk = parsed.filter(isValidMeta);
|
|
3909
|
+
}
|
|
3910
|
+
} catch {
|
|
3911
|
+
}
|
|
3912
|
+
const ourIds = new Set(this.meta.map((m) => m.id));
|
|
3913
|
+
const external = onDisk.filter((m) => !ourIds.has(m.id));
|
|
3914
|
+
let merged = [...this.meta, ...external];
|
|
3915
|
+
if (merged.length > MAX_LIBRARY_SIZE) {
|
|
3916
|
+
merged.sort((a, b) => evictionScore(b) - evictionScore(a));
|
|
3917
|
+
merged = merged.slice(0, MAX_LIBRARY_SIZE);
|
|
3918
|
+
}
|
|
3919
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
3920
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
3921
|
+
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3922
|
+
} finally {
|
|
3923
|
+
await releaseLock();
|
|
3924
|
+
}
|
|
2917
3925
|
});
|
|
2918
3926
|
return this.writeQueue;
|
|
2919
3927
|
}
|
|
@@ -2935,6 +3943,19 @@ var SECRET_PATTERNS = [
|
|
|
2935
3943
|
/AIza[a-zA-Z0-9_-]{35}/,
|
|
2936
3944
|
/AKIA[A-Z0-9]{16}/
|
|
2937
3945
|
];
|
|
3946
|
+
var SECRET_PREFIXES = ["sk-", "ghp_", "xoxb-", "AIza", "AKIA"];
|
|
3947
|
+
function collectExpressionStrings(obj, out = []) {
|
|
3948
|
+
if (typeof obj === "string") {
|
|
3949
|
+
if (obj.includes("={{")) out.push(obj);
|
|
3950
|
+
} else if (Array.isArray(obj)) {
|
|
3951
|
+
for (const item of obj) collectExpressionStrings(item, out);
|
|
3952
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
3953
|
+
for (const val of Object.values(obj)) {
|
|
3954
|
+
collectExpressionStrings(val, out);
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
return out;
|
|
3958
|
+
}
|
|
2938
3959
|
function assessTemplateSafety(workflow) {
|
|
2939
3960
|
const reasons = [];
|
|
2940
3961
|
let worst = "safe";
|
|
@@ -2957,6 +3978,15 @@ function assessTemplateSafety(workflow) {
|
|
|
2957
3978
|
break;
|
|
2958
3979
|
}
|
|
2959
3980
|
}
|
|
3981
|
+
const expressions = collectExpressionStrings(node.parameters);
|
|
3982
|
+
for (const expr of expressions) {
|
|
3983
|
+
for (const prefix of SECRET_PREFIXES) {
|
|
3984
|
+
if (expr.includes(prefix)) {
|
|
3985
|
+
escalate("review", `Node "${node.name}" has an expression containing credential-like prefix "${prefix}"`);
|
|
3986
|
+
break;
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
2960
3990
|
}
|
|
2961
3991
|
return { trustLevel: worst, reasons };
|
|
2962
3992
|
}
|
|
@@ -3014,12 +4044,26 @@ var TemplateSyncer = class {
|
|
|
3014
4044
|
}
|
|
3015
4045
|
return progress;
|
|
3016
4046
|
}
|
|
4047
|
+
async fetchWithBackoff(url, maxRetries = 3) {
|
|
4048
|
+
let delayMs = DELAY_BETWEEN_FETCHES_MS;
|
|
4049
|
+
let lastResponse;
|
|
4050
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
4051
|
+
lastResponse = await fetch(url);
|
|
4052
|
+
if (lastResponse.status !== 429 && lastResponse.status !== 503) return lastResponse;
|
|
4053
|
+
if (attempt === maxRetries) break;
|
|
4054
|
+
const retryAfterHeader = lastResponse.headers.get("Retry-After");
|
|
4055
|
+
const waitMs = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : delayMs * Math.pow(2, attempt);
|
|
4056
|
+
this.logger.warn(`HTTP ${lastResponse.status} from template API, retrying in ${waitMs}ms`, { url, attempt });
|
|
4057
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
4058
|
+
}
|
|
4059
|
+
return lastResponse;
|
|
4060
|
+
}
|
|
3017
4061
|
async fetchTemplateIds(max, progress) {
|
|
3018
4062
|
const ids = [];
|
|
3019
4063
|
let page = 1;
|
|
3020
4064
|
while (ids.length < max) {
|
|
3021
4065
|
const url = `${N8N_TEMPLATE_API}/search?page=${page}&rows=${PAGE_SIZE}`;
|
|
3022
|
-
const response = await
|
|
4066
|
+
const response = await this.fetchWithBackoff(url);
|
|
3023
4067
|
if (!response.ok) break;
|
|
3024
4068
|
const data = await response.json();
|
|
3025
4069
|
progress.total = Math.min(data.totalWorkflows, max);
|
|
@@ -3039,7 +4083,7 @@ var TemplateSyncer = class {
|
|
|
3039
4083
|
}
|
|
3040
4084
|
async processTemplate(id, progress) {
|
|
3041
4085
|
const url = `${N8N_TEMPLATE_API}/workflows/${id}`;
|
|
3042
|
-
const response = await
|
|
4086
|
+
const response = await this.fetchWithBackoff(url);
|
|
3043
4087
|
if (!response.ok) return;
|
|
3044
4088
|
const data = await response.json();
|
|
3045
4089
|
const templateMeta = data.workflow;
|