@kairos-sdk/core 0.4.5 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -11
- package/dist/{chunk-4TS6GW6O.js → chunk-GVZKMS53.js} +27 -16
- package/dist/chunk-GVZKMS53.js.map +1 -0
- package/dist/{chunk-6CLI43FI.js → chunk-MYAGTDQ2.js} +109 -13
- package/dist/chunk-MYAGTDQ2.js.map +1 -0
- package/dist/{chunk-CR2NHLOH.js → chunk-V2IZBZGB.js} +57 -11
- package/dist/chunk-V2IZBZGB.js.map +1 -0
- package/dist/{chunk-6IXW3WCC.js → chunk-VPPWTMRJ.js} +533 -77
- package/dist/chunk-VPPWTMRJ.js.map +1 -0
- package/dist/cli.cjs +790 -145
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +54 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +740 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.js +5 -5
- package/dist/mcp-server.cjs +960 -347
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +374 -261
- package/dist/mcp-server.js.map +1 -1
- package/dist/{reader-CpUcHhKW.d.ts → reader-B5mV20H6.d.cts} +34 -4
- package/dist/{reader-CpUcHhKW.d.cts → reader-B5mV20H6.d.ts} +34 -4
- package/dist/standalone.cjs +603 -85
- package/dist/standalone.cjs.map +1 -1
- package/dist/standalone.d.cts +2 -1
- package/dist/standalone.d.ts +2 -1
- package/dist/standalone.js +3 -3
- package/package.json +15 -5
- package/dist/chunk-4TS6GW6O.js.map +0 -1
- package/dist/chunk-6CLI43FI.js.map +0 -1
- package/dist/chunk-6IXW3WCC.js.map +0 -1
- package/dist/chunk-CR2NHLOH.js.map +0 -1
package/dist/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,101 @@ 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
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// src/errors/generation-error.ts
|
|
506
|
+
var GenerationError = class extends KairosError {
|
|
507
|
+
constructor(message, cause) {
|
|
508
|
+
super(message, cause);
|
|
509
|
+
this.name = "GenerationError";
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// src/errors/response-parse-error.ts
|
|
514
|
+
var ResponseParseError = class extends KairosError {
|
|
515
|
+
constructor(message, cause) {
|
|
516
|
+
super(message, cause);
|
|
517
|
+
this.name = "ResponseParseError";
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// src/errors/validation-error.ts
|
|
522
|
+
var ValidationError = class extends KairosError {
|
|
523
|
+
constructor(message, issues, attemptMetadata, warnedRules) {
|
|
524
|
+
super(message);
|
|
525
|
+
this.issues = issues;
|
|
526
|
+
this.attemptMetadata = attemptMetadata;
|
|
527
|
+
this.warnedRules = warnedRules;
|
|
528
|
+
this.name = "ValidationError";
|
|
529
|
+
}
|
|
530
|
+
issues;
|
|
531
|
+
attemptMetadata;
|
|
532
|
+
warnedRules;
|
|
376
533
|
};
|
|
377
534
|
|
|
378
535
|
// src/validation/registry.ts
|
|
@@ -475,6 +632,14 @@ var NodeRegistry = class {
|
|
|
475
632
|
if (!def) return true;
|
|
476
633
|
return def.safeTypeVersions.includes(version);
|
|
477
634
|
}
|
|
635
|
+
// Returns true when the version is a positive integer greater than the highest
|
|
636
|
+
// known safe version — indicates a newer release rather than a bad value.
|
|
637
|
+
isVersionNewer(type, version) {
|
|
638
|
+
const def = this.byType.get(type);
|
|
639
|
+
if (!def || def.safeTypeVersions.length === 0) return false;
|
|
640
|
+
const max = Math.max(...def.safeTypeVersions);
|
|
641
|
+
return Number.isInteger(version) && version > max;
|
|
642
|
+
}
|
|
478
643
|
getRequiredParams(type) {
|
|
479
644
|
return this.byType.get(type)?.requiredParams ?? [];
|
|
480
645
|
}
|
|
@@ -527,6 +692,14 @@ var N8nValidator = class {
|
|
|
527
692
|
this.checkRule24(workflow, issues);
|
|
528
693
|
this.checkRule25(workflow, issues);
|
|
529
694
|
this.checkRule26(workflow, issues);
|
|
695
|
+
this.checkRule27(workflow, issues);
|
|
696
|
+
this.checkRule28(workflow, issues);
|
|
697
|
+
this.checkRule29(workflow, issues);
|
|
698
|
+
this.checkRule30(workflow, issues);
|
|
699
|
+
this.checkRule31(workflow, issues);
|
|
700
|
+
this.checkRule32(workflow, issues);
|
|
701
|
+
this.checkRule33(workflow, issues);
|
|
702
|
+
this.checkRule34(workflow, issues);
|
|
530
703
|
if (Array.isArray(workflow.nodes)) {
|
|
531
704
|
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
532
705
|
for (const issue of issues) {
|
|
@@ -778,19 +951,22 @@ var N8nValidator = class {
|
|
|
778
951
|
}
|
|
779
952
|
}
|
|
780
953
|
}
|
|
781
|
-
// Rule 19 (WARN): typeVersion is within known safe range for registered node types
|
|
954
|
+
// Rule 19 (WARN): typeVersion is within known safe range for registered node types.
|
|
955
|
+
// In lenient mode (KAIROS_REGISTRY_STRICT != 'true'), versions higher than the known
|
|
956
|
+
// max are allowed — they likely represent newer n8n releases Kairos hasn't catalogued yet.
|
|
782
957
|
checkRule19(w, issues) {
|
|
783
958
|
if (!Array.isArray(w.nodes)) return;
|
|
959
|
+
const strict = process.env["KAIROS_REGISTRY_STRICT"] === "true";
|
|
784
960
|
for (const node of w.nodes) {
|
|
785
961
|
if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
|
|
786
|
-
if (
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
962
|
+
if (this.registry.isVersionSafe(node.type, node.typeVersion)) continue;
|
|
963
|
+
if (!strict && this.registry.isVersionNewer(node.type, node.typeVersion)) continue;
|
|
964
|
+
this.warn(
|
|
965
|
+
issues,
|
|
966
|
+
19,
|
|
967
|
+
`Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
|
|
968
|
+
node.id
|
|
969
|
+
);
|
|
794
970
|
}
|
|
795
971
|
}
|
|
796
972
|
// Rule 20 (WARN): cycle detection — no node should be reachable from itself
|
|
@@ -839,6 +1015,27 @@ var N8nValidator = class {
|
|
|
839
1015
|
}
|
|
840
1016
|
}
|
|
841
1017
|
}
|
|
1018
|
+
// Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
|
|
1019
|
+
checkRule21(w, issues) {
|
|
1020
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1021
|
+
const webhooksNeedingResponse = w.nodes.filter((n) => {
|
|
1022
|
+
if (!n.type.includes("webhook")) return false;
|
|
1023
|
+
const params = n.parameters;
|
|
1024
|
+
return params?.responseMode === "responseNode";
|
|
1025
|
+
});
|
|
1026
|
+
if (webhooksNeedingResponse.length === 0) return;
|
|
1027
|
+
const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
|
|
1028
|
+
if (!hasRespondNode) {
|
|
1029
|
+
for (const wh of webhooksNeedingResponse) {
|
|
1030
|
+
this.warn(
|
|
1031
|
+
issues,
|
|
1032
|
+
21,
|
|
1033
|
+
`Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
|
|
1034
|
+
wh.id
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
842
1039
|
// Rule 22 (WARN): check requiredParams from registry
|
|
843
1040
|
checkRule22(w, issues) {
|
|
844
1041
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -947,57 +1144,166 @@ var N8nValidator = class {
|
|
|
947
1144
|
walk(params);
|
|
948
1145
|
return expressions;
|
|
949
1146
|
}
|
|
950
|
-
// Rule
|
|
951
|
-
|
|
1147
|
+
// Rule 27 (WARN): httpRequest URL is a placeholder
|
|
1148
|
+
checkRule27(w, issues) {
|
|
952
1149
|
if (!Array.isArray(w.nodes)) return;
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1150
|
+
const PLACEHOLDER_RE = [
|
|
1151
|
+
/^https?:\/\/example\.com/i,
|
|
1152
|
+
/your[-_]?(api[-_]?)?url/i,
|
|
1153
|
+
/^https?:\/\/$/,
|
|
1154
|
+
/^<.+>$/,
|
|
1155
|
+
/placeholder/i
|
|
1156
|
+
];
|
|
1157
|
+
for (const node of w.nodes) {
|
|
1158
|
+
if (node.type !== "n8n-nodes-base.httpRequest") continue;
|
|
1159
|
+
const params = node.parameters;
|
|
1160
|
+
const url = params?.["url"];
|
|
1161
|
+
if (typeof url !== "string" || url.trim() === "") continue;
|
|
1162
|
+
if (PLACEHOLDER_RE.some((re) => re.test(url.trim()))) {
|
|
962
1163
|
this.warn(
|
|
963
1164
|
issues,
|
|
964
|
-
|
|
965
|
-
`
|
|
966
|
-
|
|
1165
|
+
27,
|
|
1166
|
+
`Node "${node.name}" httpRequest URL appears to be a placeholder: "${url}" \u2014 replace with your actual endpoint`,
|
|
1167
|
+
node.id
|
|
967
1168
|
);
|
|
968
1169
|
}
|
|
969
1170
|
}
|
|
970
1171
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1172
|
+
// Rule 28 (WARN): code node with empty or comment-only code
|
|
1173
|
+
checkRule28(w, issues) {
|
|
1174
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1175
|
+
for (const node of w.nodes) {
|
|
1176
|
+
if (node.type !== "n8n-nodes-base.code") continue;
|
|
1177
|
+
const params = node.parameters;
|
|
1178
|
+
const jsCode = typeof params?.["jsCode"] === "string" ? params["jsCode"] : "";
|
|
1179
|
+
const pythonCode = typeof params?.["pythonCode"] === "string" ? params["pythonCode"] : "";
|
|
1180
|
+
const code = jsCode || pythonCode;
|
|
1181
|
+
const stripped = code.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/#[^\n]*/g, "").trim();
|
|
1182
|
+
if (!stripped) {
|
|
1183
|
+
this.warn(issues, 28, `Node "${node.name}" code node has no executable code`, node.id);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
978
1186
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1187
|
+
// Rule 29 (WARN): slack node message operation missing channel
|
|
1188
|
+
checkRule29(w, issues) {
|
|
1189
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1190
|
+
for (const node of w.nodes) {
|
|
1191
|
+
if (node.type !== "n8n-nodes-base.slack") continue;
|
|
1192
|
+
const params = node.parameters;
|
|
1193
|
+
const resource = params?.["resource"];
|
|
1194
|
+
const operation = params?.["operation"];
|
|
1195
|
+
const isMessageOp = resource === "message" || operation === "sendMessage" || operation === "post";
|
|
1196
|
+
if (!isMessageOp) continue;
|
|
1197
|
+
const channel = params?.["channel"] ?? params?.["channelId"];
|
|
1198
|
+
const rlValue = typeof channel === "object" && channel !== null ? channel["value"] : void 0;
|
|
1199
|
+
const isEmpty = channel === void 0 || channel === null || typeof channel === "string" && channel.trim() === "" || typeof channel === "object" && (!rlValue || typeof rlValue === "string" && rlValue.trim() === "");
|
|
1200
|
+
if (isEmpty) {
|
|
1201
|
+
this.warn(issues, 29, `Node "${node.name}" Slack message has no channel specified`, node.id);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
986
1204
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1205
|
+
// Rule 30 (WARN): gmail node send operation missing recipient
|
|
1206
|
+
checkRule30(w, issues) {
|
|
1207
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1208
|
+
for (const node of w.nodes) {
|
|
1209
|
+
if (node.type !== "n8n-nodes-base.gmail") continue;
|
|
1210
|
+
const params = node.parameters;
|
|
1211
|
+
const operation = params?.["operation"];
|
|
1212
|
+
if (operation !== "send") continue;
|
|
1213
|
+
const to = params?.["to"] ?? params?.["toList"];
|
|
1214
|
+
const isEmpty = to === void 0 || to === null || typeof to === "string" && to.trim() === "" || Array.isArray(to) && to.length === 0;
|
|
1215
|
+
if (isEmpty) {
|
|
1216
|
+
this.warn(issues, 30, `Node "${node.name}" gmail send has no recipient (to) specified`, node.id);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
// Rule 31 (WARN): if node with empty conditions
|
|
1221
|
+
checkRule31(w, issues) {
|
|
1222
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1223
|
+
for (const node of w.nodes) {
|
|
1224
|
+
if (node.type !== "n8n-nodes-base.if") continue;
|
|
1225
|
+
const params = node.parameters;
|
|
1226
|
+
const conditions = params?.["conditions"];
|
|
1227
|
+
if (conditions === void 0 || conditions === null) {
|
|
1228
|
+
this.warn(issues, 31, `Node "${node.name}" if node has no conditions defined`, node.id);
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
if (typeof conditions === "object" && !Array.isArray(conditions)) {
|
|
1232
|
+
const conds = conditions["conditions"];
|
|
1233
|
+
if (!Array.isArray(conds) || conds.length === 0) {
|
|
1234
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1235
|
+
}
|
|
1236
|
+
} else if (Array.isArray(conditions) && conditions.length === 0) {
|
|
1237
|
+
this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
// Rule 32 (WARN): set node with no assignments
|
|
1242
|
+
checkRule32(w, issues) {
|
|
1243
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1244
|
+
for (const node of w.nodes) {
|
|
1245
|
+
if (node.type !== "n8n-nodes-base.set") continue;
|
|
1246
|
+
const params = node.parameters;
|
|
1247
|
+
const assignmentsObj = params?.["assignments"];
|
|
1248
|
+
const assignmentsArr = assignmentsObj?.["assignments"];
|
|
1249
|
+
const valuesObj = params?.["values"];
|
|
1250
|
+
const hasV1 = valuesObj && Object.values(valuesObj).some((v) => Array.isArray(v) && v.length > 0);
|
|
1251
|
+
const hasV3 = Array.isArray(assignmentsArr) && assignmentsArr.length > 0;
|
|
1252
|
+
if (!hasV1 && !hasV3) {
|
|
1253
|
+
this.warn(
|
|
1254
|
+
issues,
|
|
1255
|
+
32,
|
|
1256
|
+
`Node "${node.name}" set node has no fields defined \u2014 it will pass data through unchanged`,
|
|
1257
|
+
node.id
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
// Rule 33 (WARN): scheduleTrigger with no schedule rules
|
|
1263
|
+
checkRule33(w, issues) {
|
|
1264
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1265
|
+
for (const node of w.nodes) {
|
|
1266
|
+
if (node.type !== "n8n-nodes-base.scheduleTrigger") continue;
|
|
1267
|
+
const params = node.parameters;
|
|
1268
|
+
const rule = params?.["rule"];
|
|
1269
|
+
const intervals = rule?.["interval"];
|
|
1270
|
+
if (!Array.isArray(intervals) || intervals.length === 0) {
|
|
1271
|
+
this.warn(issues, 33, `Node "${node.name}" scheduleTrigger has no schedule rules defined`, node.id);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
// Rule 34 (WARN): webhook path contains spaces, starts with slash, or looks like a full URL
|
|
1276
|
+
checkRule34(w, issues) {
|
|
1277
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1278
|
+
for (const node of w.nodes) {
|
|
1279
|
+
if (node.type !== "n8n-nodes-base.webhook") continue;
|
|
1280
|
+
const params = node.parameters;
|
|
1281
|
+
const path = params?.["path"];
|
|
1282
|
+
if (typeof path !== "string") continue;
|
|
1283
|
+
if (/\s/.test(path)) {
|
|
1284
|
+
this.warn(
|
|
1285
|
+
issues,
|
|
1286
|
+
34,
|
|
1287
|
+
`Node "${node.name}" webhook path contains spaces: "${path}" \u2014 use hyphens or underscores instead`,
|
|
1288
|
+
node.id
|
|
1289
|
+
);
|
|
1290
|
+
} else if (/^https?:\/\//i.test(path)) {
|
|
1291
|
+
this.warn(
|
|
1292
|
+
issues,
|
|
1293
|
+
34,
|
|
1294
|
+
`Node "${node.name}" webhook path looks like a full URL \u2014 it should be a relative path (e.g. "my-hook")`,
|
|
1295
|
+
node.id
|
|
1296
|
+
);
|
|
1297
|
+
} else if (path.startsWith("/")) {
|
|
1298
|
+
this.warn(
|
|
1299
|
+
issues,
|
|
1300
|
+
34,
|
|
1301
|
+
`Node "${node.name}" webhook path starts with "/" \u2014 n8n adds the leading slash automatically`,
|
|
1302
|
+
node.id
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
997
1306
|
}
|
|
998
|
-
issues;
|
|
999
|
-
attemptMetadata;
|
|
1000
|
-
warnedRules;
|
|
1001
1307
|
};
|
|
1002
1308
|
|
|
1003
1309
|
// src/generation/prompt-builder.ts
|
|
@@ -1204,6 +1510,14 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
|
|
|
1204
1510
|
8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
|
|
1205
1511
|
9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
|
|
1206
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://)
|
|
1207
1521
|
|
|
1208
1522
|
---
|
|
1209
1523
|
|
|
@@ -1220,7 +1534,7 @@ function scoreToMode(score) {
|
|
|
1220
1534
|
}
|
|
1221
1535
|
|
|
1222
1536
|
// src/validation/rule-metadata.ts
|
|
1223
|
-
var VALIDATOR_RULE_IDS = Array.from({ length:
|
|
1537
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 34 }, (_, i) => i + 1);
|
|
1224
1538
|
var RULE_PIPELINE_STAGES = {
|
|
1225
1539
|
1: "node_generation",
|
|
1226
1540
|
2: "node_generation",
|
|
@@ -1247,7 +1561,15 @@ var RULE_PIPELINE_STAGES = {
|
|
|
1247
1561
|
23: "node_generation",
|
|
1248
1562
|
24: "expression_syntax",
|
|
1249
1563
|
25: "expression_syntax",
|
|
1250
|
-
26: "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"
|
|
1251
1573
|
};
|
|
1252
1574
|
var RULE_EXAMPLES = {
|
|
1253
1575
|
17: {
|
|
@@ -1265,6 +1587,38 @@ var RULE_EXAMPLES = {
|
|
|
1265
1587
|
26: {
|
|
1266
1588
|
bad: "$('Fetch Data').json.email",
|
|
1267
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"'
|
|
1268
1622
|
}
|
|
1269
1623
|
};
|
|
1270
1624
|
var RULE_MITIGATIONS = {
|
|
@@ -1293,7 +1647,15 @@ var RULE_MITIGATIONS = {
|
|
|
1293
1647
|
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
1294
1648
|
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
1295
1649
|
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
1296
|
-
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
|
|
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")'
|
|
1297
1659
|
};
|
|
1298
1660
|
|
|
1299
1661
|
// src/generation/prompt-builder.ts
|
|
@@ -1325,18 +1687,37 @@ var PromptBuilder = class {
|
|
|
1325
1687
|
}
|
|
1326
1688
|
build(request, matches, globalFailureRates = [], dynamicCatalog) {
|
|
1327
1689
|
const mode = this.resolveMode(matches);
|
|
1328
|
-
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
|
|
1690
|
+
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog, request.description);
|
|
1329
1691
|
const userMessage = this.buildUserMessage(request, matches, mode);
|
|
1330
1692
|
return { system, userMessage, mode, matches };
|
|
1331
1693
|
}
|
|
1332
|
-
buildCorrectionMessage(request, matches, allIssues, attempt) {
|
|
1694
|
+
buildCorrectionMessage(request, matches, allIssues, attempt, failingRuleIds) {
|
|
1333
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
|
+
}
|
|
1334
1715
|
return `${base}
|
|
1335
1716
|
|
|
1336
1717
|
IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
|
|
1337
1718
|
${allIssues.join("\n")}
|
|
1338
1719
|
|
|
1339
|
-
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}`;
|
|
1340
1721
|
}
|
|
1341
1722
|
resolveMode(matches) {
|
|
1342
1723
|
if (matches.length === 0) return "scratch";
|
|
@@ -1344,7 +1725,7 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1344
1725
|
if (!top) return "scratch";
|
|
1345
1726
|
return scoreToMode(top.score);
|
|
1346
1727
|
}
|
|
1347
|
-
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog) {
|
|
1728
|
+
buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog, description) {
|
|
1348
1729
|
let basePrompt = SYSTEM_PROMPT_V1;
|
|
1349
1730
|
if (dynamicCatalog) {
|
|
1350
1731
|
basePrompt = basePrompt.replace(
|
|
@@ -1404,7 +1785,7 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1404
1785
|
});
|
|
1405
1786
|
}
|
|
1406
1787
|
}
|
|
1407
|
-
const warnings = this.buildFailureWarnings(matches, globalFailureRates);
|
|
1788
|
+
const warnings = this.buildFailureWarnings(matches, globalFailureRates, description);
|
|
1408
1789
|
if (warnings) {
|
|
1409
1790
|
blocks.push({ type: "text", text: warnings });
|
|
1410
1791
|
}
|
|
@@ -1431,15 +1812,34 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1431
1812
|
const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
|
|
1432
1813
|
return patterns.map((p) => p.rule);
|
|
1433
1814
|
}
|
|
1434
|
-
getActivePatterns(maxCount = 10) {
|
|
1815
|
+
getActivePatterns(maxCount = 10, description) {
|
|
1435
1816
|
const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
|
|
1436
1817
|
const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1437
1818
|
const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1438
1819
|
const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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);
|
|
1443
1843
|
this._lastActivePatterns = richPatterns;
|
|
1444
1844
|
if (richPatterns.length > 0) {
|
|
1445
1845
|
return this.buildStageGroupedWarnings(richPatterns, matches);
|
|
@@ -1619,7 +2019,8 @@ var WorkflowDesigner = class {
|
|
|
1619
2019
|
const issueLines = lastErrors.map(
|
|
1620
2020
|
(i) => `- [Rule ${i.rule}] ${i.message}${i.nodeId ? ` (node: ${i.nodeId})` : ""}`
|
|
1621
2021
|
);
|
|
1622
|
-
|
|
2022
|
+
const failingRuleIds = lastErrors.map((i) => i.rule);
|
|
2023
|
+
userMessage = this.promptBuilder.buildCorrectionMessage(request, matches, issueLines, attempt - 1, failingRuleIds);
|
|
1623
2024
|
this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: lastErrors.length });
|
|
1624
2025
|
}
|
|
1625
2026
|
const start = Date.now();
|
|
@@ -1805,19 +2206,20 @@ var TelemetryReader = class {
|
|
|
1805
2206
|
}
|
|
1806
2207
|
const events = await this.readRecentEvents(days);
|
|
1807
2208
|
const buildSessions = new Set(
|
|
1808
|
-
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)
|
|
1809
2210
|
);
|
|
1810
2211
|
const MIN_BUILDS_FOR_RATES = 3;
|
|
1811
2212
|
if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
|
|
1812
2213
|
const ruleSessions = /* @__PURE__ */ new Map();
|
|
1813
2214
|
for (const event of events) {
|
|
1814
2215
|
if (event.eventType !== "generation_attempt") continue;
|
|
1815
|
-
|
|
2216
|
+
const eventKey = event.runId ?? event.sessionId;
|
|
2217
|
+
if (!buildSessions.has(eventKey)) continue;
|
|
1816
2218
|
const data = event.data;
|
|
1817
2219
|
if (data.validationPassed || !data.issues) continue;
|
|
1818
2220
|
for (const issue of data.issues) {
|
|
1819
2221
|
const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
|
|
1820
|
-
entry.sessions.add(
|
|
2222
|
+
entry.sessions.add(eventKey);
|
|
1821
2223
|
entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
|
|
1822
2224
|
ruleSessions.set(issue.rule, entry);
|
|
1823
2225
|
}
|
|
@@ -1859,22 +2261,24 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1859
2261
|
telemetryDir;
|
|
1860
2262
|
outputDir;
|
|
1861
2263
|
_cachedEvents = null;
|
|
2264
|
+
_cachedPreviousPatterns = null;
|
|
1862
2265
|
constructor(telemetryDir) {
|
|
1863
2266
|
const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
|
|
1864
2267
|
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
1865
2268
|
this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
|
|
1866
2269
|
}
|
|
1867
2270
|
async loadPreviousPatterns() {
|
|
2271
|
+
if (this._cachedPreviousPatterns !== null) return this._cachedPreviousPatterns;
|
|
1868
2272
|
try {
|
|
1869
2273
|
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
|
|
1870
2274
|
const prev = JSON.parse(raw);
|
|
1871
2275
|
const version = prev.schemaVersion ?? 0;
|
|
1872
2276
|
const patterns = prev.topFailureRules ?? [];
|
|
1873
|
-
|
|
1874
|
-
return this.migratePatterns(patterns, version);
|
|
2277
|
+
this._cachedPreviousPatterns = version === PATTERN_SCHEMA_VERSION ? patterns : this.migratePatterns(patterns, version);
|
|
1875
2278
|
} catch {
|
|
1876
|
-
|
|
2279
|
+
this._cachedPreviousPatterns = [];
|
|
1877
2280
|
}
|
|
2281
|
+
return this._cachedPreviousPatterns;
|
|
1878
2282
|
}
|
|
1879
2283
|
migratePatterns(patterns, fromVersion) {
|
|
1880
2284
|
let migrated = patterns;
|
|
@@ -1906,7 +2310,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1906
2310
|
this._cachedEvents = events;
|
|
1907
2311
|
const starts = events.filter((e) => e.eventType === "build_start");
|
|
1908
2312
|
const attempts = events.filter((e) => e.eventType === "generation_attempt");
|
|
1909
|
-
const
|
|
2313
|
+
const _passed = attempts.filter(
|
|
1910
2314
|
(a) => a.data.validationPassed === true
|
|
1911
2315
|
);
|
|
1912
2316
|
const failed = attempts.filter(
|
|
@@ -2174,6 +2578,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2174
2578
|
const tmpPath = `${outputPath}.tmp`;
|
|
2175
2579
|
await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
2176
2580
|
await (0, import_promises3.rename)(tmpPath, outputPath);
|
|
2581
|
+
this._cachedPreviousPatterns = null;
|
|
2177
2582
|
const historySummary = {
|
|
2178
2583
|
timestamp: analysis.generatedAt,
|
|
2179
2584
|
totalBuilds: analysis.summary.totalBuilds,
|
|
@@ -2222,7 +2627,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2222
2627
|
})
|
|
2223
2628
|
));
|
|
2224
2629
|
return {
|
|
2225
|
-
sessionId: bc.sessionId,
|
|
2630
|
+
sessionId: bc.runId ?? bc.sessionId,
|
|
2226
2631
|
date: bc.fileDate,
|
|
2227
2632
|
description: data.description ?? "",
|
|
2228
2633
|
workflowType: data.workflowType ?? null,
|
|
@@ -2255,7 +2660,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2255
2660
|
alerts.push({
|
|
2256
2661
|
type: "stale_pattern",
|
|
2257
2662
|
rule: p.rule,
|
|
2258
|
-
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)`
|
|
2259
2664
|
});
|
|
2260
2665
|
}
|
|
2261
2666
|
}
|
|
@@ -2395,11 +2800,10 @@ function inferWorkflowType(description) {
|
|
|
2395
2800
|
// src/client.ts
|
|
2396
2801
|
var import_node_os5 = require("os");
|
|
2397
2802
|
var import_node_path6 = require("path");
|
|
2398
|
-
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
2803
|
+
var DEFAULT_MODEL = process.env["KAIROS_MODEL"] ?? "claude-sonnet-4-6";
|
|
2399
2804
|
var Kairos = class {
|
|
2400
2805
|
provider;
|
|
2401
2806
|
designer;
|
|
2402
|
-
validator;
|
|
2403
2807
|
library;
|
|
2404
2808
|
logger;
|
|
2405
2809
|
telemetry;
|
|
@@ -2425,7 +2829,6 @@ var Kairos = class {
|
|
|
2425
2829
|
const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
|
|
2426
2830
|
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");
|
|
2427
2831
|
this.designer = new WorkflowDesigner(anthropic, this.model, logger, patternsPath);
|
|
2428
|
-
this.validator = new N8nValidator();
|
|
2429
2832
|
this.library = options.library ?? new NullLibrary();
|
|
2430
2833
|
this.logger = logger;
|
|
2431
2834
|
if (options.telemetry === true) {
|
|
@@ -2522,7 +2925,6 @@ var Kairos = class {
|
|
|
2522
2925
|
}
|
|
2523
2926
|
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
2524
2927
|
const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
|
|
2525
|
-
this.saveToLibrary(workflow, description, designResult, matches);
|
|
2526
2928
|
if (options?.dryRun) {
|
|
2527
2929
|
const totalTokensInput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
2528
2930
|
const totalTokensOutput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
@@ -2553,10 +2955,20 @@ var Kairos = class {
|
|
|
2553
2955
|
}
|
|
2554
2956
|
const provider = this.requireProvider();
|
|
2555
2957
|
const deployed = await provider.deploy(workflow);
|
|
2556
|
-
this.
|
|
2958
|
+
this.logger.info("Workflow deployed to n8n", { workflowId: deployed.workflowId, name: deployed.name });
|
|
2959
|
+
this.recordDeploy(deployed.workflowId);
|
|
2557
2960
|
if (options?.activate) {
|
|
2558
2961
|
await provider.activate(deployed.workflowId);
|
|
2559
2962
|
}
|
|
2963
|
+
this.saveToLibrary(workflow, description, designResult, matches, deployed.workflowId);
|
|
2964
|
+
let smokeTestResult;
|
|
2965
|
+
if (options?.smokeTest) {
|
|
2966
|
+
smokeTestResult = await provider.smokeTest(deployed.workflowId, workflow).catch((err) => {
|
|
2967
|
+
this.logger.warn("Smoke test threw unexpectedly", { err: String(err) });
|
|
2968
|
+
return { status: "error", triggerType: "manual", error: String(err) };
|
|
2969
|
+
});
|
|
2970
|
+
this.logger.info("Smoke test complete", { status: smokeTestResult.status, triggerType: smokeTestResult.triggerType });
|
|
2971
|
+
}
|
|
2560
2972
|
const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
2561
2973
|
const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
2562
2974
|
await this.telemetry?.emit("build_complete", {
|
|
@@ -2581,7 +2993,8 @@ var Kairos = class {
|
|
|
2581
2993
|
credentialsNeeded: designResult.credentialsNeeded,
|
|
2582
2994
|
activationRequired: !options?.activate,
|
|
2583
2995
|
generationAttempts: designResult.attempts,
|
|
2584
|
-
dryRun: false
|
|
2996
|
+
dryRun: false,
|
|
2997
|
+
...smokeTestResult !== void 0 ? { smokeTest: smokeTestResult } : {}
|
|
2585
2998
|
};
|
|
2586
2999
|
}
|
|
2587
3000
|
async replace(id, description) {
|
|
@@ -2638,7 +3051,8 @@ var Kairos = class {
|
|
|
2638
3051
|
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
2639
3052
|
const provider = this.requireProvider();
|
|
2640
3053
|
const deployed = await provider.update(id, designResult.workflow);
|
|
2641
|
-
this.
|
|
3054
|
+
this.logger.info("Workflow updated in n8n", { workflowId: deployed.workflowId, name: deployed.name });
|
|
3055
|
+
this.saveToLibrary(designResult.workflow, description, designResult, matches, deployed.workflowId);
|
|
2642
3056
|
this.recordDeploy();
|
|
2643
3057
|
const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
|
|
2644
3058
|
const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
|
|
@@ -2694,10 +3108,10 @@ var Kairos = class {
|
|
|
2694
3108
|
}, runId);
|
|
2695
3109
|
}
|
|
2696
3110
|
}
|
|
2697
|
-
recordDeploy() {
|
|
3111
|
+
recordDeploy(n8nWorkflowId) {
|
|
2698
3112
|
this.saveQueue = this.saveQueue.then(async (savedId) => {
|
|
2699
3113
|
if (savedId) {
|
|
2700
|
-
await this.library.recordDeployment(savedId);
|
|
3114
|
+
await this.library.recordDeployment(savedId, n8nWorkflowId);
|
|
2701
3115
|
}
|
|
2702
3116
|
return savedId;
|
|
2703
3117
|
}).catch((err) => {
|
|
@@ -2705,7 +3119,7 @@ var Kairos = class {
|
|
|
2705
3119
|
return null;
|
|
2706
3120
|
});
|
|
2707
3121
|
}
|
|
2708
|
-
saveToLibrary(workflow, description, designResult, matches) {
|
|
3122
|
+
saveToLibrary(workflow, description, designResult, matches, n8nWorkflowId) {
|
|
2709
3123
|
const failedAttempts = designResult.attemptMetadata.filter((m) => !m.validationPassed);
|
|
2710
3124
|
const failurePatterns = failedAttempts.flatMap(
|
|
2711
3125
|
(m) => m.issues.map((i) => ({ rule: i.rule, message: i.message }))
|
|
@@ -2731,6 +3145,7 @@ var Kairos = class {
|
|
|
2731
3145
|
if (matches.length > 0) metadata.sourceWorkflowIds = matches.map((m) => m.workflow.id);
|
|
2732
3146
|
if (topMatch) metadata.topMatchScore = topMatch.score;
|
|
2733
3147
|
if (designResult.credentialsNeeded.length > 0) metadata.credentialsNeeded = designResult.credentialsNeeded;
|
|
3148
|
+
if (n8nWorkflowId) metadata.n8nWorkflowId = n8nWorkflowId;
|
|
2734
3149
|
const firstTryPass = designResult.attemptMetadata.length > 0 && designResult.attemptMetadata[0].validationPassed;
|
|
2735
3150
|
const failedRules = Array.from(new Set(
|
|
2736
3151
|
designResult.attemptMetadata.filter((m) => !m.validationPassed).flatMap((m) => m.issues.map((i) => i.rule))
|
|
@@ -2794,12 +3209,32 @@ var import_node_path7 = require("path");
|
|
|
2794
3209
|
var import_node_os6 = require("os");
|
|
2795
3210
|
|
|
2796
3211
|
// src/library/scorer.ts
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
3212
|
+
function loadWeights() {
|
|
3213
|
+
const raw = {
|
|
3214
|
+
tfidf: parseFloat(process.env["KAIROS_WEIGHT_TFIDF"] ?? ""),
|
|
3215
|
+
nodeFingerprint: parseFloat(process.env["KAIROS_WEIGHT_JACCARD"] ?? ""),
|
|
3216
|
+
outcome: parseFloat(process.env["KAIROS_WEIGHT_OUTCOME"] ?? ""),
|
|
3217
|
+
deploy: parseFloat(process.env["KAIROS_WEIGHT_DEPLOY"] ?? "")
|
|
3218
|
+
};
|
|
3219
|
+
const defaults = { tfidf: 0.35, nodeFingerprint: 0.3, outcome: 0.2, deploy: 0.15 };
|
|
3220
|
+
const anySet = Object.values(raw).some((v) => !isNaN(v) && v >= 0);
|
|
3221
|
+
if (!anySet) return defaults;
|
|
3222
|
+
const w = {
|
|
3223
|
+
tfidf: !isNaN(raw.tfidf) && raw.tfidf >= 0 ? raw.tfidf : defaults.tfidf,
|
|
3224
|
+
nodeFingerprint: !isNaN(raw.nodeFingerprint) && raw.nodeFingerprint >= 0 ? raw.nodeFingerprint : defaults.nodeFingerprint,
|
|
3225
|
+
outcome: !isNaN(raw.outcome) && raw.outcome >= 0 ? raw.outcome : defaults.outcome,
|
|
3226
|
+
deploy: !isNaN(raw.deploy) && raw.deploy >= 0 ? raw.deploy : defaults.deploy
|
|
3227
|
+
};
|
|
3228
|
+
const total = w.tfidf + w.nodeFingerprint + w.outcome + w.deploy;
|
|
3229
|
+
if (total <= 0) return defaults;
|
|
3230
|
+
return {
|
|
3231
|
+
tfidf: w.tfidf / total,
|
|
3232
|
+
nodeFingerprint: w.nodeFingerprint / total,
|
|
3233
|
+
outcome: w.outcome / total,
|
|
3234
|
+
deploy: w.deploy / total
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
var WEIGHTS = loadWeights();
|
|
2803
3238
|
var NODE_KEYWORDS = {
|
|
2804
3239
|
slack: ["slack", "slackApi"],
|
|
2805
3240
|
email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
|
|
@@ -2984,6 +3419,8 @@ function clusterWorkflows(workflows) {
|
|
|
2984
3419
|
}
|
|
2985
3420
|
return clusters.sort((a, b) => b.members.length - a.members.length);
|
|
2986
3421
|
}
|
|
3422
|
+
var NOVELTY_BOOST = 0.05;
|
|
3423
|
+
var NOVELTY_PENALTY = 0.03;
|
|
2987
3424
|
function rerank(candidates, clusters) {
|
|
2988
3425
|
const clusterMap = /* @__PURE__ */ new Map();
|
|
2989
3426
|
for (const cluster of clusters) {
|
|
@@ -2991,7 +3428,7 @@ function rerank(candidates, clusters) {
|
|
|
2991
3428
|
clusterMap.set(member.id, cluster);
|
|
2992
3429
|
}
|
|
2993
3430
|
}
|
|
2994
|
-
|
|
3431
|
+
const pass1 = candidates.map((c) => {
|
|
2995
3432
|
const cluster = clusterMap.get(c.workflow.id);
|
|
2996
3433
|
let boost = 0;
|
|
2997
3434
|
if (cluster && cluster.avgFirstTryPassRate > 0) {
|
|
@@ -3003,7 +3440,25 @@ function rerank(candidates, clusters) {
|
|
|
3003
3440
|
return {
|
|
3004
3441
|
workflow: c.workflow,
|
|
3005
3442
|
score: Math.max(0, Math.min(1, c.score + boost)),
|
|
3006
|
-
|
|
3443
|
+
cluster
|
|
3444
|
+
};
|
|
3445
|
+
}).sort((a, b) => b.score - a.score);
|
|
3446
|
+
const seenFingerprints = /* @__PURE__ */ new Set();
|
|
3447
|
+
return pass1.map((c) => {
|
|
3448
|
+
const fpKey = c.cluster ? fingerprintKey(c.cluster.fingerprint) : null;
|
|
3449
|
+
let noveltyAdjust = 0;
|
|
3450
|
+
if (fpKey !== null) {
|
|
3451
|
+
if (!seenFingerprints.has(fpKey)) {
|
|
3452
|
+
seenFingerprints.add(fpKey);
|
|
3453
|
+
noveltyAdjust = NOVELTY_BOOST;
|
|
3454
|
+
} else {
|
|
3455
|
+
noveltyAdjust = -NOVELTY_PENALTY;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
return {
|
|
3459
|
+
workflow: c.workflow,
|
|
3460
|
+
score: Math.max(0, Math.min(1, c.score + noveltyAdjust)),
|
|
3461
|
+
...c.cluster ? { clusterPattern: c.cluster.pattern } : {}
|
|
3007
3462
|
};
|
|
3008
3463
|
}).sort((a, b) => b.score - a.score);
|
|
3009
3464
|
}
|
|
@@ -3020,7 +3475,11 @@ function buildSearchCorpus(w) {
|
|
|
3020
3475
|
});
|
|
3021
3476
|
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
3022
3477
|
}
|
|
3023
|
-
var
|
|
3478
|
+
var _rawSize = parseInt(process.env["KAIROS_LIBRARY_SIZE"] ?? "500", 10);
|
|
3479
|
+
var MAX_LIBRARY_SIZE = Number.isFinite(_rawSize) && _rawSize >= 10 ? _rawSize : 500;
|
|
3480
|
+
function evictionScore(m) {
|
|
3481
|
+
return (m.deployCount ?? 0) * 3 + (m.timesRetrieved ?? 0) + (m.outcomeStats?.totalUses ?? 0);
|
|
3482
|
+
}
|
|
3024
3483
|
function isValidMeta(item) {
|
|
3025
3484
|
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
3026
3485
|
}
|
|
@@ -3068,6 +3527,7 @@ var FileLibrary = class {
|
|
|
3068
3527
|
} catch {
|
|
3069
3528
|
this.meta = [];
|
|
3070
3529
|
}
|
|
3530
|
+
await this.scanForOrphansAndCleanup();
|
|
3071
3531
|
} else {
|
|
3072
3532
|
try {
|
|
3073
3533
|
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
@@ -3082,6 +3542,31 @@ var FileLibrary = class {
|
|
|
3082
3542
|
await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
|
|
3083
3543
|
}
|
|
3084
3544
|
}
|
|
3545
|
+
async scanForOrphansAndCleanup() {
|
|
3546
|
+
let entries;
|
|
3547
|
+
try {
|
|
3548
|
+
entries = await (0, import_promises4.readdir)(this.workflowsDir);
|
|
3549
|
+
} catch {
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
const indexedIds = new Set(this.meta.map((m) => m.id));
|
|
3553
|
+
const orphanIds = [];
|
|
3554
|
+
for (const filename of entries) {
|
|
3555
|
+
if (filename.endsWith(".tmp")) {
|
|
3556
|
+
await (0, import_promises4.unlink)((0, import_node_path7.join)(this.workflowsDir, filename)).catch(() => {
|
|
3557
|
+
});
|
|
3558
|
+
continue;
|
|
3559
|
+
}
|
|
3560
|
+
if (!filename.endsWith(".json")) continue;
|
|
3561
|
+
const id = filename.slice(0, -5);
|
|
3562
|
+
if (!indexedIds.has(id)) {
|
|
3563
|
+
orphanIds.push(id);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
if (orphanIds.length > 0) {
|
|
3567
|
+
console.warn(`[FileLibrary] Found ${orphanIds.length} orphaned workflow file(s) not in index: ${orphanIds.join(", ")}`);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3085
3570
|
/**
|
|
3086
3571
|
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
3087
3572
|
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
@@ -3152,10 +3637,12 @@ var FileLibrary = class {
|
|
|
3152
3637
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
3153
3638
|
const docCount = shells.length;
|
|
3154
3639
|
const idf = /* @__PURE__ */ new Map();
|
|
3640
|
+
const idfCeiling = Math.log(docCount + 1) + 1;
|
|
3155
3641
|
const allTokens = new Set(queryTokens);
|
|
3156
3642
|
for (const token of allTokens) {
|
|
3157
3643
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
3158
|
-
|
|
3644
|
+
const rawIdf = Math.log((docCount + 1) / (docsWithToken + 1)) + 1;
|
|
3645
|
+
idf.set(token, rawIdf / idfCeiling);
|
|
3159
3646
|
}
|
|
3160
3647
|
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
3161
3648
|
const clusters = clusterWorkflows(shells);
|
|
@@ -3181,6 +3668,27 @@ var FileLibrary = class {
|
|
|
3181
3668
|
return results.filter((r) => r !== null);
|
|
3182
3669
|
}
|
|
3183
3670
|
async save(workflow, metadata) {
|
|
3671
|
+
const existingByN8nId = metadata.n8nWorkflowId ? this.meta.find((m) => m.n8nWorkflowId === metadata.n8nWorkflowId) : void 0;
|
|
3672
|
+
const normalizedDesc = metadata.description.trim().toLowerCase();
|
|
3673
|
+
const existing = existingByN8nId ?? this.meta.find((m) => m.description.trim().toLowerCase() === normalizedDesc);
|
|
3674
|
+
if (existing) {
|
|
3675
|
+
existing.description = metadata.description;
|
|
3676
|
+
existing.workflowName = workflow.name;
|
|
3677
|
+
existing.cachedNodeTypes = workflow.nodes.map((n) => n.type);
|
|
3678
|
+
if (metadata.n8nWorkflowId) existing.n8nWorkflowId = metadata.n8nWorkflowId;
|
|
3679
|
+
if (metadata.generationAttempts != null) {
|
|
3680
|
+
existing.generationAttempts = metadata.generationAttempts;
|
|
3681
|
+
}
|
|
3682
|
+
if (metadata.failurePatterns?.length) {
|
|
3683
|
+
existing.failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
3684
|
+
}
|
|
3685
|
+
if (metadata.tags?.length) {
|
|
3686
|
+
existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...metadata.tags])];
|
|
3687
|
+
}
|
|
3688
|
+
await this.writeWorkflowFile(existing.id, workflow);
|
|
3689
|
+
await this.persist();
|
|
3690
|
+
return existing.id;
|
|
3691
|
+
}
|
|
3184
3692
|
const id = generateUUID();
|
|
3185
3693
|
await this.writeWorkflowFile(id, workflow);
|
|
3186
3694
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
@@ -3202,25 +3710,27 @@ var FileLibrary = class {
|
|
|
3202
3710
|
...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
|
|
3203
3711
|
...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
|
|
3204
3712
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
3205
|
-
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
3713
|
+
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {},
|
|
3714
|
+
...metadata.n8nWorkflowId ? { n8nWorkflowId: metadata.n8nWorkflowId } : {}
|
|
3206
3715
|
};
|
|
3207
3716
|
this.meta.push(meta);
|
|
3208
3717
|
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
3209
3718
|
this.meta.sort((a, b) => {
|
|
3210
3719
|
if (a.id === id) return -1;
|
|
3211
3720
|
if (b.id === id) return 1;
|
|
3212
|
-
return (b
|
|
3721
|
+
return evictionScore(b) - evictionScore(a);
|
|
3213
3722
|
});
|
|
3214
3723
|
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
3215
3724
|
}
|
|
3216
3725
|
await this.persist();
|
|
3217
3726
|
return id;
|
|
3218
3727
|
}
|
|
3219
|
-
async recordDeployment(id) {
|
|
3728
|
+
async recordDeployment(id, n8nWorkflowId) {
|
|
3220
3729
|
const m = this.meta.find((m2) => m2.id === id);
|
|
3221
3730
|
if (m) {
|
|
3222
3731
|
m.deployCount++;
|
|
3223
3732
|
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3733
|
+
if (n8nWorkflowId) m.n8nWorkflowId = n8nWorkflowId;
|
|
3224
3734
|
await this.persist();
|
|
3225
3735
|
}
|
|
3226
3736
|
}
|
|
@@ -3283,37 +3793,98 @@ var FileLibrary = class {
|
|
|
3283
3793
|
}
|
|
3284
3794
|
return [...map.values()];
|
|
3285
3795
|
}
|
|
3796
|
+
// ── Cross-process file locking ────────────────────────────────────────────
|
|
3797
|
+
// Uses O_EXCL (exclusive create) which is atomic on POSIX and Windows NTFS.
|
|
3798
|
+
// Protects the read-modify-write cycle in persist() from concurrent writers
|
|
3799
|
+
// in separate OS processes (e.g. MCP server + CLI running simultaneously).
|
|
3800
|
+
get lockPath() {
|
|
3801
|
+
return (0, import_node_path7.join)(this.dir, ".index.lock");
|
|
3802
|
+
}
|
|
3803
|
+
async acquireLock(timeoutMs = 3e3) {
|
|
3804
|
+
const deadline = Date.now() + timeoutMs;
|
|
3805
|
+
let delayMs = 10;
|
|
3806
|
+
while (true) {
|
|
3807
|
+
try {
|
|
3808
|
+
const fh = await (0, import_promises4.open)(this.lockPath, "wx");
|
|
3809
|
+
await fh.writeFile(String(process.pid));
|
|
3810
|
+
await fh.close();
|
|
3811
|
+
return async () => {
|
|
3812
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3813
|
+
});
|
|
3814
|
+
};
|
|
3815
|
+
} catch {
|
|
3816
|
+
try {
|
|
3817
|
+
const content = await (0, import_promises4.readFile)(this.lockPath, "utf-8");
|
|
3818
|
+
const lockPid = parseInt(content.trim(), 10);
|
|
3819
|
+
const fileStat = await (0, import_promises4.stat)(this.lockPath);
|
|
3820
|
+
const ageMs = Date.now() - fileStat.mtimeMs;
|
|
3821
|
+
if (ageMs > 1e4) {
|
|
3822
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3823
|
+
});
|
|
3824
|
+
continue;
|
|
3825
|
+
}
|
|
3826
|
+
if (!isNaN(lockPid)) {
|
|
3827
|
+
try {
|
|
3828
|
+
process.kill(lockPid, 0);
|
|
3829
|
+
} catch {
|
|
3830
|
+
await (0, import_promises4.unlink)(this.lockPath).catch(() => {
|
|
3831
|
+
});
|
|
3832
|
+
continue;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
} catch {
|
|
3836
|
+
continue;
|
|
3837
|
+
}
|
|
3838
|
+
if (Date.now() > deadline) {
|
|
3839
|
+
return async () => {
|
|
3840
|
+
};
|
|
3841
|
+
}
|
|
3842
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
3843
|
+
delayMs = Math.min(delayMs * 1.5, 200);
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3286
3847
|
/**
|
|
3287
3848
|
* Direct write used only during migration (before writeQueue is needed).
|
|
3288
3849
|
*/
|
|
3289
3850
|
async persistNow() {
|
|
3290
|
-
const
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3851
|
+
const releaseLock = await this.acquireLock();
|
|
3852
|
+
try {
|
|
3853
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3854
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
3855
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
3856
|
+
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3857
|
+
} finally {
|
|
3858
|
+
await releaseLock();
|
|
3859
|
+
}
|
|
3294
3860
|
}
|
|
3295
3861
|
persist() {
|
|
3296
3862
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
3297
|
-
const
|
|
3298
|
-
let onDisk = [];
|
|
3863
|
+
const releaseLock = await this.acquireLock();
|
|
3299
3864
|
try {
|
|
3300
|
-
const
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3865
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3866
|
+
let onDisk = [];
|
|
3867
|
+
try {
|
|
3868
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3869
|
+
const parsed = JSON.parse(raw);
|
|
3870
|
+
if (Array.isArray(parsed)) {
|
|
3871
|
+
onDisk = parsed.filter(isValidMeta);
|
|
3872
|
+
}
|
|
3873
|
+
} catch {
|
|
3304
3874
|
}
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3875
|
+
const ourIds = new Set(this.meta.map((m) => m.id));
|
|
3876
|
+
const external = onDisk.filter((m) => !ourIds.has(m.id));
|
|
3877
|
+
let merged = [...this.meta, ...external];
|
|
3878
|
+
if (merged.length > MAX_LIBRARY_SIZE) {
|
|
3879
|
+
merged.sort((a, b) => evictionScore(b) - evictionScore(a));
|
|
3880
|
+
merged = merged.slice(0, MAX_LIBRARY_SIZE);
|
|
3881
|
+
}
|
|
3882
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
3883
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
3884
|
+
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3885
|
+
} finally {
|
|
3886
|
+
await releaseLock();
|
|
3313
3887
|
}
|
|
3314
|
-
const tmpPath = `${indexPath}.tmp`;
|
|
3315
|
-
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
3316
|
-
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3317
3888
|
});
|
|
3318
3889
|
return this.writeQueue;
|
|
3319
3890
|
}
|
|
@@ -3335,6 +3906,19 @@ var SECRET_PATTERNS = [
|
|
|
3335
3906
|
/AIza[a-zA-Z0-9_-]{35}/,
|
|
3336
3907
|
/AKIA[A-Z0-9]{16}/
|
|
3337
3908
|
];
|
|
3909
|
+
var SECRET_PREFIXES = ["sk-", "ghp_", "xoxb-", "AIza", "AKIA"];
|
|
3910
|
+
function collectExpressionStrings(obj, out = []) {
|
|
3911
|
+
if (typeof obj === "string") {
|
|
3912
|
+
if (obj.includes("={{")) out.push(obj);
|
|
3913
|
+
} else if (Array.isArray(obj)) {
|
|
3914
|
+
for (const item of obj) collectExpressionStrings(item, out);
|
|
3915
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
3916
|
+
for (const val of Object.values(obj)) {
|
|
3917
|
+
collectExpressionStrings(val, out);
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
return out;
|
|
3921
|
+
}
|
|
3338
3922
|
function assessTemplateSafety(workflow) {
|
|
3339
3923
|
const reasons = [];
|
|
3340
3924
|
let worst = "safe";
|
|
@@ -3357,6 +3941,15 @@ function assessTemplateSafety(workflow) {
|
|
|
3357
3941
|
break;
|
|
3358
3942
|
}
|
|
3359
3943
|
}
|
|
3944
|
+
const expressions = collectExpressionStrings(node.parameters);
|
|
3945
|
+
for (const expr of expressions) {
|
|
3946
|
+
for (const prefix of SECRET_PREFIXES) {
|
|
3947
|
+
if (expr.includes(prefix)) {
|
|
3948
|
+
escalate("review", `Node "${node.name}" has an expression containing credential-like prefix "${prefix}"`);
|
|
3949
|
+
break;
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3360
3953
|
}
|
|
3361
3954
|
return { trustLevel: worst, reasons };
|
|
3362
3955
|
}
|
|
@@ -3414,12 +4007,26 @@ var TemplateSyncer = class {
|
|
|
3414
4007
|
}
|
|
3415
4008
|
return progress;
|
|
3416
4009
|
}
|
|
4010
|
+
async fetchWithBackoff(url, maxRetries = 3) {
|
|
4011
|
+
let delayMs = DELAY_BETWEEN_FETCHES_MS;
|
|
4012
|
+
let lastResponse;
|
|
4013
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
4014
|
+
lastResponse = await fetch(url);
|
|
4015
|
+
if (lastResponse.status !== 429 && lastResponse.status !== 503) return lastResponse;
|
|
4016
|
+
if (attempt === maxRetries) break;
|
|
4017
|
+
const retryAfterHeader = lastResponse.headers.get("Retry-After");
|
|
4018
|
+
const waitMs = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : delayMs * Math.pow(2, attempt);
|
|
4019
|
+
this.logger.warn(`HTTP ${lastResponse.status} from template API, retrying in ${waitMs}ms`, { url, attempt });
|
|
4020
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
4021
|
+
}
|
|
4022
|
+
return lastResponse;
|
|
4023
|
+
}
|
|
3417
4024
|
async fetchTemplateIds(max, progress) {
|
|
3418
4025
|
const ids = [];
|
|
3419
4026
|
let page = 1;
|
|
3420
4027
|
while (ids.length < max) {
|
|
3421
4028
|
const url = `${N8N_TEMPLATE_API}/search?page=${page}&rows=${PAGE_SIZE}`;
|
|
3422
|
-
const response = await
|
|
4029
|
+
const response = await this.fetchWithBackoff(url);
|
|
3423
4030
|
if (!response.ok) break;
|
|
3424
4031
|
const data = await response.json();
|
|
3425
4032
|
progress.total = Math.min(data.totalWorkflows, max);
|
|
@@ -3439,7 +4046,7 @@ var TemplateSyncer = class {
|
|
|
3439
4046
|
}
|
|
3440
4047
|
async processTemplate(id, progress) {
|
|
3441
4048
|
const url = `${N8N_TEMPLATE_API}/workflows/${id}`;
|
|
3442
|
-
const response = await
|
|
4049
|
+
const response = await this.fetchWithBackoff(url);
|
|
3443
4050
|
if (!response.ok) return;
|
|
3444
4051
|
const data = await response.json();
|
|
3445
4052
|
const templateMeta = data.workflow;
|
|
@@ -3501,6 +4108,7 @@ Kairos SDK \u2014 LLM-powered n8n workflow generation
|
|
|
3501
4108
|
Usage:
|
|
3502
4109
|
kairos init First-time setup wizard
|
|
3503
4110
|
kairos build <description> [options]
|
|
4111
|
+
kairos replace <n8n-id> <description>
|
|
3504
4112
|
kairos patterns [options]
|
|
3505
4113
|
kairos sessions [options]
|
|
3506
4114
|
kairos list
|
|
@@ -3514,6 +4122,7 @@ Build options:
|
|
|
3514
4122
|
--dry-run Generate and validate without deploying
|
|
3515
4123
|
--name <name> Override the generated workflow name
|
|
3516
4124
|
--activate Activate the workflow after deployment
|
|
4125
|
+
--smoke-test After deploy, trigger the workflow and verify it runs without error
|
|
3517
4126
|
|
|
3518
4127
|
Patterns options:
|
|
3519
4128
|
--days <days> Analysis window (default: 30)
|
|
@@ -3607,7 +4216,7 @@ function createDryRunClient() {
|
|
|
3607
4216
|
async function handleBuild(positional, flags) {
|
|
3608
4217
|
const description = positional.join(" ");
|
|
3609
4218
|
if (!description) {
|
|
3610
|
-
console.error("Usage: kairos build <description> [--dry-run] [--name <name>] [--activate]");
|
|
4219
|
+
console.error("Usage: kairos build <description> [--dry-run] [--name <name>] [--activate] [--smoke-test]");
|
|
3611
4220
|
process.exit(1);
|
|
3612
4221
|
}
|
|
3613
4222
|
const isDryRun = flags["dry-run"] === true;
|
|
@@ -3617,7 +4226,8 @@ async function handleBuild(positional, flags) {
|
|
|
3617
4226
|
const result = await kairos.build(description, {
|
|
3618
4227
|
dryRun: isDryRun,
|
|
3619
4228
|
...typeof flags["name"] === "string" ? { name: flags["name"] } : {},
|
|
3620
|
-
activate: flags["activate"] === true
|
|
4229
|
+
activate: flags["activate"] === true || flags["smoke-test"] === true,
|
|
4230
|
+
smokeTest: flags["smoke-test"] === true
|
|
3621
4231
|
});
|
|
3622
4232
|
await kairos.drain();
|
|
3623
4233
|
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
@@ -3630,7 +4240,29 @@ async function handleBuild(positional, flags) {
|
|
|
3630
4240
|
activationRequired: result.activationRequired,
|
|
3631
4241
|
dryRun: result.dryRun,
|
|
3632
4242
|
credentialsNeeded: result.credentialsNeeded,
|
|
3633
|
-
...result.dryRun ? { workflow: result.workflow } : {}
|
|
4243
|
+
...result.dryRun ? { workflow: result.workflow } : {},
|
|
4244
|
+
...result.smokeTest ? { smokeTest: result.smokeTest } : {}
|
|
4245
|
+
}, null, 2));
|
|
4246
|
+
}
|
|
4247
|
+
async function handleReplace(positional) {
|
|
4248
|
+
const id = positional[0];
|
|
4249
|
+
const description = positional.slice(1).join(" ");
|
|
4250
|
+
if (!id || !description) {
|
|
4251
|
+
console.error("Usage: kairos replace <n8n-workflow-id> <description>");
|
|
4252
|
+
process.exit(1);
|
|
4253
|
+
}
|
|
4254
|
+
const kairos = createClient();
|
|
4255
|
+
const start = Date.now();
|
|
4256
|
+
console.error(`Replacing workflow ${id}...`);
|
|
4257
|
+
const result = await kairos.replace(id, description);
|
|
4258
|
+
await kairos.drain();
|
|
4259
|
+
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
4260
|
+
console.error(`Done in ${elapsed}s (${result.generationAttempts} attempt${result.generationAttempts > 1 ? "s" : ""})`);
|
|
4261
|
+
console.error("");
|
|
4262
|
+
console.log(JSON.stringify({
|
|
4263
|
+
workflowId: result.workflowId,
|
|
4264
|
+
name: result.name,
|
|
4265
|
+
generationAttempts: result.generationAttempts
|
|
3634
4266
|
}, null, 2));
|
|
3635
4267
|
}
|
|
3636
4268
|
async function handleList() {
|
|
@@ -3700,14 +4332,7 @@ async function handleSyncTemplates(flags) {
|
|
|
3700
4332
|
const maxRaw = typeof flags["max"] === "string" ? parseInt(flags["max"], 10) : NaN;
|
|
3701
4333
|
const max = Number.isNaN(maxRaw) ? 500 : maxRaw;
|
|
3702
4334
|
const library = new FileLibrary();
|
|
3703
|
-
const
|
|
3704
|
-
debug: () => {
|
|
3705
|
-
},
|
|
3706
|
-
info: (msg, meta) => console.error(meta ? `${msg} ${JSON.stringify(meta)}` : msg),
|
|
3707
|
-
warn: (msg, meta) => console.error(meta ? `[warn] ${msg} ${JSON.stringify(meta)}` : `[warn] ${msg}`),
|
|
3708
|
-
error: (msg, meta) => console.error(meta ? `[error] ${msg} ${JSON.stringify(meta)}` : `[error] ${msg}`)
|
|
3709
|
-
};
|
|
3710
|
-
const syncer = new TemplateSyncer(library, logger);
|
|
4335
|
+
const syncer = new TemplateSyncer(library, CLI_LOGGER);
|
|
3711
4336
|
console.error(`Syncing up to ${max} templates from n8n community library...`);
|
|
3712
4337
|
const result = await syncer.sync({
|
|
3713
4338
|
maxTemplates: max,
|
|
@@ -3907,15 +4532,32 @@ async function handleInit() {
|
|
|
3907
4532
|
}
|
|
3908
4533
|
const kairosDir = join8(homedir7(), ".kairos");
|
|
3909
4534
|
await mkdir4(join8(kairosDir, "telemetry"), { recursive: true });
|
|
4535
|
+
const kairosPath = process.execPath ? `${process.execPath.replace(/node$/, "kairos-mcp")}` : "kairos-mcp";
|
|
3910
4536
|
console.error("");
|
|
3911
4537
|
console.error(" Setup complete! Try:");
|
|
3912
4538
|
console.error("");
|
|
3913
4539
|
console.error(' kairos build "Send a Slack message when a webhook fires" --dry-run');
|
|
3914
4540
|
console.error("");
|
|
4541
|
+
console.error(" \u2500\u2500\u2500 Claude Desktop MCP config \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\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
4542
|
+
console.error(" Add this to ~/Library/Application Support/Claude/claude_desktop_config.json:");
|
|
4543
|
+
console.error("");
|
|
4544
|
+
console.error(" {");
|
|
4545
|
+
console.error(' "mcpServers": {');
|
|
4546
|
+
console.error(' "kairos": {');
|
|
4547
|
+
console.error(` "command": "${kairosPath}",`);
|
|
4548
|
+
console.error(' "env": {');
|
|
4549
|
+
console.error(` "ANTHROPIC_API_KEY": "${process.env["ANTHROPIC_API_KEY"] ? "<set>" : "your-key-here"}",`);
|
|
4550
|
+
console.error(` "N8N_BASE_URL": "${process.env["N8N_BASE_URL"] ?? "https://your-n8n-instance"}",`);
|
|
4551
|
+
console.error(` "N8N_API_KEY": "${process.env["N8N_API_KEY"] ? "<set>" : "your-n8n-api-key"}"`);
|
|
4552
|
+
console.error(" }");
|
|
4553
|
+
console.error(" }");
|
|
4554
|
+
console.error(" }");
|
|
4555
|
+
console.error(" }");
|
|
4556
|
+
console.error("");
|
|
3915
4557
|
}
|
|
3916
4558
|
async function main() {
|
|
3917
4559
|
const { command, positional, flags } = parseArgs(process.argv);
|
|
3918
|
-
if (!command || command === "help" || flags["help"] === true) {
|
|
4560
|
+
if (!command || command === "help" || command === "--help" || flags["help"] === true) {
|
|
3919
4561
|
console.log(HELP);
|
|
3920
4562
|
return;
|
|
3921
4563
|
}
|
|
@@ -3926,6 +4568,9 @@ async function main() {
|
|
|
3926
4568
|
case "build":
|
|
3927
4569
|
await handleBuild(positional, flags);
|
|
3928
4570
|
break;
|
|
4571
|
+
case "replace":
|
|
4572
|
+
await handleReplace(positional);
|
|
4573
|
+
break;
|
|
3929
4574
|
case "patterns":
|
|
3930
4575
|
await handlePatterns(flags);
|
|
3931
4576
|
break;
|