@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/cli.cjs
CHANGED
|
@@ -28,7 +28,13 @@ var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
|
28
28
|
|
|
29
29
|
// src/utils/uuid.ts
|
|
30
30
|
function generateUUID() {
|
|
31
|
-
|
|
31
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
32
|
+
return crypto.randomUUID();
|
|
33
|
+
}
|
|
34
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
35
|
+
const r = Math.random() * 16 | 0;
|
|
36
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
37
|
+
});
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
// src/library/null-library.ts
|
|
@@ -84,7 +90,26 @@ var ProviderError = class extends KairosError {
|
|
|
84
90
|
}
|
|
85
91
|
};
|
|
86
92
|
|
|
93
|
+
// src/errors/guard-error.ts
|
|
94
|
+
var GuardError = class extends KairosError {
|
|
95
|
+
constructor(message) {
|
|
96
|
+
super(message);
|
|
97
|
+
this.name = "GuardError";
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
87
101
|
// src/utils/retry.ts
|
|
102
|
+
function isTransientNetworkError(err) {
|
|
103
|
+
const TRANSIENT_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "ENOTFOUND", "ECONNABORTED"]);
|
|
104
|
+
let current = err;
|
|
105
|
+
for (let i = 0; i < 4; i++) {
|
|
106
|
+
if (current === null || typeof current !== "object") break;
|
|
107
|
+
const code = current.code;
|
|
108
|
+
if (typeof code === "string" && TRANSIENT_CODES.has(code)) return true;
|
|
109
|
+
current = current.cause;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
88
113
|
async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
|
|
89
114
|
let lastError;
|
|
90
115
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
@@ -109,6 +134,7 @@ function fetchWithTimeout(url, init, timeoutMs) {
|
|
|
109
134
|
|
|
110
135
|
// src/providers/n8n/api-client.ts
|
|
111
136
|
var EXECUTION_LIMIT_CAP = 100;
|
|
137
|
+
var N8N_API_PAGE_SIZE = 250;
|
|
112
138
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
113
139
|
var RETRY_ATTEMPTS = 3;
|
|
114
140
|
var RETRY_DELAY_MS = 1e3;
|
|
@@ -117,6 +143,17 @@ var N8nApiClient = class {
|
|
|
117
143
|
this.baseUrl = baseUrl;
|
|
118
144
|
this.apiKey = apiKey;
|
|
119
145
|
this.logger = logger;
|
|
146
|
+
if (!baseUrl || typeof baseUrl !== "string") {
|
|
147
|
+
throw new GuardError("N8nApiClient: baseUrl must be a non-empty string");
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
new URL(baseUrl);
|
|
151
|
+
} catch {
|
|
152
|
+
throw new GuardError(`N8nApiClient: baseUrl is not a valid URL: "${baseUrl}"`);
|
|
153
|
+
}
|
|
154
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
155
|
+
throw new GuardError("N8nApiClient: apiKey must be a non-empty string");
|
|
156
|
+
}
|
|
120
157
|
}
|
|
121
158
|
baseUrl;
|
|
122
159
|
apiKey;
|
|
@@ -126,7 +163,12 @@ var N8nApiClient = class {
|
|
|
126
163
|
this.logger.debug(`n8n ${method} ${path}`);
|
|
127
164
|
const isSafe = method === "GET";
|
|
128
165
|
if (!isSafe) {
|
|
129
|
-
return
|
|
166
|
+
return withRetry(
|
|
167
|
+
() => this.singleRequest(url, method, path, body),
|
|
168
|
+
2,
|
|
169
|
+
RETRY_DELAY_MS,
|
|
170
|
+
isTransientNetworkError
|
|
171
|
+
);
|
|
130
172
|
}
|
|
131
173
|
return withRetry(
|
|
132
174
|
() => this.singleRequest(url, method, path, body),
|
|
@@ -181,7 +223,7 @@ var N8nApiClient = class {
|
|
|
181
223
|
}
|
|
182
224
|
async listWorkflows() {
|
|
183
225
|
const all = [];
|
|
184
|
-
let path =
|
|
226
|
+
let path = `/workflows?limit=${N8N_API_PAGE_SIZE}`;
|
|
185
227
|
for (; ; ) {
|
|
186
228
|
const response = await this.request("GET", path);
|
|
187
229
|
for (const w of response.data) {
|
|
@@ -195,7 +237,7 @@ var N8nApiClient = class {
|
|
|
195
237
|
});
|
|
196
238
|
}
|
|
197
239
|
if (!response.nextCursor) break;
|
|
198
|
-
path = `/workflows?limit
|
|
240
|
+
path = `/workflows?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
199
241
|
}
|
|
200
242
|
return all;
|
|
201
243
|
}
|
|
@@ -225,14 +267,14 @@ var N8nApiClient = class {
|
|
|
225
267
|
}
|
|
226
268
|
async listTags() {
|
|
227
269
|
const all = [];
|
|
228
|
-
let path =
|
|
270
|
+
let path = `/tags?limit=${N8N_API_PAGE_SIZE}`;
|
|
229
271
|
for (; ; ) {
|
|
230
272
|
const response = await this.request("GET", path);
|
|
231
273
|
for (const t of response.data) {
|
|
232
274
|
all.push({ id: t.id, name: t.name });
|
|
233
275
|
}
|
|
234
276
|
if (!response.nextCursor) break;
|
|
235
|
-
path = `/tags?limit
|
|
277
|
+
path = `/tags?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
|
|
236
278
|
}
|
|
237
279
|
return all;
|
|
238
280
|
}
|
|
@@ -256,6 +298,32 @@ var N8nApiClient = class {
|
|
|
256
298
|
return [];
|
|
257
299
|
}
|
|
258
300
|
}
|
|
301
|
+
async triggerManual(workflowId) {
|
|
302
|
+
const raw = await this.request("POST", `/workflows/${workflowId}/run`);
|
|
303
|
+
const inner = raw["data"];
|
|
304
|
+
const execId = inner?.["executionId"] ?? raw["executionId"];
|
|
305
|
+
if (execId === void 0 || execId === null) {
|
|
306
|
+
throw new ProviderError(
|
|
307
|
+
`n8n trigger response missing executionId \u2014 got: ${JSON.stringify(raw)}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return String(execId);
|
|
311
|
+
}
|
|
312
|
+
async triggerWebhookTest(path) {
|
|
313
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
314
|
+
const url = `${this.baseUrl.replace(/\/$/, "")}/webhook-test${cleanPath}`;
|
|
315
|
+
this.logger.debug(`n8n POST webhook-test ${cleanPath}`);
|
|
316
|
+
try {
|
|
317
|
+
const response = await fetchWithTimeout(
|
|
318
|
+
url,
|
|
319
|
+
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) },
|
|
320
|
+
REQUEST_TIMEOUT_MS
|
|
321
|
+
);
|
|
322
|
+
return response.status;
|
|
323
|
+
} catch (err) {
|
|
324
|
+
throw new ProviderError(`Webhook test request failed for path "${path}"`, err);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
259
327
|
mapExecution(e) {
|
|
260
328
|
return {
|
|
261
329
|
id: e.id,
|
|
@@ -303,15 +371,9 @@ var N8nFieldStripper = class {
|
|
|
303
371
|
}
|
|
304
372
|
};
|
|
305
373
|
|
|
306
|
-
// src/errors/guard-error.ts
|
|
307
|
-
var GuardError = class extends KairosError {
|
|
308
|
-
constructor(message) {
|
|
309
|
-
super(message);
|
|
310
|
-
this.name = "GuardError";
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
|
|
314
374
|
// src/providers/n8n/provider.ts
|
|
375
|
+
var SMOKE_TEST_TIMEOUT_MS = 3e4;
|
|
376
|
+
var SMOKE_TEST_POLL_INTERVAL_MS = 1e3;
|
|
315
377
|
var N8nProvider = class {
|
|
316
378
|
constructor(client, stripper) {
|
|
317
379
|
this.client = client;
|
|
@@ -373,6 +435,71 @@ var N8nProvider = class {
|
|
|
373
435
|
async untag(workflowId, tagIds) {
|
|
374
436
|
await this.client.untagWorkflow(workflowId, tagIds);
|
|
375
437
|
}
|
|
438
|
+
async smokeTest(workflowId, workflow) {
|
|
439
|
+
const start = Date.now();
|
|
440
|
+
const trigger = this.detectTrigger(workflow);
|
|
441
|
+
if (trigger.type === "unsupported") {
|
|
442
|
+
return { status: "not-applicable", triggerType: "not-applicable" };
|
|
443
|
+
}
|
|
444
|
+
if (trigger.type === "manual") {
|
|
445
|
+
let executionId;
|
|
446
|
+
try {
|
|
447
|
+
executionId = await this.client.triggerManual(workflowId);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
return { status: "error", triggerType: "manual", durationMs: Date.now() - start, error: String(err) };
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const execution = await this.pollExecution(executionId);
|
|
453
|
+
const durationMs = Date.now() - start;
|
|
454
|
+
if (execution.status === "success") {
|
|
455
|
+
return { status: "passed", triggerType: "manual", executionId, durationMs };
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
status: "failed",
|
|
459
|
+
triggerType: "manual",
|
|
460
|
+
executionId,
|
|
461
|
+
durationMs,
|
|
462
|
+
error: `Execution ended with status: ${execution.status}`
|
|
463
|
+
};
|
|
464
|
+
} catch (err) {
|
|
465
|
+
return { status: "error", triggerType: "manual", executionId, durationMs: Date.now() - start, error: String(err) };
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
const statusCode = await this.client.triggerWebhookTest(trigger.path);
|
|
470
|
+
const durationMs = Date.now() - start;
|
|
471
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
472
|
+
return { status: "passed", triggerType: "webhook", durationMs };
|
|
473
|
+
}
|
|
474
|
+
return { status: "failed", triggerType: "webhook", durationMs, error: `Webhook returned HTTP ${statusCode}` };
|
|
475
|
+
} catch (err) {
|
|
476
|
+
return { status: "error", triggerType: "webhook", durationMs: Date.now() - start, error: String(err) };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
detectTrigger(workflow) {
|
|
480
|
+
for (const node of workflow.nodes) {
|
|
481
|
+
if (node.type === "n8n-nodes-base.manualTrigger") return { type: "manual" };
|
|
482
|
+
if (node.type === "n8n-nodes-base.webhook") {
|
|
483
|
+
const params = node.parameters;
|
|
484
|
+
const path = typeof params?.["path"] === "string" ? params["path"] : "webhook";
|
|
485
|
+
return { type: "webhook", path };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return { type: "unsupported" };
|
|
489
|
+
}
|
|
490
|
+
async pollExecution(executionId) {
|
|
491
|
+
const deadline = Date.now() + SMOKE_TEST_TIMEOUT_MS;
|
|
492
|
+
for (; ; ) {
|
|
493
|
+
const execution = await this.client.getExecution(executionId);
|
|
494
|
+
if (execution.status !== "running" && execution.status !== "waiting") {
|
|
495
|
+
return execution;
|
|
496
|
+
}
|
|
497
|
+
const remaining = deadline - Date.now();
|
|
498
|
+
if (remaining <= 0) break;
|
|
499
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(SMOKE_TEST_POLL_INTERVAL_MS, remaining)));
|
|
500
|
+
}
|
|
501
|
+
throw new ProviderError(`Smoke test: execution ${executionId} did not complete within ${SMOKE_TEST_TIMEOUT_MS}ms`);
|
|
502
|
+
}
|
|
376
503
|
};
|
|
377
504
|
|
|
378
505
|
// src/validation/registry.ts
|
|
@@ -475,6 +602,14 @@ var NodeRegistry = class {
|
|
|
475
602
|
if (!def) return true;
|
|
476
603
|
return def.safeTypeVersions.includes(version);
|
|
477
604
|
}
|
|
605
|
+
// Returns true when the version is a positive integer greater than the highest
|
|
606
|
+
// known safe version — indicates a newer release rather than a bad value.
|
|
607
|
+
isVersionNewer(type, version) {
|
|
608
|
+
const def = this.byType.get(type);
|
|
609
|
+
if (!def || def.safeTypeVersions.length === 0) return false;
|
|
610
|
+
const max = Math.max(...def.safeTypeVersions);
|
|
611
|
+
return Number.isInteger(version) && version > max;
|
|
612
|
+
}
|
|
478
613
|
getRequiredParams(type) {
|
|
479
614
|
return this.byType.get(type)?.requiredParams ?? [];
|
|
480
615
|
}
|
|
@@ -524,6 +659,17 @@ var N8nValidator = class {
|
|
|
524
659
|
this.checkRule21(workflow, issues);
|
|
525
660
|
this.checkRule22(workflow, issues);
|
|
526
661
|
this.checkRule23(workflow, issues);
|
|
662
|
+
this.checkRule24(workflow, issues);
|
|
663
|
+
this.checkRule25(workflow, issues);
|
|
664
|
+
this.checkRule26(workflow, issues);
|
|
665
|
+
this.checkRule27(workflow, issues);
|
|
666
|
+
this.checkRule28(workflow, issues);
|
|
667
|
+
this.checkRule29(workflow, issues);
|
|
668
|
+
this.checkRule30(workflow, issues);
|
|
669
|
+
this.checkRule31(workflow, issues);
|
|
670
|
+
this.checkRule32(workflow, issues);
|
|
671
|
+
this.checkRule33(workflow, issues);
|
|
672
|
+
this.checkRule34(workflow, issues);
|
|
527
673
|
if (Array.isArray(workflow.nodes)) {
|
|
528
674
|
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
529
675
|
for (const issue of issues) {
|
|
@@ -656,10 +802,14 @@ var N8nValidator = class {
|
|
|
656
802
|
checkRule11(w, issues) {
|
|
657
803
|
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
658
804
|
const reachable = /* @__PURE__ */ new Set();
|
|
659
|
-
|
|
805
|
+
const aiSubNodeSources = /* @__PURE__ */ new Set();
|
|
806
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
660
807
|
if (typeof outputs !== "object" || outputs === null) continue;
|
|
661
|
-
|
|
808
|
+
let hasAiPort = false;
|
|
809
|
+
for (const [portName, portGroup] of Object.entries(outputs)) {
|
|
662
810
|
if (!Array.isArray(portGroup)) continue;
|
|
811
|
+
const isAiPort = portName.startsWith("ai_");
|
|
812
|
+
if (isAiPort) hasAiPort = true;
|
|
663
813
|
for (const targets of portGroup) {
|
|
664
814
|
if (!Array.isArray(targets)) continue;
|
|
665
815
|
for (const target of targets) {
|
|
@@ -668,10 +818,13 @@ var N8nValidator = class {
|
|
|
668
818
|
}
|
|
669
819
|
}
|
|
670
820
|
}
|
|
821
|
+
if (hasAiPort) aiSubNodeSources.add(sourceName);
|
|
671
822
|
}
|
|
672
823
|
for (const node of w.nodes) {
|
|
673
824
|
if (node.type.includes("stickyNote")) continue;
|
|
674
|
-
if (
|
|
825
|
+
if (this.isTriggerNode(node)) continue;
|
|
826
|
+
if (aiSubNodeSources.has(node.name)) continue;
|
|
827
|
+
if (!reachable.has(node.name)) {
|
|
675
828
|
this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
|
|
676
829
|
}
|
|
677
830
|
}
|
|
@@ -768,19 +921,22 @@ var N8nValidator = class {
|
|
|
768
921
|
}
|
|
769
922
|
}
|
|
770
923
|
}
|
|
771
|
-
// Rule 19 (WARN): typeVersion is within known safe range for registered node types
|
|
924
|
+
// Rule 19 (WARN): typeVersion is within known safe range for registered node types.
|
|
925
|
+
// In lenient mode (KAIROS_REGISTRY_STRICT != 'true'), versions higher than the known
|
|
926
|
+
// max are allowed — they likely represent newer n8n releases Kairos hasn't catalogued yet.
|
|
772
927
|
checkRule19(w, issues) {
|
|
773
928
|
if (!Array.isArray(w.nodes)) return;
|
|
929
|
+
const strict = process.env["KAIROS_REGISTRY_STRICT"] === "true";
|
|
774
930
|
for (const node of w.nodes) {
|
|
775
931
|
if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
|
|
776
|
-
if (
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
932
|
+
if (this.registry.isVersionSafe(node.type, node.typeVersion)) continue;
|
|
933
|
+
if (!strict && this.registry.isVersionNewer(node.type, node.typeVersion)) continue;
|
|
934
|
+
this.warn(
|
|
935
|
+
issues,
|
|
936
|
+
19,
|
|
937
|
+
`Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
|
|
938
|
+
node.id
|
|
939
|
+
);
|
|
784
940
|
}
|
|
785
941
|
}
|
|
786
942
|
// Rule 20 (WARN): cycle detection — no node should be reachable from itself
|
|
@@ -829,6 +985,27 @@ var N8nValidator = class {
|
|
|
829
985
|
}
|
|
830
986
|
}
|
|
831
987
|
}
|
|
988
|
+
// Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
|
|
989
|
+
checkRule21(w, issues) {
|
|
990
|
+
if (!Array.isArray(w.nodes)) return;
|
|
991
|
+
const webhooksNeedingResponse = w.nodes.filter((n) => {
|
|
992
|
+
if (!n.type.includes("webhook")) return false;
|
|
993
|
+
const params = n.parameters;
|
|
994
|
+
return params?.responseMode === "responseNode";
|
|
995
|
+
});
|
|
996
|
+
if (webhooksNeedingResponse.length === 0) return;
|
|
997
|
+
const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
|
|
998
|
+
if (!hasRespondNode) {
|
|
999
|
+
for (const wh of webhooksNeedingResponse) {
|
|
1000
|
+
this.warn(
|
|
1001
|
+
issues,
|
|
1002
|
+
21,
|
|
1003
|
+
`Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
|
|
1004
|
+
wh.id
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
832
1009
|
// Rule 22 (WARN): check requiredParams from registry
|
|
833
1010
|
checkRule22(w, issues) {
|
|
834
1011
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -867,23 +1044,232 @@ var N8nValidator = class {
|
|
|
867
1044
|
}
|
|
868
1045
|
}
|
|
869
1046
|
}
|
|
870
|
-
// Rule
|
|
871
|
-
|
|
1047
|
+
// Rule 24 (WARN): deprecated accessor syntax in expressions
|
|
1048
|
+
checkRule24(w, issues) {
|
|
872
1049
|
if (!Array.isArray(w.nodes)) return;
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1050
|
+
const deprecated = /\$node\s*\[/;
|
|
1051
|
+
for (const node of w.nodes) {
|
|
1052
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1053
|
+
if (deprecated.test(expr)) {
|
|
1054
|
+
this.warn(
|
|
1055
|
+
issues,
|
|
1056
|
+
24,
|
|
1057
|
+
`Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
|
|
1058
|
+
node.id
|
|
1059
|
+
);
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
// Rule 25 (WARN): wrong item index assumptions in expressions
|
|
1066
|
+
checkRule25(w, issues) {
|
|
1067
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1068
|
+
const itemIndex = /\$json\s*\.\s*items\s*\[/;
|
|
1069
|
+
for (const node of w.nodes) {
|
|
1070
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1071
|
+
if (itemIndex.test(expr)) {
|
|
1072
|
+
this.warn(
|
|
1073
|
+
issues,
|
|
1074
|
+
25,
|
|
1075
|
+
`Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
|
|
1076
|
+
node.id
|
|
1077
|
+
);
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// Rule 26 (WARN): missing .first() or .all() on node references
|
|
1084
|
+
checkRule26(w, issues) {
|
|
1085
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1086
|
+
const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
|
|
1087
|
+
for (const node of w.nodes) {
|
|
1088
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1089
|
+
if (bareRef.test(expr)) {
|
|
1090
|
+
this.warn(
|
|
1091
|
+
issues,
|
|
1092
|
+
26,
|
|
1093
|
+
`Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
|
|
1094
|
+
node.id
|
|
1095
|
+
);
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
extractExpressions(params) {
|
|
1102
|
+
const expressions = [];
|
|
1103
|
+
const walk = (val) => {
|
|
1104
|
+
if (typeof val === "string") {
|
|
1105
|
+
if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
|
|
1106
|
+
expressions.push(val);
|
|
1107
|
+
}
|
|
1108
|
+
} else if (Array.isArray(val)) {
|
|
1109
|
+
for (const item of val) walk(item);
|
|
1110
|
+
} else if (val !== null && typeof val === "object") {
|
|
1111
|
+
for (const v of Object.values(val)) walk(v);
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
walk(params);
|
|
1115
|
+
return expressions;
|
|
1116
|
+
}
|
|
1117
|
+
// Rule 27 (WARN): httpRequest URL is a placeholder
|
|
1118
|
+
checkRule27(w, issues) {
|
|
1119
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1120
|
+
const PLACEHOLDER_RE = [
|
|
1121
|
+
/^https?:\/\/example\.com/i,
|
|
1122
|
+
/your[-_]?(api[-_]?)?url/i,
|
|
1123
|
+
/^https?:\/\/$/,
|
|
1124
|
+
/^<.+>$/,
|
|
1125
|
+
/placeholder/i
|
|
1126
|
+
];
|
|
1127
|
+
for (const node of w.nodes) {
|
|
1128
|
+
if (node.type !== "n8n-nodes-base.httpRequest") continue;
|
|
1129
|
+
const params = node.parameters;
|
|
1130
|
+
const url = params?.["url"];
|
|
1131
|
+
if (typeof url !== "string" || url.trim() === "") continue;
|
|
1132
|
+
if (PLACEHOLDER_RE.some((re) => re.test(url.trim()))) {
|
|
882
1133
|
this.warn(
|
|
883
1134
|
issues,
|
|
884
|
-
|
|
885
|
-
`
|
|
886
|
-
|
|
1135
|
+
27,
|
|
1136
|
+
`Node "${node.name}" httpRequest URL appears to be a placeholder: "${url}" \u2014 replace with your actual endpoint`,
|
|
1137
|
+
node.id
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
// Rule 28 (WARN): code node with empty or comment-only code
|
|
1143
|
+
checkRule28(w, issues) {
|
|
1144
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1145
|
+
for (const node of w.nodes) {
|
|
1146
|
+
if (node.type !== "n8n-nodes-base.code") continue;
|
|
1147
|
+
const params = node.parameters;
|
|
1148
|
+
const jsCode = typeof params?.["jsCode"] === "string" ? params["jsCode"] : "";
|
|
1149
|
+
const pythonCode = typeof params?.["pythonCode"] === "string" ? params["pythonCode"] : "";
|
|
1150
|
+
const code = jsCode || pythonCode;
|
|
1151
|
+
const stripped = code.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/#[^\n]*/g, "").trim();
|
|
1152
|
+
if (!stripped) {
|
|
1153
|
+
this.warn(issues, 28, `Node "${node.name}" code node has no executable code`, node.id);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
// Rule 29 (WARN): slack node message operation missing channel
|
|
1158
|
+
checkRule29(w, issues) {
|
|
1159
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1160
|
+
for (const node of w.nodes) {
|
|
1161
|
+
if (node.type !== "n8n-nodes-base.slack") continue;
|
|
1162
|
+
const params = node.parameters;
|
|
1163
|
+
const resource = params?.["resource"];
|
|
1164
|
+
const operation = params?.["operation"];
|
|
1165
|
+
const isMessageOp = resource === "message" || operation === "sendMessage" || operation === "post";
|
|
1166
|
+
if (!isMessageOp) continue;
|
|
1167
|
+
const channel = params?.["channel"] ?? params?.["channelId"];
|
|
1168
|
+
const rlValue = typeof channel === "object" && channel !== null ? channel["value"] : void 0;
|
|
1169
|
+
const isEmpty = channel === void 0 || channel === null || typeof channel === "string" && channel.trim() === "" || typeof channel === "object" && (!rlValue || typeof rlValue === "string" && rlValue.trim() === "");
|
|
1170
|
+
if (isEmpty) {
|
|
1171
|
+
this.warn(issues, 29, `Node "${node.name}" Slack message has no channel specified`, node.id);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
// Rule 30 (WARN): gmail node send operation missing recipient
|
|
1176
|
+
checkRule30(w, issues) {
|
|
1177
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1178
|
+
for (const node of w.nodes) {
|
|
1179
|
+
if (node.type !== "n8n-nodes-base.gmail") continue;
|
|
1180
|
+
const params = node.parameters;
|
|
1181
|
+
const operation = params?.["operation"];
|
|
1182
|
+
if (operation !== "send") continue;
|
|
1183
|
+
const to = params?.["to"] ?? params?.["toList"];
|
|
1184
|
+
const isEmpty = to === void 0 || to === null || typeof to === "string" && to.trim() === "" || Array.isArray(to) && to.length === 0;
|
|
1185
|
+
if (isEmpty) {
|
|
1186
|
+
this.warn(issues, 30, `Node "${node.name}" gmail send has no recipient (to) specified`, node.id);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
// Rule 31 (WARN): if node with empty conditions
|
|
1191
|
+
checkRule31(w, issues) {
|
|
1192
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1193
|
+
for (const node of w.nodes) {
|
|
1194
|
+
if (node.type !== "n8n-nodes-base.if") continue;
|
|
1195
|
+
const params = node.parameters;
|
|
1196
|
+
const conditions = params?.["conditions"];
|
|
1197
|
+
if (conditions === void 0 || conditions === null) {
|
|
1198
|
+
this.warn(issues, 31, `Node "${node.name}" if node has no conditions defined`, node.id);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (typeof conditions === "object" && !Array.isArray(conditions)) {
|
|
1202
|
+
const conds = conditions["conditions"];
|
|
1203
|
+
if (!Array.isArray(conds) || conds.length === 0) {
|
|
1204
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1205
|
+
}
|
|
1206
|
+
} else if (Array.isArray(conditions) && conditions.length === 0) {
|
|
1207
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
// Rule 32 (WARN): set node with no assignments
|
|
1212
|
+
checkRule32(w, issues) {
|
|
1213
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1214
|
+
for (const node of w.nodes) {
|
|
1215
|
+
if (node.type !== "n8n-nodes-base.set") continue;
|
|
1216
|
+
const params = node.parameters;
|
|
1217
|
+
const assignmentsObj = params?.["assignments"];
|
|
1218
|
+
const assignmentsArr = assignmentsObj?.["assignments"];
|
|
1219
|
+
const valuesObj = params?.["values"];
|
|
1220
|
+
const hasV1 = valuesObj && Object.values(valuesObj).some((v) => Array.isArray(v) && v.length > 0);
|
|
1221
|
+
const hasV3 = Array.isArray(assignmentsArr) && assignmentsArr.length > 0;
|
|
1222
|
+
if (!hasV1 && !hasV3) {
|
|
1223
|
+
this.warn(
|
|
1224
|
+
issues,
|
|
1225
|
+
32,
|
|
1226
|
+
`Node "${node.name}" set node has no fields defined \u2014 it will pass data through unchanged`,
|
|
1227
|
+
node.id
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
// Rule 33 (WARN): scheduleTrigger with no schedule rules
|
|
1233
|
+
checkRule33(w, issues) {
|
|
1234
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1235
|
+
for (const node of w.nodes) {
|
|
1236
|
+
if (node.type !== "n8n-nodes-base.scheduleTrigger") continue;
|
|
1237
|
+
const params = node.parameters;
|
|
1238
|
+
const rule = params?.["rule"];
|
|
1239
|
+
const intervals = rule?.["interval"];
|
|
1240
|
+
if (!Array.isArray(intervals) || intervals.length === 0) {
|
|
1241
|
+
this.warn(issues, 33, `Node "${node.name}" scheduleTrigger has no schedule rules defined`, node.id);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
// Rule 34 (WARN): webhook path contains spaces, starts with slash, or looks like a full URL
|
|
1246
|
+
checkRule34(w, issues) {
|
|
1247
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1248
|
+
for (const node of w.nodes) {
|
|
1249
|
+
if (node.type !== "n8n-nodes-base.webhook") continue;
|
|
1250
|
+
const params = node.parameters;
|
|
1251
|
+
const path = params?.["path"];
|
|
1252
|
+
if (typeof path !== "string") continue;
|
|
1253
|
+
if (/\s/.test(path)) {
|
|
1254
|
+
this.warn(
|
|
1255
|
+
issues,
|
|
1256
|
+
34,
|
|
1257
|
+
`Node "${node.name}" webhook path contains spaces: "${path}" \u2014 use hyphens or underscores instead`,
|
|
1258
|
+
node.id
|
|
1259
|
+
);
|
|
1260
|
+
} else if (/^https?:\/\//i.test(path)) {
|
|
1261
|
+
this.warn(
|
|
1262
|
+
issues,
|
|
1263
|
+
34,
|
|
1264
|
+
`Node "${node.name}" webhook path looks like a full URL \u2014 it should be a relative path (e.g. "my-hook")`,
|
|
1265
|
+
node.id
|
|
1266
|
+
);
|
|
1267
|
+
} else if (path.startsWith("/")) {
|
|
1268
|
+
this.warn(
|
|
1269
|
+
issues,
|
|
1270
|
+
34,
|
|
1271
|
+
`Node "${node.name}" webhook path starts with "/" \u2014 n8n adds the leading slash automatically`,
|
|
1272
|
+
node.id
|
|
887
1273
|
);
|
|
888
1274
|
}
|
|
889
1275
|
}
|
|
@@ -954,9 +1340,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
|
|
|
954
1340
|
- Never reuse IDs, never use sequential fake IDs like "node-1"
|
|
955
1341
|
|
|
956
1342
|
### Credentials:
|
|
957
|
-
-
|
|
958
|
-
|
|
959
|
-
-
|
|
1343
|
+
- Each credential is keyed by its type string, with an object value containing id and name:
|
|
1344
|
+
"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack Credential" } }
|
|
1345
|
+
- Use "placeholder-id" as the id \u2014 users replace this with their real credential ID from n8n after deployment
|
|
1346
|
+
- The credentialsNeeded field in your response declares what credentials the user must configure
|
|
1347
|
+
- Never put API keys or tokens directly in node parameters when a credential type exists
|
|
960
1348
|
|
|
961
1349
|
### Node names:
|
|
962
1350
|
- All node names must be unique within the workflow
|
|
@@ -1003,6 +1391,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
|
|
|
1003
1391
|
|
|
1004
1392
|
---
|
|
1005
1393
|
|
|
1394
|
+
## EXPRESSION SYNTAX \u2014 how to reference upstream node data
|
|
1395
|
+
|
|
1396
|
+
### Accessing a field from an upstream node:
|
|
1397
|
+
- CORRECT: $('NodeName').item.json.field
|
|
1398
|
+
- WRONG: $node["NodeName"].json.field \u2190 deprecated accessor, fails at runtime (Rule 24)
|
|
1399
|
+
|
|
1400
|
+
### Accessing array items from $json:
|
|
1401
|
+
- CORRECT: $json.field \u2190 n8n auto-flattens items; each item is already a flat object
|
|
1402
|
+
- WRONG: $json.items[0].field \u2190 do not index into items[] (Rule 25)
|
|
1403
|
+
|
|
1404
|
+
### Calling node data \u2014 always qualify with .first() or .all():
|
|
1405
|
+
- CORRECT: $('NodeName').first().json.field \u2190 single item
|
|
1406
|
+
- CORRECT: $('NodeName').all() \u2190 array of all items
|
|
1407
|
+
- WRONG: $('NodeName').json \u2190 throws at runtime without .first() or .all() (Rule 26)
|
|
1408
|
+
|
|
1409
|
+
---
|
|
1410
|
+
|
|
1006
1411
|
## NODE CATALOG \u2014 exact type strings and safe typeVersions
|
|
1007
1412
|
|
|
1008
1413
|
### Triggers (always at least one required):
|
|
@@ -1102,6 +1507,17 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
|
|
|
1102
1507
|
5. At least one trigger node present
|
|
1103
1508
|
6. Every AI Agent has an ai_languageModel sub-node
|
|
1104
1509
|
7. settings block is complete with executionOrder: "v1"
|
|
1510
|
+
8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
|
|
1511
|
+
9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
|
|
1512
|
+
10. No bare $('NodeName').json \u2014 always use .first().json.field or .all()
|
|
1513
|
+
11. httpRequest URL is a real endpoint (not "example.com" or "YOUR_URL")
|
|
1514
|
+
12. code nodes contain actual logic \u2014 not empty or comment-only
|
|
1515
|
+
13. Slack message nodes have a channel specified (channelId or channel)
|
|
1516
|
+
14. Gmail send nodes have a recipient (to field non-empty)
|
|
1517
|
+
15. if nodes have at least one condition in conditions.conditions[]
|
|
1518
|
+
16. set nodes have at least one entry in assignments.assignments[]
|
|
1519
|
+
17. scheduleTrigger has at least one rule in rule.interval[]
|
|
1520
|
+
18. webhook path is relative (no spaces, no leading slash, no http://)
|
|
1105
1521
|
|
|
1106
1522
|
---
|
|
1107
1523
|
|
|
@@ -1118,7 +1534,7 @@ function scoreToMode(score) {
|
|
|
1118
1534
|
}
|
|
1119
1535
|
|
|
1120
1536
|
// src/validation/rule-metadata.ts
|
|
1121
|
-
var VALIDATOR_RULE_IDS = Array.from({ length:
|
|
1537
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 34 }, (_, i) => i + 1);
|
|
1122
1538
|
var RULE_PIPELINE_STAGES = {
|
|
1123
1539
|
1: "node_generation",
|
|
1124
1540
|
2: "node_generation",
|
|
@@ -1142,7 +1558,68 @@ var RULE_PIPELINE_STAGES = {
|
|
|
1142
1558
|
20: "connection_wiring",
|
|
1143
1559
|
21: "workflow_structure",
|
|
1144
1560
|
22: "workflow_structure",
|
|
1145
|
-
23: "node_generation"
|
|
1561
|
+
23: "node_generation",
|
|
1562
|
+
24: "expression_syntax",
|
|
1563
|
+
25: "expression_syntax",
|
|
1564
|
+
26: "expression_syntax",
|
|
1565
|
+
27: "node_generation",
|
|
1566
|
+
28: "node_generation",
|
|
1567
|
+
29: "node_generation",
|
|
1568
|
+
30: "node_generation",
|
|
1569
|
+
31: "node_generation",
|
|
1570
|
+
32: "node_generation",
|
|
1571
|
+
33: "node_generation",
|
|
1572
|
+
34: "node_generation"
|
|
1573
|
+
};
|
|
1574
|
+
var RULE_EXAMPLES = {
|
|
1575
|
+
17: {
|
|
1576
|
+
bad: '"credentials": { "slackOAuth2Api": "my-token" }',
|
|
1577
|
+
good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
|
|
1578
|
+
},
|
|
1579
|
+
24: {
|
|
1580
|
+
bad: '$node["Fetch Data"].json.email',
|
|
1581
|
+
good: "$('Fetch Data').item.json.email"
|
|
1582
|
+
},
|
|
1583
|
+
25: {
|
|
1584
|
+
bad: "$json.items[0].email",
|
|
1585
|
+
good: "$json.email"
|
|
1586
|
+
},
|
|
1587
|
+
26: {
|
|
1588
|
+
bad: "$('Fetch Data').json.email",
|
|
1589
|
+
good: "$('Fetch Data').first().json.email"
|
|
1590
|
+
},
|
|
1591
|
+
27: {
|
|
1592
|
+
bad: '"url": "https://example.com/api/data"',
|
|
1593
|
+
good: '"url": "https://api.yourservice.com/v1/endpoint"'
|
|
1594
|
+
},
|
|
1595
|
+
28: {
|
|
1596
|
+
bad: '"jsCode": "// TODO: implement this"',
|
|
1597
|
+
good: '"jsCode": "return items.map(item => ({ json: { result: item.json.value * 2 } }))"'
|
|
1598
|
+
},
|
|
1599
|
+
29: {
|
|
1600
|
+
bad: '"channelId": ""',
|
|
1601
|
+
good: '"channelId": { "__rl": true, "value": "C0123456789", "mode": "id" }'
|
|
1602
|
+
},
|
|
1603
|
+
30: {
|
|
1604
|
+
bad: '"operation": "send", "to": ""',
|
|
1605
|
+
good: '"operation": "send", "to": "recipient@example.com"'
|
|
1606
|
+
},
|
|
1607
|
+
31: {
|
|
1608
|
+
bad: '"conditions": { "combinator": "and", "conditions": [] }',
|
|
1609
|
+
good: '"conditions": { "combinator": "and", "conditions": [{ "leftValue": "={{ $json.status }}", "rightValue": "active", "operator": { "type": "string", "operation": "equals" } }] }'
|
|
1610
|
+
},
|
|
1611
|
+
32: {
|
|
1612
|
+
bad: '"assignments": { "assignments": [] }',
|
|
1613
|
+
good: '"assignments": { "assignments": [{ "id": "f1", "name": "status", "value": "processed", "type": "string" }] }'
|
|
1614
|
+
},
|
|
1615
|
+
33: {
|
|
1616
|
+
bad: '"rule": { "interval": [] }',
|
|
1617
|
+
good: '"rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] }'
|
|
1618
|
+
},
|
|
1619
|
+
34: {
|
|
1620
|
+
bad: '"path": "/my webhook"',
|
|
1621
|
+
good: '"path": "my-webhook"'
|
|
1622
|
+
}
|
|
1146
1623
|
};
|
|
1147
1624
|
var RULE_MITIGATIONS = {
|
|
1148
1625
|
1: "Provide a non-empty workflow name string",
|
|
@@ -1161,36 +1638,86 @@ var RULE_MITIGATIONS = {
|
|
|
1161
1638
|
14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
|
|
1162
1639
|
15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
|
|
1163
1640
|
16: "All node names must be unique within the workflow",
|
|
1164
|
-
17: '
|
|
1641
|
+
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',
|
|
1165
1642
|
18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
|
|
1166
1643
|
19: "Use known safe typeVersion values for each node type",
|
|
1167
1644
|
20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
|
|
1168
1645
|
21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
|
|
1169
1646
|
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
|
|
1170
|
-
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync"
|
|
1647
|
+
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
1648
|
+
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
1649
|
+
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
1650
|
+
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime',
|
|
1651
|
+
27: 'Replace placeholder URLs with your actual API endpoint \u2014 do not use "example.com" or "YOUR_URL" patterns',
|
|
1652
|
+
28: "Add executable code to the code node \u2014 empty or comment-only code nodes do nothing at runtime",
|
|
1653
|
+
29: "Set the channel parameter for Slack message operations (channelId with __rl object, or channel as string)",
|
|
1654
|
+
30: "Set the to parameter for Gmail send operations with at least one recipient email address",
|
|
1655
|
+
31: "Add at least one condition to the if node \u2014 conditions.conditions array must be non-empty",
|
|
1656
|
+
32: "Add field assignments to the set node \u2014 assignments.assignments array must be non-empty for typeVersion 3.x",
|
|
1657
|
+
33: "Add at least one schedule rule to scheduleTrigger \u2014 rule.interval array must have at least one entry",
|
|
1658
|
+
34: 'Webhook path must be a relative path without spaces, leading slashes, or protocol prefixes (e.g. "my-hook")'
|
|
1171
1659
|
};
|
|
1172
1660
|
|
|
1173
1661
|
// src/generation/prompt-builder.ts
|
|
1174
1662
|
var CRITICAL_SCORE_THRESHOLD = 0.15;
|
|
1663
|
+
function resolveProfile() {
|
|
1664
|
+
const env = process.env["KAIROS_PROMPT_PROFILE"];
|
|
1665
|
+
if (env === "minimal" || env === "standard" || env === "rich") return env;
|
|
1666
|
+
return "standard";
|
|
1667
|
+
}
|
|
1668
|
+
var PROACTIVE_EXPRESSION_GUIDANCE = `## Expression Syntax Quick Reference
|
|
1669
|
+
|
|
1670
|
+
Always use these patterns in expressions:
|
|
1671
|
+
- Access node data: $('NodeName').item.json.field (not $node["NodeName"].json)
|
|
1672
|
+
- Access JSON field: $json.field (not $json.items[0].field)
|
|
1673
|
+
- Single item: $('NodeName').first().json.field
|
|
1674
|
+
- All items: $('NodeName').all()`;
|
|
1175
1675
|
var PromptBuilder = class {
|
|
1176
1676
|
patternsPath;
|
|
1177
|
-
|
|
1677
|
+
profile;
|
|
1678
|
+
_lastActivePatterns = null;
|
|
1679
|
+
constructor(patternsPath, profile) {
|
|
1178
1680
|
this.patternsPath = patternsPath ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "patterns.json");
|
|
1681
|
+
this.profile = profile ?? resolveProfile();
|
|
1682
|
+
}
|
|
1683
|
+
resolveMaxPatterns() {
|
|
1684
|
+
if (this.profile === "minimal") return 3;
|
|
1685
|
+
if (this.profile === "rich") return 15;
|
|
1686
|
+
return 10;
|
|
1179
1687
|
}
|
|
1180
1688
|
build(request, matches, globalFailureRates = [], dynamicCatalog) {
|
|
1181
1689
|
const mode = this.resolveMode(matches);
|
|
1182
|
-
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
|
|
1690
|
+
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog, request.description);
|
|
1183
1691
|
const userMessage = this.buildUserMessage(request, matches, mode);
|
|
1184
1692
|
return { system, userMessage, mode, matches };
|
|
1185
1693
|
}
|
|
1186
|
-
buildCorrectionMessage(request, matches, allIssues, attempt) {
|
|
1694
|
+
buildCorrectionMessage(request, matches, allIssues, attempt, failingRuleIds) {
|
|
1187
1695
|
const base = this.buildUserMessage(request, matches, this.resolveMode(matches));
|
|
1696
|
+
let examplesSection = "";
|
|
1697
|
+
if (failingRuleIds && failingRuleIds.length > 0) {
|
|
1698
|
+
const uniqueRules = [...new Set(failingRuleIds)];
|
|
1699
|
+
const exampleLines = [];
|
|
1700
|
+
for (const rule of uniqueRules) {
|
|
1701
|
+
const ex = RULE_EXAMPLES[rule];
|
|
1702
|
+
if (ex) {
|
|
1703
|
+
exampleLines.push(`Rule ${rule}:
|
|
1704
|
+
Bad: ${ex.bad}
|
|
1705
|
+
Good: ${ex.good}`);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
if (exampleLines.length > 0) {
|
|
1709
|
+
examplesSection = `
|
|
1710
|
+
|
|
1711
|
+
## Concrete Fix Examples
|
|
1712
|
+
${exampleLines.join("\n\n")}`;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1188
1715
|
return `${base}
|
|
1189
1716
|
|
|
1190
1717
|
IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
|
|
1191
1718
|
${allIssues.join("\n")}
|
|
1192
1719
|
|
|
1193
|
-
Fix ALL of the above issues in your new response. Do not repeat any of these mistakes
|
|
1720
|
+
Fix ALL of the above issues in your new response. Do not repeat any of these mistakes.${examplesSection}`;
|
|
1194
1721
|
}
|
|
1195
1722
|
resolveMode(matches) {
|
|
1196
1723
|
if (matches.length === 0) return "scratch";
|
|
@@ -1198,7 +1725,7 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1198
1725
|
if (!top) return "scratch";
|
|
1199
1726
|
return scoreToMode(top.score);
|
|
1200
1727
|
}
|
|
1201
|
-
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog) {
|
|
1728
|
+
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog, description) {
|
|
1202
1729
|
let basePrompt = SYSTEM_PROMPT_V1;
|
|
1203
1730
|
if (dynamicCatalog) {
|
|
1204
1731
|
basePrompt = basePrompt.replace(
|
|
@@ -1213,53 +1740,62 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1213
1740
|
cache_control: { type: "ephemeral" }
|
|
1214
1741
|
}
|
|
1215
1742
|
];
|
|
1216
|
-
if (
|
|
1217
|
-
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1743
|
+
if (this.profile !== "minimal") {
|
|
1744
|
+
if (mode === "reference" && matches.length > 0) {
|
|
1745
|
+
const refText = matches.slice(0, 3).map((m) => {
|
|
1746
|
+
const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1747
|
+
return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
|
|
1220
1748
|
Nodes:
|
|
1221
1749
|
${nodes}`;
|
|
1222
|
-
|
|
1223
|
-
blocks.push({
|
|
1224
|
-
type: "text",
|
|
1225
|
-
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1226
|
-
|
|
1227
|
-
${refText}`
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
if (mode === "direct" && matches[0]) {
|
|
1231
|
-
const match = matches[0];
|
|
1232
|
-
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1233
|
-
if (json.length > 3e4) {
|
|
1234
|
-
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1750
|
+
}).join("\n\n");
|
|
1235
1751
|
blocks.push({
|
|
1236
1752
|
type: "text",
|
|
1237
|
-
text: `##
|
|
1753
|
+
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1754
|
+
|
|
1755
|
+
${refText}`
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
if (mode === "direct" && matches[0]) {
|
|
1759
|
+
const match = matches[0];
|
|
1760
|
+
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1761
|
+
if (json.length > 3e4) {
|
|
1762
|
+
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1763
|
+
blocks.push({
|
|
1764
|
+
type: "text",
|
|
1765
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
|
|
1238
1766
|
Nodes:
|
|
1239
1767
|
${nodes}`
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1768
|
+
});
|
|
1769
|
+
} else {
|
|
1770
|
+
blocks.push({
|
|
1771
|
+
type: "text",
|
|
1772
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
|
|
1245
1773
|
|
|
1246
1774
|
${json}`
|
|
1247
|
-
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1248
1777
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
text: `## Weak Structural Hint
|
|
1778
|
+
if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
|
|
1779
|
+
const hint = matches[0];
|
|
1780
|
+
const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
|
|
1781
|
+
blocks.push({
|
|
1782
|
+
type: "text",
|
|
1783
|
+
text: `## Weak Structural Hint
|
|
1256
1784
|
A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
|
|
1257
|
-
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1258
1787
|
}
|
|
1259
|
-
const warnings = this.buildFailureWarnings(matches, globalFailureRates);
|
|
1788
|
+
const warnings = this.buildFailureWarnings(matches, globalFailureRates, description);
|
|
1260
1789
|
if (warnings) {
|
|
1261
1790
|
blocks.push({ type: "text", text: warnings });
|
|
1262
1791
|
}
|
|
1792
|
+
if (this.profile === "rich") {
|
|
1793
|
+
const expressionRules = /* @__PURE__ */ new Set([24, 25, 26]);
|
|
1794
|
+
const expressionAlreadyCovered = (this._lastActivePatterns ?? []).some((p) => expressionRules.has(p.rule));
|
|
1795
|
+
if (!expressionAlreadyCovered) {
|
|
1796
|
+
blocks.push({ type: "text", text: PROACTIVE_EXPRESSION_GUIDANCE });
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1263
1799
|
return blocks;
|
|
1264
1800
|
}
|
|
1265
1801
|
loadPatterns() {
|
|
@@ -1273,18 +1809,38 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1273
1809
|
}
|
|
1274
1810
|
}
|
|
1275
1811
|
getWarnedRules() {
|
|
1276
|
-
|
|
1812
|
+
const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
|
|
1813
|
+
return patterns.map((p) => p.rule);
|
|
1277
1814
|
}
|
|
1278
|
-
getActivePatterns() {
|
|
1279
|
-
const MAX_WARNED = 10;
|
|
1815
|
+
getActivePatterns(maxCount = 10, description) {
|
|
1280
1816
|
const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
|
|
1281
1817
|
const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1282
1818
|
const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1283
1819
|
const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1820
|
+
const ordered = [...regressed, ...confirmed, ...drafts];
|
|
1821
|
+
if (this.profile === "minimal" && description) {
|
|
1822
|
+
return this.rankByRelevance(ordered, description).slice(0, maxCount);
|
|
1823
|
+
}
|
|
1824
|
+
return ordered.slice(0, maxCount);
|
|
1825
|
+
}
|
|
1826
|
+
rankByRelevance(patterns, description) {
|
|
1827
|
+
const lower = description.toLowerCase();
|
|
1828
|
+
const STAGE_KEYWORDS = {
|
|
1829
|
+
credential_injection: ["credential", "auth", "api key", "token", "oauth", "smtp", "imap", "password", "secret"],
|
|
1830
|
+
connection_wiring: ["connect", "link", "wire", "chain", "merge", "branch", "join"],
|
|
1831
|
+
expression_syntax: ["expression", "variable", "json", "field", "data", "$json", "item"],
|
|
1832
|
+
workflow_structure: ["trigger", "webhook", "schedule", "structure", "workflow"],
|
|
1833
|
+
node_generation: ["node", "generate", "create", "build", "send", "fetch", "email", "slack", "http"]
|
|
1834
|
+
};
|
|
1835
|
+
return patterns.map((p) => {
|
|
1836
|
+
const keywords = STAGE_KEYWORDS[p.pipelineStage] ?? [];
|
|
1837
|
+
const relevanceBoost = keywords.some((kw) => lower.includes(kw)) ? 1 : 0;
|
|
1838
|
+
return { pattern: p, sort: relevanceBoost * 10 + p.compositeScore };
|
|
1839
|
+
}).sort((a, b) => b.sort - a.sort).map((x) => x.pattern);
|
|
1840
|
+
}
|
|
1841
|
+
buildFailureWarnings(matches, globalFailureRates, description) {
|
|
1842
|
+
const richPatterns = this.getActivePatterns(this.resolveMaxPatterns(), description);
|
|
1843
|
+
this._lastActivePatterns = richPatterns;
|
|
1288
1844
|
if (richPatterns.length > 0) {
|
|
1289
1845
|
return this.buildStageGroupedWarnings(richPatterns, matches);
|
|
1290
1846
|
}
|
|
@@ -1295,7 +1851,8 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1295
1851
|
credential_injection: "CREDENTIAL FORMATTING",
|
|
1296
1852
|
connection_wiring: "CONNECTION WIRING",
|
|
1297
1853
|
node_generation: "NODE GENERATION",
|
|
1298
|
-
workflow_structure: "WORKFLOW STRUCTURE"
|
|
1854
|
+
workflow_structure: "WORKFLOW STRUCTURE",
|
|
1855
|
+
expression_syntax: "EXPRESSION SYNTAX"
|
|
1299
1856
|
};
|
|
1300
1857
|
const byStage = /* @__PURE__ */ new Map();
|
|
1301
1858
|
for (const p of patterns) {
|
|
@@ -1323,7 +1880,11 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1323
1880
|
const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
|
|
1324
1881
|
const remedyStr = remedy ? `
|
|
1325
1882
|
Fix: ${remedy}` : "";
|
|
1326
|
-
|
|
1883
|
+
const ex = RULE_EXAMPLES[p.rule];
|
|
1884
|
+
const exampleStr = ex ? `
|
|
1885
|
+
Bad: ${ex.bad}
|
|
1886
|
+
Good: ${ex.good}` : "";
|
|
1887
|
+
lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}${exampleStr}`);
|
|
1327
1888
|
} else {
|
|
1328
1889
|
const ruleNums = group.map((p) => p.rule).join(", ");
|
|
1329
1890
|
const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
|
|
@@ -1430,12 +1991,12 @@ var GENERATE_WORKFLOW_TOOL = {
|
|
|
1430
1991
|
}
|
|
1431
1992
|
};
|
|
1432
1993
|
var WorkflowDesigner = class {
|
|
1433
|
-
constructor(anthropic, model, logger) {
|
|
1994
|
+
constructor(anthropic, model, logger, patternsPath) {
|
|
1434
1995
|
this.anthropic = anthropic;
|
|
1435
1996
|
this.model = model;
|
|
1436
1997
|
this.logger = logger;
|
|
1437
1998
|
this.validator = new N8nValidator();
|
|
1438
|
-
this.promptBuilder = new PromptBuilder();
|
|
1999
|
+
this.promptBuilder = new PromptBuilder(patternsPath);
|
|
1439
2000
|
}
|
|
1440
2001
|
anthropic;
|
|
1441
2002
|
model;
|
|
@@ -1458,7 +2019,8 @@ var WorkflowDesigner = class {
|
|
|
1458
2019
|
const issueLines = lastErrors.map(
|
|
1459
2020
|
(i) => `- [Rule ${i.rule}] ${i.message}${i.nodeId ? ` (node: ${i.nodeId})` : ""}`
|
|
1460
2021
|
);
|
|
1461
|
-
|
|
2022
|
+
const failingRuleIds = lastErrors.map((i) => i.rule);
|
|
2023
|
+
userMessage = this.promptBuilder.buildCorrectionMessage(request, matches, issueLines, attempt - 1, failingRuleIds);
|
|
1462
2024
|
this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: lastErrors.length });
|
|
1463
2025
|
}
|
|
1464
2026
|
const start = Date.now();
|
|
@@ -1519,6 +2081,11 @@ var WorkflowDesigner = class {
|
|
|
1519
2081
|
}
|
|
1520
2082
|
}
|
|
1521
2083
|
extractToolUse(message) {
|
|
2084
|
+
if (message.stop_reason === "max_tokens") {
|
|
2085
|
+
throw new GenerationError(
|
|
2086
|
+
"Claude response was truncated (max_tokens reached) \u2014 the workflow may be too large. Try a simpler description or break it into smaller workflows."
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
1522
2089
|
const toolUseBlock = message.content.find(
|
|
1523
2090
|
(block) => block.type === "tool_use"
|
|
1524
2091
|
);
|
|
@@ -1561,11 +2128,12 @@ var TelemetryCollector = class {
|
|
|
1561
2128
|
this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
|
|
1562
2129
|
this.sessionId = generateUUID();
|
|
1563
2130
|
}
|
|
1564
|
-
async emit(eventType, data) {
|
|
2131
|
+
async emit(eventType, data, runId) {
|
|
1565
2132
|
const event = {
|
|
1566
2133
|
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
1567
2134
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1568
2135
|
sessionId: this.sessionId,
|
|
2136
|
+
...runId ? { runId } : {},
|
|
1569
2137
|
eventType,
|
|
1570
2138
|
data
|
|
1571
2139
|
};
|
|
@@ -1638,19 +2206,20 @@ var TelemetryReader = class {
|
|
|
1638
2206
|
}
|
|
1639
2207
|
const events = await this.readRecentEvents(days);
|
|
1640
2208
|
const buildSessions = new Set(
|
|
1641
|
-
events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
|
|
2209
|
+
events.filter((e) => e.eventType === "build_complete").map((e) => e.runId ?? e.sessionId)
|
|
1642
2210
|
);
|
|
1643
2211
|
const MIN_BUILDS_FOR_RATES = 3;
|
|
1644
2212
|
if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
|
|
1645
2213
|
const ruleSessions = /* @__PURE__ */ new Map();
|
|
1646
2214
|
for (const event of events) {
|
|
1647
2215
|
if (event.eventType !== "generation_attempt") continue;
|
|
1648
|
-
|
|
2216
|
+
const eventKey = event.runId ?? event.sessionId;
|
|
2217
|
+
if (!buildSessions.has(eventKey)) continue;
|
|
1649
2218
|
const data = event.data;
|
|
1650
2219
|
if (data.validationPassed || !data.issues) continue;
|
|
1651
2220
|
for (const issue of data.issues) {
|
|
1652
2221
|
const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
|
|
1653
|
-
entry.sessions.add(
|
|
2222
|
+
entry.sessions.add(eventKey);
|
|
1654
2223
|
entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
|
|
1655
2224
|
ruleSessions.set(issue.rule, entry);
|
|
1656
2225
|
}
|
|
@@ -1691,22 +2260,25 @@ var PATTERN_SCHEMA_VERSION = 2;
|
|
|
1691
2260
|
var PatternAnalyzer = class _PatternAnalyzer {
|
|
1692
2261
|
telemetryDir;
|
|
1693
2262
|
outputDir;
|
|
2263
|
+
_cachedEvents = null;
|
|
2264
|
+
_cachedPreviousPatterns = null;
|
|
1694
2265
|
constructor(telemetryDir) {
|
|
1695
2266
|
const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
|
|
1696
2267
|
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
1697
2268
|
this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
|
|
1698
2269
|
}
|
|
1699
2270
|
async loadPreviousPatterns() {
|
|
2271
|
+
if (this._cachedPreviousPatterns !== null) return this._cachedPreviousPatterns;
|
|
1700
2272
|
try {
|
|
1701
2273
|
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
|
|
1702
2274
|
const prev = JSON.parse(raw);
|
|
1703
2275
|
const version = prev.schemaVersion ?? 0;
|
|
1704
2276
|
const patterns = prev.topFailureRules ?? [];
|
|
1705
|
-
|
|
1706
|
-
return this.migratePatterns(patterns, version);
|
|
2277
|
+
this._cachedPreviousPatterns = version === PATTERN_SCHEMA_VERSION ? patterns : this.migratePatterns(patterns, version);
|
|
1707
2278
|
} catch {
|
|
1708
|
-
|
|
2279
|
+
this._cachedPreviousPatterns = [];
|
|
1709
2280
|
}
|
|
2281
|
+
return this._cachedPreviousPatterns;
|
|
1710
2282
|
}
|
|
1711
2283
|
migratePatterns(patterns, fromVersion) {
|
|
1712
2284
|
let migrated = patterns;
|
|
@@ -1719,19 +2291,23 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1719
2291
|
}));
|
|
1720
2292
|
}
|
|
1721
2293
|
if (fromVersion < 2) {
|
|
1722
|
-
migrated = migrated.map((p) =>
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
...p
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
2294
|
+
migrated = migrated.map((p) => {
|
|
2295
|
+
const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
|
|
2296
|
+
return {
|
|
2297
|
+
...p,
|
|
2298
|
+
scoringFactors: {
|
|
2299
|
+
...sf,
|
|
2300
|
+
stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
});
|
|
1729
2304
|
}
|
|
1730
2305
|
return migrated;
|
|
1731
2306
|
}
|
|
1732
2307
|
async analyze(days = 30) {
|
|
1733
2308
|
const previousPatterns = await this.loadPreviousPatterns();
|
|
1734
2309
|
const events = await this.readAllEvents(days);
|
|
2310
|
+
this._cachedEvents = events;
|
|
1735
2311
|
const starts = events.filter((e) => e.eventType === "build_start");
|
|
1736
2312
|
const attempts = events.filter((e) => e.eventType === "generation_attempt");
|
|
1737
2313
|
const passed = attempts.filter(
|
|
@@ -1744,13 +2320,18 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1744
2320
|
const credentialFailures = /* @__PURE__ */ new Map();
|
|
1745
2321
|
for (const a of failed) {
|
|
1746
2322
|
const weight = this.recencyWeight(a.fileDate);
|
|
2323
|
+
const buildId = a.runId ?? a.sessionId;
|
|
1747
2324
|
const data = a.data;
|
|
1748
2325
|
for (const issue of data.issues ?? []) {
|
|
1749
|
-
|
|
2326
|
+
if (issue.severity === "warn") continue;
|
|
2327
|
+
const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
|
|
1750
2328
|
entry.count++;
|
|
1751
|
-
entry.sessions.add(
|
|
2329
|
+
entry.sessions.add(buildId);
|
|
1752
2330
|
entry.recencyWeights.push(weight);
|
|
1753
2331
|
entry.allMessages.push(issue.message);
|
|
2332
|
+
if (data.workflowType) {
|
|
2333
|
+
entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
|
|
2334
|
+
}
|
|
1754
2335
|
ruleFailures.set(issue.rule, entry);
|
|
1755
2336
|
if (issue.rule === 17) {
|
|
1756
2337
|
const credPatterns = [
|
|
@@ -1803,9 +2384,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1803
2384
|
}
|
|
1804
2385
|
const sessions = /* @__PURE__ */ new Map();
|
|
1805
2386
|
for (const a of attempts) {
|
|
1806
|
-
const
|
|
2387
|
+
const buildId = a.runId ?? a.sessionId;
|
|
2388
|
+
const list = sessions.get(buildId) ?? [];
|
|
1807
2389
|
list.push(a);
|
|
1808
|
-
sessions.set(
|
|
2390
|
+
sessions.set(buildId, list);
|
|
1809
2391
|
}
|
|
1810
2392
|
let firstTryPass = 0;
|
|
1811
2393
|
let correctionNeeded = 0;
|
|
@@ -1852,7 +2434,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1852
2434
|
const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
|
|
1853
2435
|
const stickiness = stickinessCount.get(rule) ?? 0;
|
|
1854
2436
|
const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
|
|
1855
|
-
|
|
2437
|
+
const pattern = {
|
|
1856
2438
|
rule,
|
|
1857
2439
|
failureCount: entry.count,
|
|
1858
2440
|
confidence: Math.round(rawConfidence * 1e3) / 1e3,
|
|
@@ -1864,6 +2446,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1864
2446
|
exampleMessages: this.deduplicateMessages(entry.allMessages),
|
|
1865
2447
|
mitigation: RULE_MITIGATIONS[rule] ?? null
|
|
1866
2448
|
};
|
|
2449
|
+
if (entry.workflowTypes.size > 0) {
|
|
2450
|
+
pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
|
|
2451
|
+
}
|
|
2452
|
+
return pattern;
|
|
1867
2453
|
}).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1868
2454
|
const activeRules = new Set(activePatterns.map((p) => p.rule));
|
|
1869
2455
|
for (const p of activePatterns) {
|
|
@@ -1920,7 +2506,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1920
2506
|
const warned = bcData.warnedRules ?? [];
|
|
1921
2507
|
if (warned.length === 0) continue;
|
|
1922
2508
|
const sessionFailedRules = /* @__PURE__ */ new Set();
|
|
1923
|
-
const sessionAttempts = sessions.get(bc.sessionId) ?? [];
|
|
2509
|
+
const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
|
|
1924
2510
|
for (const a of sessionAttempts) {
|
|
1925
2511
|
const ad = a.data;
|
|
1926
2512
|
if (ad.validationPassed === false) {
|
|
@@ -1992,6 +2578,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1992
2578
|
const tmpPath = `${outputPath}.tmp`;
|
|
1993
2579
|
await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
1994
2580
|
await (0, import_promises3.rename)(tmpPath, outputPath);
|
|
2581
|
+
this._cachedPreviousPatterns = null;
|
|
1995
2582
|
const historySummary = {
|
|
1996
2583
|
timestamp: analysis.generatedAt,
|
|
1997
2584
|
totalBuilds: analysis.summary.totalBuilds,
|
|
@@ -2003,8 +2590,55 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2003
2590
|
};
|
|
2004
2591
|
const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
|
|
2005
2592
|
await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
|
|
2593
|
+
const sessions = await this.buildSessionSummaries(days);
|
|
2594
|
+
const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
|
|
2595
|
+
const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
|
|
2596
|
+
await (0, import_promises3.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
|
|
2597
|
+
await (0, import_promises3.rename)(sessionHistoryTmp, sessionHistoryPath);
|
|
2006
2598
|
return analysis;
|
|
2007
2599
|
}
|
|
2600
|
+
async getSessions(limit = 20) {
|
|
2601
|
+
try {
|
|
2602
|
+
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
|
|
2603
|
+
const all = JSON.parse(raw);
|
|
2604
|
+
return all.slice(-limit);
|
|
2605
|
+
} catch {
|
|
2606
|
+
return [];
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
async buildSessionSummaries(days = 30) {
|
|
2610
|
+
const events = this._cachedEvents ?? await this.readAllEvents(days);
|
|
2611
|
+
const buildCompletes = events.filter((e) => e.eventType === "build_complete");
|
|
2612
|
+
const attemptsByBuild = /* @__PURE__ */ new Map();
|
|
2613
|
+
for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
|
|
2614
|
+
const buildId = e.runId ?? e.sessionId;
|
|
2615
|
+
const list = attemptsByBuild.get(buildId) ?? [];
|
|
2616
|
+
list.push(e);
|
|
2617
|
+
attemptsByBuild.set(buildId, list);
|
|
2618
|
+
}
|
|
2619
|
+
const summaries = buildCompletes.map((bc) => {
|
|
2620
|
+
const data = bc.data;
|
|
2621
|
+
const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
|
|
2622
|
+
const failedRules = Array.from(new Set(
|
|
2623
|
+
sessionAttempts.flatMap((a) => {
|
|
2624
|
+
const ad = a.data;
|
|
2625
|
+
if (ad.validationPassed !== false) return [];
|
|
2626
|
+
return (ad.issues ?? []).map((i) => i.rule);
|
|
2627
|
+
})
|
|
2628
|
+
));
|
|
2629
|
+
return {
|
|
2630
|
+
sessionId: bc.runId ?? bc.sessionId,
|
|
2631
|
+
date: bc.fileDate,
|
|
2632
|
+
description: data.description ?? "",
|
|
2633
|
+
workflowType: data.workflowType ?? null,
|
|
2634
|
+
attempts: data.totalAttempts ?? 1,
|
|
2635
|
+
success: data.success ?? false,
|
|
2636
|
+
failedRules,
|
|
2637
|
+
workflowName: data.workflowName ?? null
|
|
2638
|
+
};
|
|
2639
|
+
});
|
|
2640
|
+
return summaries.sort((a, b) => a.date.localeCompare(b.date));
|
|
2641
|
+
}
|
|
2008
2642
|
async getHistory(limit = 20) {
|
|
2009
2643
|
try {
|
|
2010
2644
|
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
|
|
@@ -2026,7 +2660,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2026
2660
|
alerts.push({
|
|
2027
2661
|
type: "stale_pattern",
|
|
2028
2662
|
rule: p.rule,
|
|
2029
|
-
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-
|
|
2663
|
+
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-34)`
|
|
2030
2664
|
});
|
|
2031
2665
|
}
|
|
2032
2666
|
}
|
|
@@ -2113,8 +2747,60 @@ var nullLogger = {
|
|
|
2113
2747
|
}
|
|
2114
2748
|
};
|
|
2115
2749
|
|
|
2750
|
+
// src/utils/workflow-type.ts
|
|
2751
|
+
var TYPE_KEYWORDS = [
|
|
2752
|
+
["gmail", "email"],
|
|
2753
|
+
["imap", "email"],
|
|
2754
|
+
["smtp", "email"],
|
|
2755
|
+
[" email", "email"],
|
|
2756
|
+
["slack", "slack"],
|
|
2757
|
+
["telegram", "messaging"],
|
|
2758
|
+
["discord", "messaging"],
|
|
2759
|
+
[" sms", "messaging"],
|
|
2760
|
+
["twilio", "messaging"],
|
|
2761
|
+
["webhook", "webhook"],
|
|
2762
|
+
["google sheets", "data"],
|
|
2763
|
+
["spreadsheet", "data"],
|
|
2764
|
+
["airtable", "data"],
|
|
2765
|
+
["notion", "data"],
|
|
2766
|
+
["github", "devops"],
|
|
2767
|
+
["gitlab", "devops"],
|
|
2768
|
+
["schedule", "schedule"],
|
|
2769
|
+
[" cron", "schedule"],
|
|
2770
|
+
["daily", "schedule"],
|
|
2771
|
+
["weekly", "schedule"],
|
|
2772
|
+
["hourly", "schedule"],
|
|
2773
|
+
["every day", "schedule"],
|
|
2774
|
+
["every hour", "schedule"],
|
|
2775
|
+
["every morning", "schedule"],
|
|
2776
|
+
["postgres", "database"],
|
|
2777
|
+
["mysql", "database"],
|
|
2778
|
+
["supabase", "database"],
|
|
2779
|
+
["redis", "database"],
|
|
2780
|
+
[" database", "database"],
|
|
2781
|
+
[" llm", "ai"],
|
|
2782
|
+
[" gpt", "ai"],
|
|
2783
|
+
["claude", "ai"],
|
|
2784
|
+
[" agent", "ai"],
|
|
2785
|
+
["langchain", "ai"],
|
|
2786
|
+
[" ai ", "ai"],
|
|
2787
|
+
[" ai", "ai"],
|
|
2788
|
+
["http request", "api"],
|
|
2789
|
+
["rest api", "api"],
|
|
2790
|
+
[" api", "api"]
|
|
2791
|
+
];
|
|
2792
|
+
function inferWorkflowType(description) {
|
|
2793
|
+
const lower = " " + description.toLowerCase();
|
|
2794
|
+
for (const [keyword, type] of TYPE_KEYWORDS) {
|
|
2795
|
+
if (lower.includes(keyword)) return type;
|
|
2796
|
+
}
|
|
2797
|
+
return null;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2116
2800
|
// src/client.ts
|
|
2117
|
-
var
|
|
2801
|
+
var import_node_os5 = require("os");
|
|
2802
|
+
var import_node_path6 = require("path");
|
|
2803
|
+
var DEFAULT_MODEL = process.env["KAIROS_MODEL"] ?? "claude-sonnet-4-6";
|
|
2118
2804
|
var Kairos = class {
|
|
2119
2805
|
provider;
|
|
2120
2806
|
designer;
|
|
@@ -2142,7 +2828,8 @@ var Kairos = class {
|
|
|
2142
2828
|
this.provider = null;
|
|
2143
2829
|
}
|
|
2144
2830
|
const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
|
|
2145
|
-
|
|
2831
|
+
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");
|
|
2832
|
+
this.designer = new WorkflowDesigner(anthropic, this.model, logger, patternsPath);
|
|
2146
2833
|
this.validator = new N8nValidator();
|
|
2147
2834
|
this.library = options.library ?? new NullLibrary();
|
|
2148
2835
|
this.logger = logger;
|
|
@@ -2175,11 +2862,13 @@ var Kairos = class {
|
|
|
2175
2862
|
this.validateDescription(description);
|
|
2176
2863
|
this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
|
|
2177
2864
|
const buildStart = Date.now();
|
|
2865
|
+
const runId = generateUUID();
|
|
2866
|
+
const workflowType = inferWorkflowType(description);
|
|
2178
2867
|
await this.telemetry?.emit("build_start", {
|
|
2179
2868
|
description,
|
|
2180
2869
|
model: this.model,
|
|
2181
2870
|
dryRun: options?.dryRun ?? false
|
|
2182
|
-
});
|
|
2871
|
+
}, runId);
|
|
2183
2872
|
await this.library.initialize();
|
|
2184
2873
|
const matches = await this.library.search(description);
|
|
2185
2874
|
if (matches.length > 0) {
|
|
@@ -2214,8 +2903,9 @@ var Kairos = class {
|
|
|
2214
2903
|
tokensOutput: meta.tokensOutput,
|
|
2215
2904
|
validationPassed: meta.validationPassed,
|
|
2216
2905
|
issueCount: meta.issues.length,
|
|
2217
|
-
issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
|
|
2218
|
-
|
|
2906
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
2907
|
+
workflowType
|
|
2908
|
+
}, runId);
|
|
2219
2909
|
}
|
|
2220
2910
|
await this.telemetry?.emit("build_complete", {
|
|
2221
2911
|
description,
|
|
@@ -2228,13 +2918,14 @@ var Kairos = class {
|
|
|
2228
2918
|
workflowId: null,
|
|
2229
2919
|
dryRun: options?.dryRun ?? false,
|
|
2230
2920
|
credentialsNeeded: 0,
|
|
2231
|
-
warnedRules: err.warnedRules ?? []
|
|
2232
|
-
|
|
2921
|
+
warnedRules: err.warnedRules ?? [],
|
|
2922
|
+
workflowType
|
|
2923
|
+
}, runId);
|
|
2233
2924
|
this.updatePatterns();
|
|
2234
2925
|
}
|
|
2235
2926
|
throw err;
|
|
2236
2927
|
}
|
|
2237
|
-
await this.emitAttemptTelemetry(description, designResult);
|
|
2928
|
+
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
2238
2929
|
const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
|
|
2239
2930
|
this.saveToLibrary(workflow, description, designResult, matches);
|
|
2240
2931
|
if (options?.dryRun) {
|
|
@@ -2251,8 +2942,9 @@ var Kairos = class {
|
|
|
2251
2942
|
workflowId: null,
|
|
2252
2943
|
dryRun: true,
|
|
2253
2944
|
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2254
|
-
warnedRules: designResult.warnedRules
|
|
2255
|
-
|
|
2945
|
+
warnedRules: designResult.warnedRules,
|
|
2946
|
+
workflowType
|
|
2947
|
+
}, runId);
|
|
2256
2948
|
this.updatePatterns();
|
|
2257
2949
|
return {
|
|
2258
2950
|
workflowId: null,
|
|
@@ -2266,10 +2958,19 @@ var Kairos = class {
|
|
|
2266
2958
|
}
|
|
2267
2959
|
const provider = this.requireProvider();
|
|
2268
2960
|
const deployed = await provider.deploy(workflow);
|
|
2269
|
-
this.
|
|
2961
|
+
this.logger.info("Workflow deployed to n8n", { workflowId: deployed.workflowId, name: deployed.name });
|
|
2962
|
+
this.recordDeploy(deployed.workflowId);
|
|
2270
2963
|
if (options?.activate) {
|
|
2271
2964
|
await provider.activate(deployed.workflowId);
|
|
2272
2965
|
}
|
|
2966
|
+
let smokeTestResult;
|
|
2967
|
+
if (options?.smokeTest) {
|
|
2968
|
+
smokeTestResult = await provider.smokeTest(deployed.workflowId, workflow).catch((err) => {
|
|
2969
|
+
this.logger.warn("Smoke test threw unexpectedly", { err: String(err) });
|
|
2970
|
+
return { status: "error", triggerType: "manual", error: String(err) };
|
|
2971
|
+
});
|
|
2972
|
+
this.logger.info("Smoke test complete", { status: smokeTestResult.status, triggerType: smokeTestResult.triggerType });
|
|
2973
|
+
}
|
|
2273
2974
|
const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
2274
2975
|
const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
2275
2976
|
await this.telemetry?.emit("build_complete", {
|
|
@@ -2283,8 +2984,9 @@ var Kairos = class {
|
|
|
2283
2984
|
workflowId: deployed.workflowId,
|
|
2284
2985
|
dryRun: false,
|
|
2285
2986
|
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2286
|
-
warnedRules: designResult.warnedRules
|
|
2287
|
-
|
|
2987
|
+
warnedRules: designResult.warnedRules,
|
|
2988
|
+
workflowType
|
|
2989
|
+
}, runId);
|
|
2288
2990
|
this.updatePatterns();
|
|
2289
2991
|
return {
|
|
2290
2992
|
workflowId: deployed.workflowId,
|
|
@@ -2293,18 +2995,21 @@ var Kairos = class {
|
|
|
2293
2995
|
credentialsNeeded: designResult.credentialsNeeded,
|
|
2294
2996
|
activationRequired: !options?.activate,
|
|
2295
2997
|
generationAttempts: designResult.attempts,
|
|
2296
|
-
dryRun: false
|
|
2998
|
+
dryRun: false,
|
|
2999
|
+
...smokeTestResult !== void 0 ? { smokeTest: smokeTestResult } : {}
|
|
2297
3000
|
};
|
|
2298
3001
|
}
|
|
2299
3002
|
async replace(id, description) {
|
|
2300
3003
|
this.validateDescription(description);
|
|
2301
3004
|
this.logger.info("Kairos.update", { id, description });
|
|
2302
3005
|
const buildStart = Date.now();
|
|
3006
|
+
const runId = generateUUID();
|
|
3007
|
+
const workflowType = inferWorkflowType(description);
|
|
2303
3008
|
await this.telemetry?.emit("build_start", {
|
|
2304
3009
|
description,
|
|
2305
3010
|
model: this.model,
|
|
2306
3011
|
dryRun: false
|
|
2307
|
-
});
|
|
3012
|
+
}, runId);
|
|
2308
3013
|
await this.library.initialize();
|
|
2309
3014
|
const matches = await this.library.search(description);
|
|
2310
3015
|
const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
|
|
@@ -2323,8 +3028,9 @@ var Kairos = class {
|
|
|
2323
3028
|
tokensOutput: meta.tokensOutput,
|
|
2324
3029
|
validationPassed: meta.validationPassed,
|
|
2325
3030
|
issueCount: meta.issues.length,
|
|
2326
|
-
issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
|
|
2327
|
-
|
|
3031
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
3032
|
+
workflowType
|
|
3033
|
+
}, runId);
|
|
2328
3034
|
}
|
|
2329
3035
|
await this.telemetry?.emit("build_complete", {
|
|
2330
3036
|
description,
|
|
@@ -2337,16 +3043,18 @@ var Kairos = class {
|
|
|
2337
3043
|
workflowId: null,
|
|
2338
3044
|
dryRun: false,
|
|
2339
3045
|
credentialsNeeded: 0,
|
|
2340
|
-
warnedRules: err.warnedRules ?? []
|
|
2341
|
-
|
|
3046
|
+
warnedRules: err.warnedRules ?? [],
|
|
3047
|
+
workflowType
|
|
3048
|
+
}, runId);
|
|
2342
3049
|
this.updatePatterns();
|
|
2343
3050
|
}
|
|
2344
3051
|
throw err;
|
|
2345
3052
|
}
|
|
2346
|
-
await this.emitAttemptTelemetry(description, designResult);
|
|
3053
|
+
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
2347
3054
|
const provider = this.requireProvider();
|
|
2348
3055
|
const deployed = await provider.update(id, designResult.workflow);
|
|
2349
|
-
this.
|
|
3056
|
+
this.logger.info("Workflow updated in n8n", { workflowId: deployed.workflowId, name: deployed.name });
|
|
3057
|
+
this.saveToLibrary(designResult.workflow, description, designResult, matches, deployed.workflowId);
|
|
2350
3058
|
this.recordDeploy();
|
|
2351
3059
|
const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
2352
3060
|
const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
@@ -2361,8 +3069,9 @@ var Kairos = class {
|
|
|
2361
3069
|
workflowId: deployed.workflowId,
|
|
2362
3070
|
dryRun: false,
|
|
2363
3071
|
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2364
|
-
warnedRules: designResult.warnedRules
|
|
2365
|
-
|
|
3072
|
+
warnedRules: designResult.warnedRules,
|
|
3073
|
+
workflowType
|
|
3074
|
+
}, runId);
|
|
2366
3075
|
this.updatePatterns();
|
|
2367
3076
|
return {
|
|
2368
3077
|
workflowId: deployed.workflowId,
|
|
@@ -2385,7 +3094,7 @@ var Kairos = class {
|
|
|
2385
3094
|
return null;
|
|
2386
3095
|
});
|
|
2387
3096
|
}
|
|
2388
|
-
async emitAttemptTelemetry(description, designResult) {
|
|
3097
|
+
async emitAttemptTelemetry(description, designResult, workflowType, runId) {
|
|
2389
3098
|
for (const meta of designResult.attemptMetadata) {
|
|
2390
3099
|
await this.telemetry?.emit("generation_attempt", {
|
|
2391
3100
|
description,
|
|
@@ -2396,14 +3105,15 @@ var Kairos = class {
|
|
|
2396
3105
|
tokensOutput: meta.tokensOutput,
|
|
2397
3106
|
validationPassed: meta.validationPassed,
|
|
2398
3107
|
issueCount: meta.issues.length,
|
|
2399
|
-
issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
|
|
2400
|
-
|
|
3108
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
3109
|
+
workflowType
|
|
3110
|
+
}, runId);
|
|
2401
3111
|
}
|
|
2402
3112
|
}
|
|
2403
|
-
recordDeploy() {
|
|
3113
|
+
recordDeploy(n8nWorkflowId) {
|
|
2404
3114
|
this.saveQueue = this.saveQueue.then(async (savedId) => {
|
|
2405
3115
|
if (savedId) {
|
|
2406
|
-
await this.library.recordDeployment(savedId);
|
|
3116
|
+
await this.library.recordDeployment(savedId, n8nWorkflowId);
|
|
2407
3117
|
}
|
|
2408
3118
|
return savedId;
|
|
2409
3119
|
}).catch((err) => {
|
|
@@ -2411,7 +3121,7 @@ var Kairos = class {
|
|
|
2411
3121
|
return null;
|
|
2412
3122
|
});
|
|
2413
3123
|
}
|
|
2414
|
-
saveToLibrary(workflow, description, designResult, matches) {
|
|
3124
|
+
saveToLibrary(workflow, description, designResult, matches, n8nWorkflowId) {
|
|
2415
3125
|
const failedAttempts = designResult.attemptMetadata.filter((m) => !m.validationPassed);
|
|
2416
3126
|
const failurePatterns = failedAttempts.flatMap(
|
|
2417
3127
|
(m) => m.issues.map((i) => ({ rule: i.rule, message: i.message }))
|
|
@@ -2437,6 +3147,7 @@ var Kairos = class {
|
|
|
2437
3147
|
if (matches.length > 0) metadata.sourceWorkflowIds = matches.map((m) => m.workflow.id);
|
|
2438
3148
|
if (topMatch) metadata.topMatchScore = topMatch.score;
|
|
2439
3149
|
if (designResult.credentialsNeeded.length > 0) metadata.credentialsNeeded = designResult.credentialsNeeded;
|
|
3150
|
+
if (n8nWorkflowId) metadata.n8nWorkflowId = n8nWorkflowId;
|
|
2440
3151
|
const firstTryPass = designResult.attemptMetadata.length > 0 && designResult.attemptMetadata[0].validationPassed;
|
|
2441
3152
|
const failedRules = Array.from(new Set(
|
|
2442
3153
|
designResult.attemptMetadata.filter((m) => !m.validationPassed).flatMap((m) => m.issues.map((i) => i.rule))
|
|
@@ -2496,16 +3207,36 @@ var Kairos = class {
|
|
|
2496
3207
|
|
|
2497
3208
|
// src/library/file-library.ts
|
|
2498
3209
|
var import_promises4 = require("fs/promises");
|
|
2499
|
-
var
|
|
2500
|
-
var
|
|
3210
|
+
var import_node_path7 = require("path");
|
|
3211
|
+
var import_node_os6 = require("os");
|
|
2501
3212
|
|
|
2502
3213
|
// src/library/scorer.ts
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
3214
|
+
function loadWeights() {
|
|
3215
|
+
const raw = {
|
|
3216
|
+
tfidf: parseFloat(process.env["KAIROS_WEIGHT_TFIDF"] ?? ""),
|
|
3217
|
+
nodeFingerprint: parseFloat(process.env["KAIROS_WEIGHT_JACCARD"] ?? ""),
|
|
3218
|
+
outcome: parseFloat(process.env["KAIROS_WEIGHT_OUTCOME"] ?? ""),
|
|
3219
|
+
deploy: parseFloat(process.env["KAIROS_WEIGHT_DEPLOY"] ?? "")
|
|
3220
|
+
};
|
|
3221
|
+
const defaults = { tfidf: 0.35, nodeFingerprint: 0.3, outcome: 0.2, deploy: 0.15 };
|
|
3222
|
+
const anySet = Object.values(raw).some((v) => !isNaN(v) && v >= 0);
|
|
3223
|
+
if (!anySet) return defaults;
|
|
3224
|
+
const w = {
|
|
3225
|
+
tfidf: !isNaN(raw.tfidf) && raw.tfidf >= 0 ? raw.tfidf : defaults.tfidf,
|
|
3226
|
+
nodeFingerprint: !isNaN(raw.nodeFingerprint) && raw.nodeFingerprint >= 0 ? raw.nodeFingerprint : defaults.nodeFingerprint,
|
|
3227
|
+
outcome: !isNaN(raw.outcome) && raw.outcome >= 0 ? raw.outcome : defaults.outcome,
|
|
3228
|
+
deploy: !isNaN(raw.deploy) && raw.deploy >= 0 ? raw.deploy : defaults.deploy
|
|
3229
|
+
};
|
|
3230
|
+
const total = w.tfidf + w.nodeFingerprint + w.outcome + w.deploy;
|
|
3231
|
+
if (total <= 0) return defaults;
|
|
3232
|
+
return {
|
|
3233
|
+
tfidf: w.tfidf / total,
|
|
3234
|
+
nodeFingerprint: w.nodeFingerprint / total,
|
|
3235
|
+
outcome: w.outcome / total,
|
|
3236
|
+
deploy: w.deploy / total
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
var WEIGHTS = loadWeights();
|
|
2509
3240
|
var NODE_KEYWORDS = {
|
|
2510
3241
|
slack: ["slack", "slackApi"],
|
|
2511
3242
|
email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
|
|
@@ -2690,6 +3421,8 @@ function clusterWorkflows(workflows) {
|
|
|
2690
3421
|
}
|
|
2691
3422
|
return clusters.sort((a, b) => b.members.length - a.members.length);
|
|
2692
3423
|
}
|
|
3424
|
+
var NOVELTY_BOOST = 0.05;
|
|
3425
|
+
var NOVELTY_PENALTY = 0.03;
|
|
2693
3426
|
function rerank(candidates, clusters) {
|
|
2694
3427
|
const clusterMap = /* @__PURE__ */ new Map();
|
|
2695
3428
|
for (const cluster of clusters) {
|
|
@@ -2697,7 +3430,7 @@ function rerank(candidates, clusters) {
|
|
|
2697
3430
|
clusterMap.set(member.id, cluster);
|
|
2698
3431
|
}
|
|
2699
3432
|
}
|
|
2700
|
-
|
|
3433
|
+
const pass1 = candidates.map((c) => {
|
|
2701
3434
|
const cluster = clusterMap.get(c.workflow.id);
|
|
2702
3435
|
let boost = 0;
|
|
2703
3436
|
if (cluster && cluster.avgFirstTryPassRate > 0) {
|
|
@@ -2709,7 +3442,25 @@ function rerank(candidates, clusters) {
|
|
|
2709
3442
|
return {
|
|
2710
3443
|
workflow: c.workflow,
|
|
2711
3444
|
score: Math.max(0, Math.min(1, c.score + boost)),
|
|
2712
|
-
|
|
3445
|
+
cluster
|
|
3446
|
+
};
|
|
3447
|
+
}).sort((a, b) => b.score - a.score);
|
|
3448
|
+
const seenFingerprints = /* @__PURE__ */ new Set();
|
|
3449
|
+
return pass1.map((c) => {
|
|
3450
|
+
const fpKey = c.cluster ? fingerprintKey(c.cluster.fingerprint) : null;
|
|
3451
|
+
let noveltyAdjust = 0;
|
|
3452
|
+
if (fpKey !== null) {
|
|
3453
|
+
if (!seenFingerprints.has(fpKey)) {
|
|
3454
|
+
seenFingerprints.add(fpKey);
|
|
3455
|
+
noveltyAdjust = NOVELTY_BOOST;
|
|
3456
|
+
} else {
|
|
3457
|
+
noveltyAdjust = -NOVELTY_PENALTY;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
return {
|
|
3461
|
+
workflow: c.workflow,
|
|
3462
|
+
score: Math.max(0, Math.min(1, c.score + noveltyAdjust)),
|
|
3463
|
+
...c.cluster ? { clusterPattern: c.cluster.pattern } : {}
|
|
2713
3464
|
};
|
|
2714
3465
|
}).sort((a, b) => b.score - a.score);
|
|
2715
3466
|
}
|
|
@@ -2726,14 +3477,32 @@ function buildSearchCorpus(w) {
|
|
|
2726
3477
|
});
|
|
2727
3478
|
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
2728
3479
|
}
|
|
2729
|
-
var
|
|
3480
|
+
var _rawSize = parseInt(process.env["KAIROS_LIBRARY_SIZE"] ?? "500", 10);
|
|
3481
|
+
var MAX_LIBRARY_SIZE = Number.isFinite(_rawSize) && _rawSize >= 10 ? _rawSize : 500;
|
|
3482
|
+
function evictionScore(m) {
|
|
3483
|
+
return (m.deployCount ?? 0) * 3 + (m.timesRetrieved ?? 0) + (m.outcomeStats?.totalUses ?? 0);
|
|
3484
|
+
}
|
|
3485
|
+
function isValidMeta(item) {
|
|
3486
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
3487
|
+
}
|
|
3488
|
+
function isValidOldEntry(item) {
|
|
3489
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
|
|
3490
|
+
item.workflow.nodes
|
|
3491
|
+
);
|
|
3492
|
+
}
|
|
2730
3493
|
var FileLibrary = class {
|
|
2731
3494
|
dir;
|
|
2732
|
-
|
|
3495
|
+
meta = [];
|
|
2733
3496
|
initPromise = null;
|
|
2734
3497
|
writeQueue = Promise.resolve();
|
|
2735
3498
|
constructor(dir) {
|
|
2736
|
-
this.dir = dir ?? (0,
|
|
3499
|
+
this.dir = dir ?? (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".kairos", "library");
|
|
3500
|
+
}
|
|
3501
|
+
get workflowsDir() {
|
|
3502
|
+
return (0, import_node_path7.join)(this.dir, "workflows");
|
|
3503
|
+
}
|
|
3504
|
+
workflowFilePath(id) {
|
|
3505
|
+
return (0, import_node_path7.join)(this.workflowsDir, `${id}.json`);
|
|
2737
3506
|
}
|
|
2738
3507
|
async initialize() {
|
|
2739
3508
|
if (!this.initPromise) {
|
|
@@ -2743,61 +3512,197 @@ var FileLibrary = class {
|
|
|
2743
3512
|
}
|
|
2744
3513
|
async doInitialize() {
|
|
2745
3514
|
await (0, import_promises4.mkdir)(this.dir, { recursive: true });
|
|
2746
|
-
const indexPath = (0,
|
|
3515
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3516
|
+
let workflowsDirExists = false;
|
|
2747
3517
|
try {
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
);
|
|
3518
|
+
await (0, import_promises4.stat)(this.workflowsDir);
|
|
3519
|
+
workflowsDirExists = true;
|
|
3520
|
+
} catch {
|
|
3521
|
+
}
|
|
3522
|
+
if (workflowsDirExists) {
|
|
3523
|
+
try {
|
|
3524
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3525
|
+
const parsed = JSON.parse(raw);
|
|
3526
|
+
if (Array.isArray(parsed)) {
|
|
3527
|
+
this.meta = parsed.filter(isValidMeta);
|
|
3528
|
+
}
|
|
3529
|
+
} catch {
|
|
3530
|
+
this.meta = [];
|
|
3531
|
+
}
|
|
3532
|
+
await this.scanForOrphansAndCleanup();
|
|
3533
|
+
} else {
|
|
3534
|
+
try {
|
|
3535
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3536
|
+
const parsed = JSON.parse(raw);
|
|
3537
|
+
if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
|
|
3538
|
+
await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
|
|
3539
|
+
return;
|
|
3540
|
+
}
|
|
3541
|
+
} catch {
|
|
3542
|
+
}
|
|
3543
|
+
this.meta = [];
|
|
3544
|
+
await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
async scanForOrphansAndCleanup() {
|
|
3548
|
+
let entries;
|
|
3549
|
+
try {
|
|
3550
|
+
entries = await (0, import_promises4.readdir)(this.workflowsDir);
|
|
3551
|
+
} catch {
|
|
3552
|
+
return;
|
|
3553
|
+
}
|
|
3554
|
+
const indexedIds = new Set(this.meta.map((m) => m.id));
|
|
3555
|
+
const orphanIds = [];
|
|
3556
|
+
for (const filename of entries) {
|
|
3557
|
+
if (filename.endsWith(".tmp")) {
|
|
3558
|
+
await (0, import_promises4.unlink)((0, import_node_path7.join)(this.workflowsDir, filename)).catch(() => {
|
|
3559
|
+
});
|
|
3560
|
+
continue;
|
|
3561
|
+
}
|
|
3562
|
+
if (!filename.endsWith(".json")) continue;
|
|
3563
|
+
const id = filename.slice(0, -5);
|
|
3564
|
+
if (!indexedIds.has(id)) {
|
|
3565
|
+
orphanIds.push(id);
|
|
2756
3566
|
}
|
|
3567
|
+
}
|
|
3568
|
+
if (orphanIds.length > 0) {
|
|
3569
|
+
console.warn(`[FileLibrary] Found ${orphanIds.length} orphaned workflow file(s) not in index: ${orphanIds.join(", ")}`);
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
/**
|
|
3573
|
+
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
3574
|
+
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
3575
|
+
* meta entry. Rewrites index.json in the new format.
|
|
3576
|
+
*/
|
|
3577
|
+
async migrateFromMonolithic(oldEntries) {
|
|
3578
|
+
await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
|
|
3579
|
+
const newMeta = [];
|
|
3580
|
+
for (const entry of oldEntries) {
|
|
3581
|
+
const wfPath = this.workflowFilePath(entry.id);
|
|
3582
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
3583
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
|
|
3584
|
+
await (0, import_promises4.rename)(tmpPath, wfPath);
|
|
3585
|
+
const { workflow, ...metaFields } = entry;
|
|
3586
|
+
newMeta.push({
|
|
3587
|
+
...metaFields,
|
|
3588
|
+
workflowName: workflow.name,
|
|
3589
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type)
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
this.meta = newMeta;
|
|
3593
|
+
await this.persistNow();
|
|
3594
|
+
}
|
|
3595
|
+
async loadWorkflowFile(id) {
|
|
3596
|
+
try {
|
|
3597
|
+
const raw = await (0, import_promises4.readFile)(this.workflowFilePath(id), "utf-8");
|
|
3598
|
+
return JSON.parse(raw);
|
|
2757
3599
|
} catch {
|
|
2758
|
-
|
|
3600
|
+
return null;
|
|
2759
3601
|
}
|
|
2760
3602
|
}
|
|
3603
|
+
async writeWorkflowFile(id, workflow) {
|
|
3604
|
+
const wfPath = this.workflowFilePath(id);
|
|
3605
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
3606
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
|
|
3607
|
+
await (0, import_promises4.rename)(tmpPath, wfPath);
|
|
3608
|
+
}
|
|
3609
|
+
/**
|
|
3610
|
+
* Build a lightweight StoredWorkflow shell from a meta entry for use in
|
|
3611
|
+
* scoring / clustering. Only node.type is populated in each node — no other
|
|
3612
|
+
* node fields are used by hybridScore or clusterWorkflows.
|
|
3613
|
+
*/
|
|
3614
|
+
makeSearchShell(m) {
|
|
3615
|
+
return {
|
|
3616
|
+
...m,
|
|
3617
|
+
workflow: {
|
|
3618
|
+
name: m.workflowName,
|
|
3619
|
+
nodes: m.cachedNodeTypes.map((type) => ({
|
|
3620
|
+
id: "",
|
|
3621
|
+
name: "",
|
|
3622
|
+
type,
|
|
3623
|
+
typeVersion: 1,
|
|
3624
|
+
position: [0, 0],
|
|
3625
|
+
parameters: {}
|
|
3626
|
+
})),
|
|
3627
|
+
connections: {}
|
|
3628
|
+
}
|
|
3629
|
+
};
|
|
3630
|
+
}
|
|
2761
3631
|
async search(description, options) {
|
|
2762
|
-
const
|
|
2763
|
-
if (
|
|
3632
|
+
const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
|
|
3633
|
+
if (filteredMeta.length === 0) return [];
|
|
2764
3634
|
const limit = options?.limit ?? 3;
|
|
2765
3635
|
const queryTokens = tokenize(description);
|
|
2766
3636
|
if (queryTokens.length === 0) return [];
|
|
2767
|
-
const
|
|
3637
|
+
const shells = filteredMeta.map((m) => this.makeSearchShell(m));
|
|
3638
|
+
const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
|
|
2768
3639
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
2769
|
-
const docCount =
|
|
3640
|
+
const docCount = shells.length;
|
|
2770
3641
|
const idf = /* @__PURE__ */ new Map();
|
|
3642
|
+
const idfCeiling = Math.log(docCount + 1) + 1;
|
|
2771
3643
|
const allTokens = new Set(queryTokens);
|
|
2772
3644
|
for (const token of allTokens) {
|
|
2773
3645
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
2774
|
-
|
|
3646
|
+
const rawIdf = Math.log((docCount + 1) / (docsWithToken + 1)) + 1;
|
|
3647
|
+
idf.set(token, rawIdf / idfCeiling);
|
|
2775
3648
|
}
|
|
2776
|
-
const scored = hybridScore(queryTokens, description,
|
|
2777
|
-
const clusters = clusterWorkflows(
|
|
3649
|
+
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
3650
|
+
const clusters = clusterWorkflows(shells);
|
|
2778
3651
|
const reranked = rerank(scored, clusters).slice(0, limit);
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
for (const r of results) {
|
|
2784
|
-
r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
|
|
2785
|
-
}
|
|
2786
|
-
this.persist();
|
|
3652
|
+
if (reranked.length === 0) return [];
|
|
3653
|
+
for (const r of reranked) {
|
|
3654
|
+
const m = this.meta.find((m2) => m2.id === r.workflow.id);
|
|
3655
|
+
if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
|
|
2787
3656
|
}
|
|
2788
|
-
|
|
3657
|
+
this.persist();
|
|
3658
|
+
const results = await Promise.all(
|
|
3659
|
+
reranked.map(async (r) => {
|
|
3660
|
+
const m = this.meta.find((meta) => meta.id === r.workflow.id);
|
|
3661
|
+
const workflow = await this.loadWorkflowFile(r.workflow.id);
|
|
3662
|
+
if (!workflow) return null;
|
|
3663
|
+
return {
|
|
3664
|
+
workflow: { ...m, workflow },
|
|
3665
|
+
score: r.score,
|
|
3666
|
+
mode: scoreToMode(r.score)
|
|
3667
|
+
};
|
|
3668
|
+
})
|
|
3669
|
+
);
|
|
3670
|
+
return results.filter((r) => r !== null);
|
|
2789
3671
|
}
|
|
2790
3672
|
async save(workflow, metadata) {
|
|
3673
|
+
const existingByN8nId = metadata.n8nWorkflowId ? this.meta.find((m) => m.n8nWorkflowId === metadata.n8nWorkflowId) : void 0;
|
|
3674
|
+
const normalizedDesc = metadata.description.trim().toLowerCase();
|
|
3675
|
+
const existing = existingByN8nId ?? this.meta.find((m) => m.description.trim().toLowerCase() === normalizedDesc);
|
|
3676
|
+
if (existing) {
|
|
3677
|
+
existing.description = metadata.description;
|
|
3678
|
+
existing.workflowName = workflow.name;
|
|
3679
|
+
existing.cachedNodeTypes = workflow.nodes.map((n) => n.type);
|
|
3680
|
+
if (metadata.n8nWorkflowId) existing.n8nWorkflowId = metadata.n8nWorkflowId;
|
|
3681
|
+
if (metadata.generationAttempts != null) {
|
|
3682
|
+
existing.generationAttempts = metadata.generationAttempts;
|
|
3683
|
+
}
|
|
3684
|
+
if (metadata.failurePatterns?.length) {
|
|
3685
|
+
existing.failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
3686
|
+
}
|
|
3687
|
+
if (metadata.tags?.length) {
|
|
3688
|
+
existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...metadata.tags])];
|
|
3689
|
+
}
|
|
3690
|
+
await this.writeWorkflowFile(existing.id, workflow);
|
|
3691
|
+
await this.persist();
|
|
3692
|
+
return existing.id;
|
|
3693
|
+
}
|
|
2791
3694
|
const id = generateUUID();
|
|
3695
|
+
await this.writeWorkflowFile(id, workflow);
|
|
2792
3696
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
2793
|
-
const
|
|
3697
|
+
const meta = {
|
|
2794
3698
|
id,
|
|
2795
|
-
workflow,
|
|
2796
3699
|
description: metadata.description,
|
|
2797
3700
|
tags: metadata.tags ?? [],
|
|
2798
3701
|
platform: metadata.platform ?? "n8n",
|
|
2799
3702
|
deployCount: 0,
|
|
2800
3703
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3704
|
+
workflowName: workflow.name,
|
|
3705
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type),
|
|
2801
3706
|
...failurePatterns?.length ? { failurePatterns } : {},
|
|
2802
3707
|
...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
|
|
2803
3708
|
...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
|
|
@@ -2807,33 +3712,39 @@ var FileLibrary = class {
|
|
|
2807
3712
|
...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
|
|
2808
3713
|
...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
|
|
2809
3714
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
2810
|
-
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
3715
|
+
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {},
|
|
3716
|
+
...metadata.n8nWorkflowId ? { n8nWorkflowId: metadata.n8nWorkflowId } : {}
|
|
2811
3717
|
};
|
|
2812
|
-
this.
|
|
2813
|
-
if (this.
|
|
2814
|
-
this.
|
|
2815
|
-
|
|
3718
|
+
this.meta.push(meta);
|
|
3719
|
+
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
3720
|
+
this.meta.sort((a, b) => {
|
|
3721
|
+
if (a.id === id) return -1;
|
|
3722
|
+
if (b.id === id) return 1;
|
|
3723
|
+
return evictionScore(b) - evictionScore(a);
|
|
3724
|
+
});
|
|
3725
|
+
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
2816
3726
|
}
|
|
2817
3727
|
await this.persist();
|
|
2818
3728
|
return id;
|
|
2819
3729
|
}
|
|
2820
|
-
async recordDeployment(id) {
|
|
2821
|
-
const
|
|
2822
|
-
if (
|
|
2823
|
-
|
|
2824
|
-
|
|
3730
|
+
async recordDeployment(id, n8nWorkflowId) {
|
|
3731
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3732
|
+
if (m) {
|
|
3733
|
+
m.deployCount++;
|
|
3734
|
+
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3735
|
+
if (n8nWorkflowId) m.n8nWorkflowId = n8nWorkflowId;
|
|
2825
3736
|
await this.persist();
|
|
2826
3737
|
}
|
|
2827
3738
|
}
|
|
2828
3739
|
async recordOutcome(id, outcome) {
|
|
2829
|
-
const
|
|
2830
|
-
if (!
|
|
3740
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3741
|
+
if (!m) return;
|
|
2831
3742
|
if (outcome.mode === "direct") {
|
|
2832
|
-
|
|
3743
|
+
m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
|
|
2833
3744
|
} else {
|
|
2834
|
-
|
|
3745
|
+
m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
|
|
2835
3746
|
}
|
|
2836
|
-
const stats =
|
|
3747
|
+
const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
|
|
2837
3748
|
stats.totalUses++;
|
|
2838
3749
|
stats.totalAttempts += outcome.attempts;
|
|
2839
3750
|
if (outcome.firstTryPass) stats.firstTryPasses++;
|
|
@@ -2841,24 +3752,35 @@ var FileLibrary = class {
|
|
|
2841
3752
|
const key = String(rule);
|
|
2842
3753
|
stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
|
|
2843
3754
|
}
|
|
2844
|
-
|
|
3755
|
+
m.outcomeStats = stats;
|
|
2845
3756
|
await this.persist();
|
|
2846
3757
|
}
|
|
2847
3758
|
async drain() {
|
|
2848
3759
|
await this.writeQueue;
|
|
2849
3760
|
}
|
|
2850
3761
|
async get(id) {
|
|
2851
|
-
|
|
3762
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3763
|
+
if (!m) return null;
|
|
3764
|
+
const workflow = await this.loadWorkflowFile(id);
|
|
3765
|
+
if (!workflow) return null;
|
|
3766
|
+
return { ...m, workflow };
|
|
2852
3767
|
}
|
|
2853
3768
|
async list(filters) {
|
|
2854
|
-
let
|
|
3769
|
+
let filtered = this.meta;
|
|
2855
3770
|
if (filters?.platform) {
|
|
2856
|
-
|
|
3771
|
+
filtered = filtered.filter((m) => m.platform === filters.platform);
|
|
2857
3772
|
}
|
|
2858
3773
|
if (filters?.tags && filters.tags.length > 0) {
|
|
2859
|
-
|
|
3774
|
+
filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
|
|
2860
3775
|
}
|
|
2861
|
-
|
|
3776
|
+
const results = await Promise.all(
|
|
3777
|
+
filtered.map(async (m) => {
|
|
3778
|
+
const workflow = await this.loadWorkflowFile(m.id);
|
|
3779
|
+
if (!workflow) return null;
|
|
3780
|
+
return { ...m, workflow };
|
|
3781
|
+
})
|
|
3782
|
+
);
|
|
3783
|
+
return results.filter((r) => r !== null);
|
|
2862
3784
|
}
|
|
2863
3785
|
deduplicateFailurePatterns(patterns) {
|
|
2864
3786
|
if (!patterns?.length) return void 0;
|
|
@@ -2873,12 +3795,98 @@ var FileLibrary = class {
|
|
|
2873
3795
|
}
|
|
2874
3796
|
return [...map.values()];
|
|
2875
3797
|
}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
3798
|
+
// ── Cross-process file locking ────────────────────────────────────────────
|
|
3799
|
+
// Uses O_EXCL (exclusive create) which is atomic on POSIX and Windows NTFS.
|
|
3800
|
+
// Protects the read-modify-write cycle in persist() from concurrent writers
|
|
3801
|
+
// in separate OS processes (e.g. MCP server + CLI running simultaneously).
|
|
3802
|
+
get lockPath() {
|
|
3803
|
+
return (0, import_node_path7.join)(this.dir, ".index.lock");
|
|
3804
|
+
}
|
|
3805
|
+
async acquireLock(timeoutMs = 3e3) {
|
|
3806
|
+
const deadline = Date.now() + timeoutMs;
|
|
3807
|
+
let delayMs = 10;
|
|
3808
|
+
while (true) {
|
|
3809
|
+
try {
|
|
3810
|
+
const fh = await (0, import_promises4.open)(this.lockPath, "wx");
|
|
3811
|
+
await fh.writeFile(String(process.pid));
|
|
3812
|
+
await fh.close();
|
|
3813
|
+
return async () => {
|
|
3814
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3815
|
+
});
|
|
3816
|
+
};
|
|
3817
|
+
} catch {
|
|
3818
|
+
try {
|
|
3819
|
+
const content = await (0, import_promises4.readFile)(this.lockPath, "utf-8");
|
|
3820
|
+
const lockPid = parseInt(content.trim(), 10);
|
|
3821
|
+
const fileStat = await (0, import_promises4.stat)(this.lockPath);
|
|
3822
|
+
const ageMs = Date.now() - fileStat.mtimeMs;
|
|
3823
|
+
if (ageMs > 1e4) {
|
|
3824
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3825
|
+
});
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3828
|
+
if (!isNaN(lockPid)) {
|
|
3829
|
+
try {
|
|
3830
|
+
process.kill(lockPid, 0);
|
|
3831
|
+
} catch {
|
|
3832
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3833
|
+
});
|
|
3834
|
+
continue;
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
} catch {
|
|
3838
|
+
continue;
|
|
3839
|
+
}
|
|
3840
|
+
if (Date.now() > deadline) {
|
|
3841
|
+
return async () => {
|
|
3842
|
+
};
|
|
3843
|
+
}
|
|
3844
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
3845
|
+
delayMs = Math.min(delayMs * 1.5, 200);
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
/**
|
|
3850
|
+
* Direct write used only during migration (before writeQueue is needed).
|
|
3851
|
+
*/
|
|
3852
|
+
async persistNow() {
|
|
3853
|
+
const releaseLock = await this.acquireLock();
|
|
3854
|
+
try {
|
|
3855
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
2879
3856
|
const tmpPath = `${indexPath}.tmp`;
|
|
2880
|
-
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.
|
|
3857
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
2881
3858
|
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3859
|
+
} finally {
|
|
3860
|
+
await releaseLock();
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
persist() {
|
|
3864
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
3865
|
+
const releaseLock = await this.acquireLock();
|
|
3866
|
+
try {
|
|
3867
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3868
|
+
let onDisk = [];
|
|
3869
|
+
try {
|
|
3870
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3871
|
+
const parsed = JSON.parse(raw);
|
|
3872
|
+
if (Array.isArray(parsed)) {
|
|
3873
|
+
onDisk = parsed.filter(isValidMeta);
|
|
3874
|
+
}
|
|
3875
|
+
} catch {
|
|
3876
|
+
}
|
|
3877
|
+
const ourIds = new Set(this.meta.map((m) => m.id));
|
|
3878
|
+
const external = onDisk.filter((m) => !ourIds.has(m.id));
|
|
3879
|
+
let merged = [...this.meta, ...external];
|
|
3880
|
+
if (merged.length > MAX_LIBRARY_SIZE) {
|
|
3881
|
+
merged.sort((a, b) => evictionScore(b) - evictionScore(a));
|
|
3882
|
+
merged = merged.slice(0, MAX_LIBRARY_SIZE);
|
|
3883
|
+
}
|
|
3884
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
3885
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
3886
|
+
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3887
|
+
} finally {
|
|
3888
|
+
await releaseLock();
|
|
3889
|
+
}
|
|
2882
3890
|
});
|
|
2883
3891
|
return this.writeQueue;
|
|
2884
3892
|
}
|
|
@@ -2900,6 +3908,19 @@ var SECRET_PATTERNS = [
|
|
|
2900
3908
|
/AIza[a-zA-Z0-9_-]{35}/,
|
|
2901
3909
|
/AKIA[A-Z0-9]{16}/
|
|
2902
3910
|
];
|
|
3911
|
+
var SECRET_PREFIXES = ["sk-", "ghp_", "xoxb-", "AIza", "AKIA"];
|
|
3912
|
+
function collectExpressionStrings(obj, out = []) {
|
|
3913
|
+
if (typeof obj === "string") {
|
|
3914
|
+
if (obj.includes("={{")) out.push(obj);
|
|
3915
|
+
} else if (Array.isArray(obj)) {
|
|
3916
|
+
for (const item of obj) collectExpressionStrings(item, out);
|
|
3917
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
3918
|
+
for (const val of Object.values(obj)) {
|
|
3919
|
+
collectExpressionStrings(val, out);
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
return out;
|
|
3923
|
+
}
|
|
2903
3924
|
function assessTemplateSafety(workflow) {
|
|
2904
3925
|
const reasons = [];
|
|
2905
3926
|
let worst = "safe";
|
|
@@ -2922,6 +3943,15 @@ function assessTemplateSafety(workflow) {
|
|
|
2922
3943
|
break;
|
|
2923
3944
|
}
|
|
2924
3945
|
}
|
|
3946
|
+
const expressions = collectExpressionStrings(node.parameters);
|
|
3947
|
+
for (const expr of expressions) {
|
|
3948
|
+
for (const prefix of SECRET_PREFIXES) {
|
|
3949
|
+
if (expr.includes(prefix)) {
|
|
3950
|
+
escalate("review", `Node "${node.name}" has an expression containing credential-like prefix "${prefix}"`);
|
|
3951
|
+
break;
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
2925
3955
|
}
|
|
2926
3956
|
return { trustLevel: worst, reasons };
|
|
2927
3957
|
}
|
|
@@ -2979,12 +4009,26 @@ var TemplateSyncer = class {
|
|
|
2979
4009
|
}
|
|
2980
4010
|
return progress;
|
|
2981
4011
|
}
|
|
4012
|
+
async fetchWithBackoff(url, maxRetries = 3) {
|
|
4013
|
+
let delayMs = DELAY_BETWEEN_FETCHES_MS;
|
|
4014
|
+
let lastResponse;
|
|
4015
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
4016
|
+
lastResponse = await fetch(url);
|
|
4017
|
+
if (lastResponse.status !== 429 && lastResponse.status !== 503) return lastResponse;
|
|
4018
|
+
if (attempt === maxRetries) break;
|
|
4019
|
+
const retryAfterHeader = lastResponse.headers.get("Retry-After");
|
|
4020
|
+
const waitMs = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : delayMs * Math.pow(2, attempt);
|
|
4021
|
+
this.logger.warn(`HTTP ${lastResponse.status} from template API, retrying in ${waitMs}ms`, { url, attempt });
|
|
4022
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
4023
|
+
}
|
|
4024
|
+
return lastResponse;
|
|
4025
|
+
}
|
|
2982
4026
|
async fetchTemplateIds(max, progress) {
|
|
2983
4027
|
const ids = [];
|
|
2984
4028
|
let page = 1;
|
|
2985
4029
|
while (ids.length < max) {
|
|
2986
4030
|
const url = `${N8N_TEMPLATE_API}/search?page=${page}&rows=${PAGE_SIZE}`;
|
|
2987
|
-
const response = await
|
|
4031
|
+
const response = await this.fetchWithBackoff(url);
|
|
2988
4032
|
if (!response.ok) break;
|
|
2989
4033
|
const data = await response.json();
|
|
2990
4034
|
progress.total = Math.min(data.totalWorkflows, max);
|
|
@@ -3004,7 +4048,7 @@ var TemplateSyncer = class {
|
|
|
3004
4048
|
}
|
|
3005
4049
|
async processTemplate(id, progress) {
|
|
3006
4050
|
const url = `${N8N_TEMPLATE_API}/workflows/${id}`;
|
|
3007
|
-
const response = await
|
|
4051
|
+
const response = await this.fetchWithBackoff(url);
|
|
3008
4052
|
if (!response.ok) return;
|
|
3009
4053
|
const data = await response.json();
|
|
3010
4054
|
const templateMeta = data.workflow;
|
|
@@ -3066,7 +4110,9 @@ Kairos SDK \u2014 LLM-powered n8n workflow generation
|
|
|
3066
4110
|
Usage:
|
|
3067
4111
|
kairos init First-time setup wizard
|
|
3068
4112
|
kairos build <description> [options]
|
|
4113
|
+
kairos replace <n8n-id> <description>
|
|
3069
4114
|
kairos patterns [options]
|
|
4115
|
+
kairos sessions [options]
|
|
3070
4116
|
kairos list
|
|
3071
4117
|
kairos get <id>
|
|
3072
4118
|
kairos activate <id>
|
|
@@ -3083,15 +4129,23 @@ Patterns options:
|
|
|
3083
4129
|
--days <days> Analysis window (default: 30)
|
|
3084
4130
|
--json Output raw JSON instead of summary
|
|
3085
4131
|
|
|
4132
|
+
Sessions options:
|
|
4133
|
+
--limit <n> Number of recent sessions to show (default: 20)
|
|
4134
|
+
--json Output raw JSON instead of summary
|
|
4135
|
+
|
|
3086
4136
|
Sync options:
|
|
3087
4137
|
--max <count> Maximum templates to fetch (default: 500)
|
|
3088
4138
|
|
|
3089
4139
|
Environment variables:
|
|
3090
|
-
ANTHROPIC_API_KEY
|
|
3091
|
-
N8N_BASE_URL
|
|
3092
|
-
N8N_API_KEY
|
|
3093
|
-
KAIROS_MODEL
|
|
3094
|
-
KAIROS_TELEMETRY
|
|
4140
|
+
ANTHROPIC_API_KEY Anthropic API key (required)
|
|
4141
|
+
N8N_BASE_URL n8n instance URL (required for deploy, optional for --dry-run)
|
|
4142
|
+
N8N_API_KEY n8n API key (required for deploy, optional for --dry-run)
|
|
4143
|
+
KAIROS_MODEL Claude model override (default: claude-sonnet-4-6)
|
|
4144
|
+
KAIROS_TELEMETRY Set to "true" or a directory path to enable telemetry logging
|
|
4145
|
+
KAIROS_PROMPT_PROFILE minimal | standard | rich (default: standard)
|
|
4146
|
+
minimal: base prompt only, no library context, top 3 patterns
|
|
4147
|
+
standard: full library context, top 10 patterns (default)
|
|
4148
|
+
rich: full library context, top 15 patterns, proactive expression guidance
|
|
3095
4149
|
`;
|
|
3096
4150
|
function getEnvOrExit(name) {
|
|
3097
4151
|
const val = process.env[name];
|
|
@@ -3189,6 +4243,27 @@ async function handleBuild(positional, flags) {
|
|
|
3189
4243
|
...result.dryRun ? { workflow: result.workflow } : {}
|
|
3190
4244
|
}, null, 2));
|
|
3191
4245
|
}
|
|
4246
|
+
async function handleReplace(positional) {
|
|
4247
|
+
const id = positional[0];
|
|
4248
|
+
const description = positional.slice(1).join(" ");
|
|
4249
|
+
if (!id || !description) {
|
|
4250
|
+
console.error("Usage: kairos replace <n8n-workflow-id> <description>");
|
|
4251
|
+
process.exit(1);
|
|
4252
|
+
}
|
|
4253
|
+
const kairos = createClient();
|
|
4254
|
+
const start = Date.now();
|
|
4255
|
+
console.error(`Replacing workflow ${id}...`);
|
|
4256
|
+
const result = await kairos.replace(id, description);
|
|
4257
|
+
await kairos.drain();
|
|
4258
|
+
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
4259
|
+
console.error(`Done in ${elapsed}s (${result.generationAttempts} attempt${result.generationAttempts > 1 ? "s" : ""})`);
|
|
4260
|
+
console.error("");
|
|
4261
|
+
console.log(JSON.stringify({
|
|
4262
|
+
workflowId: result.workflowId,
|
|
4263
|
+
name: result.name,
|
|
4264
|
+
generationAttempts: result.generationAttempts
|
|
4265
|
+
}, null, 2));
|
|
4266
|
+
}
|
|
3192
4267
|
async function handleList() {
|
|
3193
4268
|
const kairos = createClient();
|
|
3194
4269
|
const workflows = await kairos.list();
|
|
@@ -3253,16 +4328,10 @@ async function handleDelete(positional, flags) {
|
|
|
3253
4328
|
console.log(`Deleted workflow ${id}`);
|
|
3254
4329
|
}
|
|
3255
4330
|
async function handleSyncTemplates(flags) {
|
|
3256
|
-
const
|
|
4331
|
+
const maxRaw = typeof flags["max"] === "string" ? parseInt(flags["max"], 10) : NaN;
|
|
4332
|
+
const max = Number.isNaN(maxRaw) ? 500 : maxRaw;
|
|
3257
4333
|
const library = new FileLibrary();
|
|
3258
|
-
const
|
|
3259
|
-
debug: () => {
|
|
3260
|
-
},
|
|
3261
|
-
info: (msg, meta) => console.error(meta ? `${msg} ${JSON.stringify(meta)}` : msg),
|
|
3262
|
-
warn: (msg, meta) => console.error(meta ? `[warn] ${msg} ${JSON.stringify(meta)}` : `[warn] ${msg}`),
|
|
3263
|
-
error: (msg, meta) => console.error(meta ? `[error] ${msg} ${JSON.stringify(meta)}` : `[error] ${msg}`)
|
|
3264
|
-
};
|
|
3265
|
-
const syncer = new TemplateSyncer(library, logger);
|
|
4334
|
+
const syncer = new TemplateSyncer(library, CLI_LOGGER);
|
|
3266
4335
|
console.error(`Syncing up to ${max} templates from n8n community library...`);
|
|
3267
4336
|
const result = await syncer.sync({
|
|
3268
4337
|
maxTemplates: max,
|
|
@@ -3281,7 +4350,8 @@ async function handleSyncTemplates(flags) {
|
|
|
3281
4350
|
console.error(` Paid: ${result.skippedPaid} (skipped)`);
|
|
3282
4351
|
}
|
|
3283
4352
|
async function handlePatterns(flags) {
|
|
3284
|
-
const
|
|
4353
|
+
const daysRaw = typeof flags["days"] === "string" ? parseInt(flags["days"], 10) : NaN;
|
|
4354
|
+
const days = Number.isNaN(daysRaw) ? 30 : daysRaw;
|
|
3285
4355
|
const analyzer = PatternAnalyzer.fromEnv();
|
|
3286
4356
|
const analysis = await analyzer.analyzeAndSave(days);
|
|
3287
4357
|
if (flags["json"] === true) {
|
|
@@ -3315,6 +4385,10 @@ Active Failure Patterns:`);
|
|
|
3315
4385
|
console.log(` Factors: confidence=${f.rawConfidence} \xD7 impact=${f.impact} \xD7 recency=${f.recency} + boost=${f.stickinessBoost}`);
|
|
3316
4386
|
if (p.mitigation) console.log(` Fix: ${p.mitigation}`);
|
|
3317
4387
|
if (p.exampleMessages.length > 0) console.log(` e.g. ${p.exampleMessages[0]}`);
|
|
4388
|
+
if (p.workflowTypeBreakdown) {
|
|
4389
|
+
const topType = Object.entries(p.workflowTypeBreakdown).sort((a, b) => b[1] - a[1])[0];
|
|
4390
|
+
if (topType) console.log(` Top workflow type: ${topType[0]} (${topType[1]} failures)`);
|
|
4391
|
+
}
|
|
3318
4392
|
}
|
|
3319
4393
|
} else {
|
|
3320
4394
|
console.log(`
|
|
@@ -3355,10 +4429,35 @@ Drift Detection: ${drift.healthy ? "HEALTHY" : "ALERTS FOUND"}`);
|
|
|
3355
4429
|
console.log(`
|
|
3356
4430
|
Patterns saved to ~/.kairos/patterns.json`);
|
|
3357
4431
|
}
|
|
4432
|
+
async function handleSessions(flags) {
|
|
4433
|
+
const limitRaw = typeof flags["limit"] === "string" ? parseInt(flags["limit"], 10) : NaN;
|
|
4434
|
+
const limit = Number.isNaN(limitRaw) ? 20 : limitRaw;
|
|
4435
|
+
const analyzer = PatternAnalyzer.fromEnv();
|
|
4436
|
+
const sessions = await analyzer.getSessions(limit);
|
|
4437
|
+
if (flags["json"] === true) {
|
|
4438
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
if (sessions.length === 0) {
|
|
4442
|
+
console.log("No session history found. Run kairos patterns first to generate session data.");
|
|
4443
|
+
return;
|
|
4444
|
+
}
|
|
4445
|
+
console.log(`
|
|
4446
|
+
Recent Sessions (last ${sessions.length})`);
|
|
4447
|
+
console.log("\u2500".repeat(60));
|
|
4448
|
+
for (const s of [...sessions].reverse()) {
|
|
4449
|
+
const status = s.success ? "\u2713" : "\u2717";
|
|
4450
|
+
const typeTag = s.workflowType ? ` [${s.workflowType}]` : "";
|
|
4451
|
+
const attemptsStr = s.attempts > 1 ? ` (${s.attempts} attempts)` : "";
|
|
4452
|
+
const nameStr = s.workflowName ? ` ${s.workflowName}` : ` ${s.description.slice(0, 50)}`;
|
|
4453
|
+
const rulesStr = s.failedRules.length > 0 ? ` \u2014 rules ${s.failedRules.join(", ")} failed` : "";
|
|
4454
|
+
console.log(`${s.date} ${status}${nameStr}${attemptsStr}${typeTag}${rulesStr}`);
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
3358
4457
|
async function handleInit() {
|
|
3359
4458
|
const { writeFile: writeFile3, readFile: readFile2, mkdir: mkdir4 } = await import("fs/promises");
|
|
3360
|
-
const { join:
|
|
3361
|
-
const { homedir:
|
|
4459
|
+
const { join: join8 } = await import("path");
|
|
4460
|
+
const { homedir: homedir7 } = await import("os");
|
|
3362
4461
|
const readline = await import("readline");
|
|
3363
4462
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
3364
4463
|
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
@@ -3366,7 +4465,7 @@ async function handleInit() {
|
|
|
3366
4465
|
console.error(" Kairos SDK \u2014 Setup Wizard");
|
|
3367
4466
|
console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
3368
4467
|
console.error("");
|
|
3369
|
-
const envPath =
|
|
4468
|
+
const envPath = join8(process.cwd(), ".env");
|
|
3370
4469
|
let existingEnv = "";
|
|
3371
4470
|
try {
|
|
3372
4471
|
existingEnv = await readFile2(envPath, "utf-8");
|
|
@@ -3430,8 +4529,8 @@ async function handleInit() {
|
|
|
3430
4529
|
});
|
|
3431
4530
|
console.error(` Synced ${result.saved} templates (${result.blocked} blocked, ${result.skippedDuplicate} duplicates)`);
|
|
3432
4531
|
}
|
|
3433
|
-
const kairosDir =
|
|
3434
|
-
await mkdir4(
|
|
4532
|
+
const kairosDir = join8(homedir7(), ".kairos");
|
|
4533
|
+
await mkdir4(join8(kairosDir, "telemetry"), { recursive: true });
|
|
3435
4534
|
console.error("");
|
|
3436
4535
|
console.error(" Setup complete! Try:");
|
|
3437
4536
|
console.error("");
|
|
@@ -3440,7 +4539,7 @@ async function handleInit() {
|
|
|
3440
4539
|
}
|
|
3441
4540
|
async function main() {
|
|
3442
4541
|
const { command, positional, flags } = parseArgs(process.argv);
|
|
3443
|
-
if (!command || command === "help" || flags["help"] === true) {
|
|
4542
|
+
if (!command || command === "help" || command === "--help" || flags["help"] === true) {
|
|
3444
4543
|
console.log(HELP);
|
|
3445
4544
|
return;
|
|
3446
4545
|
}
|
|
@@ -3451,9 +4550,15 @@ async function main() {
|
|
|
3451
4550
|
case "build":
|
|
3452
4551
|
await handleBuild(positional, flags);
|
|
3453
4552
|
break;
|
|
4553
|
+
case "replace":
|
|
4554
|
+
await handleReplace(positional);
|
|
4555
|
+
break;
|
|
3454
4556
|
case "patterns":
|
|
3455
4557
|
await handlePatterns(flags);
|
|
3456
4558
|
break;
|
|
4559
|
+
case "sessions":
|
|
4560
|
+
await handleSessions(flags);
|
|
4561
|
+
break;
|
|
3457
4562
|
case "list":
|
|
3458
4563
|
await handleList();
|
|
3459
4564
|
break;
|