@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.
@@ -0,0 +1,2978 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/standalone.ts
21
+ var standalone_exports = {};
22
+ __export(standalone_exports, {
23
+ ApiError: () => ApiError,
24
+ DEFAULT_REGISTRY: () => DEFAULT_REGISTRY,
25
+ FileLibrary: () => FileLibrary,
26
+ GenerationError: () => GenerationError,
27
+ GuardError: () => GuardError,
28
+ KairosError: () => KairosError,
29
+ N8nApiClient: () => N8nApiClient,
30
+ N8nFieldStripper: () => N8nFieldStripper,
31
+ N8nProvider: () => N8nProvider,
32
+ N8nValidator: () => N8nValidator,
33
+ NodeRegistry: () => NodeRegistry,
34
+ NullLibrary: () => NullLibrary,
35
+ PatternAnalyzer: () => PatternAnalyzer,
36
+ ProviderError: () => ProviderError,
37
+ ResponseParseError: () => ResponseParseError,
38
+ TelemetryCollector: () => TelemetryCollector,
39
+ TelemetryReader: () => TelemetryReader,
40
+ TemplateSyncer: () => TemplateSyncer,
41
+ ValidationError: () => ValidationError,
42
+ buildSearchCorpus: () => buildSearchCorpus,
43
+ clusterWorkflows: () => clusterWorkflows,
44
+ hybridScore: () => hybridScore,
45
+ nullLogger: () => nullLogger,
46
+ rerank: () => rerank,
47
+ tokenize: () => tokenize
48
+ });
49
+ module.exports = __toCommonJS(standalone_exports);
50
+
51
+ // src/errors/base.ts
52
+ var KairosError = class extends Error {
53
+ constructor(message, cause) {
54
+ super(message);
55
+ this.cause = cause;
56
+ this.name = "KairosError";
57
+ if (Error.captureStackTrace) {
58
+ Error.captureStackTrace(this, this.constructor);
59
+ }
60
+ }
61
+ cause;
62
+ };
63
+
64
+ // src/errors/guard-error.ts
65
+ var GuardError = class extends KairosError {
66
+ constructor(message) {
67
+ super(message);
68
+ this.name = "GuardError";
69
+ }
70
+ };
71
+
72
+ // src/errors/provider-error.ts
73
+ var ProviderError = class extends KairosError {
74
+ constructor(message, cause) {
75
+ super(message, cause);
76
+ this.name = "ProviderError";
77
+ }
78
+ };
79
+
80
+ // src/providers/n8n/provider.ts
81
+ var SMOKE_TEST_TIMEOUT_MS = 3e4;
82
+ var SMOKE_TEST_POLL_INTERVAL_MS = 1e3;
83
+ var N8nProvider = class {
84
+ constructor(client, stripper) {
85
+ this.client = client;
86
+ this.stripper = stripper;
87
+ }
88
+ client;
89
+ stripper;
90
+ platform = "n8n";
91
+ async deploy(workflow) {
92
+ const stripped = this.stripper.stripForCreate(workflow);
93
+ const response = await this.client.createWorkflow(stripped);
94
+ return { workflowId: response.id, name: response.name };
95
+ }
96
+ async update(id, workflow) {
97
+ const stripped = this.stripper.stripForUpdate(workflow);
98
+ const response = await this.client.updateWorkflow(id, stripped);
99
+ return { workflowId: response.id, name: response.name };
100
+ }
101
+ async get(id) {
102
+ const response = await this.client.getWorkflow(id);
103
+ return {
104
+ name: response.name,
105
+ nodes: response.nodes,
106
+ connections: response.connections,
107
+ ...response.settings !== void 0 ? { settings: response.settings } : {},
108
+ ...response.tags !== void 0 ? { tags: response.tags } : {}
109
+ };
110
+ }
111
+ async list() {
112
+ return this.client.listWorkflows();
113
+ }
114
+ async activate(id) {
115
+ await this.client.activateWorkflow(id);
116
+ }
117
+ async deactivate(id) {
118
+ await this.client.deactivateWorkflow(id);
119
+ }
120
+ async delete(id, options) {
121
+ if (options.confirm !== true) {
122
+ throw new GuardError("delete() requires { confirm: true } to prevent accidental deletion");
123
+ }
124
+ await this.client.deleteWorkflow(id);
125
+ }
126
+ async executions(workflowId, filter) {
127
+ return this.client.getExecutions(workflowId, filter);
128
+ }
129
+ async execution(id) {
130
+ return this.client.getExecution(id);
131
+ }
132
+ async listTags() {
133
+ return this.client.listTags();
134
+ }
135
+ async createTag(name) {
136
+ return this.client.createTag(name);
137
+ }
138
+ async tag(workflowId, tagIds) {
139
+ await this.client.tagWorkflow(workflowId, tagIds);
140
+ }
141
+ async untag(workflowId, tagIds) {
142
+ await this.client.untagWorkflow(workflowId, tagIds);
143
+ }
144
+ async smokeTest(workflowId, workflow) {
145
+ const start = Date.now();
146
+ const trigger = this.detectTrigger(workflow);
147
+ if (trigger.type === "unsupported") {
148
+ return { status: "not-applicable", triggerType: "not-applicable" };
149
+ }
150
+ if (trigger.type === "manual") {
151
+ let executionId;
152
+ try {
153
+ executionId = await this.client.triggerManual(workflowId);
154
+ } catch (err) {
155
+ return { status: "error", triggerType: "manual", durationMs: Date.now() - start, error: String(err) };
156
+ }
157
+ try {
158
+ const execution = await this.pollExecution(executionId);
159
+ const durationMs = Date.now() - start;
160
+ if (execution.status === "success") {
161
+ return { status: "passed", triggerType: "manual", executionId, durationMs };
162
+ }
163
+ return {
164
+ status: "failed",
165
+ triggerType: "manual",
166
+ executionId,
167
+ durationMs,
168
+ error: `Execution ended with status: ${execution.status}`
169
+ };
170
+ } catch (err) {
171
+ return { status: "error", triggerType: "manual", executionId, durationMs: Date.now() - start, error: String(err) };
172
+ }
173
+ }
174
+ try {
175
+ const statusCode = await this.client.triggerWebhookTest(trigger.path);
176
+ const durationMs = Date.now() - start;
177
+ if (statusCode >= 200 && statusCode < 300) {
178
+ return { status: "passed", triggerType: "webhook", durationMs };
179
+ }
180
+ return { status: "failed", triggerType: "webhook", durationMs, error: `Webhook returned HTTP ${statusCode}` };
181
+ } catch (err) {
182
+ return { status: "error", triggerType: "webhook", durationMs: Date.now() - start, error: String(err) };
183
+ }
184
+ }
185
+ detectTrigger(workflow) {
186
+ for (const node of workflow.nodes) {
187
+ if (node.type === "n8n-nodes-base.manualTrigger") return { type: "manual" };
188
+ if (node.type === "n8n-nodes-base.webhook") {
189
+ const params = node.parameters;
190
+ const path = typeof params?.["path"] === "string" ? params["path"] : "webhook";
191
+ return { type: "webhook", path };
192
+ }
193
+ }
194
+ return { type: "unsupported" };
195
+ }
196
+ async pollExecution(executionId) {
197
+ const deadline = Date.now() + SMOKE_TEST_TIMEOUT_MS;
198
+ for (; ; ) {
199
+ const execution = await this.client.getExecution(executionId);
200
+ if (execution.status !== "running" && execution.status !== "waiting") {
201
+ return execution;
202
+ }
203
+ const remaining = deadline - Date.now();
204
+ if (remaining <= 0) break;
205
+ await new Promise((resolve) => setTimeout(resolve, Math.min(SMOKE_TEST_POLL_INTERVAL_MS, remaining)));
206
+ }
207
+ throw new ProviderError(`Smoke test: execution ${executionId} did not complete within ${SMOKE_TEST_TIMEOUT_MS}ms`);
208
+ }
209
+ };
210
+
211
+ // src/errors/api-error.ts
212
+ var ApiError = class extends KairosError {
213
+ constructor(message, statusCode, cause) {
214
+ super(message, cause);
215
+ this.statusCode = statusCode;
216
+ this.name = "ApiError";
217
+ }
218
+ statusCode;
219
+ };
220
+
221
+ // src/utils/retry.ts
222
+ function isTransientNetworkError(err) {
223
+ const TRANSIENT_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "ENOTFOUND", "ECONNABORTED"]);
224
+ let current = err;
225
+ for (let i = 0; i < 4; i++) {
226
+ if (current === null || typeof current !== "object") break;
227
+ const code = current.code;
228
+ if (typeof code === "string" && TRANSIENT_CODES.has(code)) return true;
229
+ current = current.cause;
230
+ }
231
+ return false;
232
+ }
233
+ async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
234
+ let lastError;
235
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
236
+ if (attempt > 0) {
237
+ const jitter = Math.random() * delayMs * 0.5;
238
+ await new Promise((resolve) => setTimeout(resolve, delayMs * 2 ** (attempt - 1) + jitter));
239
+ }
240
+ try {
241
+ return await fn();
242
+ } catch (err) {
243
+ lastError = err;
244
+ if (shouldRetry && !shouldRetry(err)) throw err;
245
+ }
246
+ }
247
+ throw lastError;
248
+ }
249
+ function fetchWithTimeout(url, init, timeoutMs) {
250
+ const controller = new AbortController();
251
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
252
+ return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
253
+ }
254
+
255
+ // src/providers/n8n/api-client.ts
256
+ var EXECUTION_LIMIT_CAP = 100;
257
+ var N8N_API_PAGE_SIZE = 250;
258
+ var REQUEST_TIMEOUT_MS = 3e4;
259
+ var RETRY_ATTEMPTS = 3;
260
+ var RETRY_DELAY_MS = 1e3;
261
+ var N8nApiClient = class {
262
+ constructor(baseUrl, apiKey, logger) {
263
+ this.baseUrl = baseUrl;
264
+ this.apiKey = apiKey;
265
+ this.logger = logger;
266
+ if (!baseUrl || typeof baseUrl !== "string") {
267
+ throw new GuardError("N8nApiClient: baseUrl must be a non-empty string");
268
+ }
269
+ try {
270
+ new URL(baseUrl);
271
+ } catch {
272
+ throw new GuardError(`N8nApiClient: baseUrl is not a valid URL: "${baseUrl}"`);
273
+ }
274
+ if (!apiKey || typeof apiKey !== "string") {
275
+ throw new GuardError("N8nApiClient: apiKey must be a non-empty string");
276
+ }
277
+ }
278
+ baseUrl;
279
+ apiKey;
280
+ logger;
281
+ async request(method, path, body) {
282
+ const url = `${this.baseUrl.replace(/\/$/, "")}/api/v1${path}`;
283
+ this.logger.debug(`n8n ${method} ${path}`);
284
+ const isSafe = method === "GET";
285
+ if (!isSafe) {
286
+ return withRetry(
287
+ () => this.singleRequest(url, method, path, body),
288
+ 2,
289
+ RETRY_DELAY_MS,
290
+ isTransientNetworkError
291
+ );
292
+ }
293
+ return withRetry(
294
+ () => this.singleRequest(url, method, path, body),
295
+ RETRY_ATTEMPTS,
296
+ RETRY_DELAY_MS,
297
+ (err) => err instanceof ProviderError || err instanceof ApiError && err.statusCode === 429
298
+ );
299
+ }
300
+ async singleRequest(url, method, path, body) {
301
+ let response;
302
+ try {
303
+ response = await fetchWithTimeout(url, {
304
+ method,
305
+ headers: {
306
+ "X-N8N-API-KEY": this.apiKey,
307
+ "Content-Type": "application/json",
308
+ Accept: "application/json"
309
+ },
310
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
311
+ }, REQUEST_TIMEOUT_MS);
312
+ } catch (err) {
313
+ throw new ProviderError(`Network error calling n8n API: ${path}`, err);
314
+ }
315
+ if (!response.ok) {
316
+ let errorBody;
317
+ try {
318
+ errorBody = await response.json();
319
+ } catch {
320
+ errorBody = await response.text().catch(() => "");
321
+ }
322
+ this.logger.error(`n8n API error ${response.status} on ${method} ${path}`, {
323
+ status: response.status,
324
+ body: String(errorBody)
325
+ });
326
+ throw new ApiError(
327
+ `n8n API returned ${response.status} for ${method} ${path}: ${JSON.stringify(errorBody)}`,
328
+ response.status,
329
+ errorBody
330
+ );
331
+ }
332
+ if (response.status === 204) return void 0;
333
+ return response.json();
334
+ }
335
+ async createWorkflow(workflow) {
336
+ return this.request("POST", "/workflows", workflow);
337
+ }
338
+ async updateWorkflow(id, workflow) {
339
+ return this.request("PUT", `/workflows/${id}`, workflow);
340
+ }
341
+ async getWorkflow(id) {
342
+ return this.request("GET", `/workflows/${id}`);
343
+ }
344
+ async listWorkflows() {
345
+ const all = [];
346
+ let path = `/workflows?limit=${N8N_API_PAGE_SIZE}`;
347
+ for (; ; ) {
348
+ const response = await this.request("GET", path);
349
+ for (const w of response.data) {
350
+ all.push({
351
+ id: w.id,
352
+ name: w.name,
353
+ active: w.active,
354
+ createdAt: w.createdAt,
355
+ updatedAt: w.updatedAt,
356
+ ...w.tags !== void 0 ? { tags: w.tags } : {}
357
+ });
358
+ }
359
+ if (!response.nextCursor) break;
360
+ path = `/workflows?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
361
+ }
362
+ return all;
363
+ }
364
+ async deleteWorkflow(id) {
365
+ await this.request("DELETE", `/workflows/${id}`);
366
+ }
367
+ async activateWorkflow(id) {
368
+ await this.request("POST", `/workflows/${id}/activate`);
369
+ }
370
+ async deactivateWorkflow(id) {
371
+ await this.request("POST", `/workflows/${id}/deactivate`);
372
+ }
373
+ async getExecutions(workflowId, filter) {
374
+ const params = new URLSearchParams();
375
+ if (workflowId) params.set("workflowId", workflowId);
376
+ if (filter?.status) params.set("status", filter.status);
377
+ const limit = Math.min(filter?.limit ?? 20, EXECUTION_LIMIT_CAP);
378
+ params.set("limit", String(limit));
379
+ if (filter?.cursor) params.set("cursor", filter.cursor);
380
+ const qs = params.toString();
381
+ const response = await this.request("GET", `/executions${qs ? `?${qs}` : ""}`);
382
+ return response.data.map(this.mapExecution);
383
+ }
384
+ async getExecution(id) {
385
+ const response = await this.request("GET", `/executions/${id}`);
386
+ return { ...this.mapExecution(response), data: response.data, workflowData: response.workflowData };
387
+ }
388
+ async listTags() {
389
+ const all = [];
390
+ let path = `/tags?limit=${N8N_API_PAGE_SIZE}`;
391
+ for (; ; ) {
392
+ const response = await this.request("GET", path);
393
+ for (const t of response.data) {
394
+ all.push({ id: t.id, name: t.name });
395
+ }
396
+ if (!response.nextCursor) break;
397
+ path = `/tags?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
398
+ }
399
+ return all;
400
+ }
401
+ async createTag(name) {
402
+ const response = await this.request("POST", "/tags", { name });
403
+ return { id: response.id, name: response.name };
404
+ }
405
+ async tagWorkflow(workflowId, tagIds) {
406
+ await this.request("PUT", `/workflows/${workflowId}/tags`, tagIds.map((id) => ({ id })));
407
+ }
408
+ async untagWorkflow(workflowId, tagIds) {
409
+ const current = await this.getWorkflow(workflowId);
410
+ const remaining = (current.tags ?? []).filter((t) => !tagIds.includes(t.id)).map((t) => ({ id: t.id }));
411
+ await this.request("PUT", `/workflows/${workflowId}/tags`, remaining);
412
+ }
413
+ async getNodeTypes() {
414
+ try {
415
+ const response = await this.request("GET", "/node-types");
416
+ return response.data ?? response;
417
+ } catch {
418
+ return [];
419
+ }
420
+ }
421
+ async triggerManual(workflowId) {
422
+ const raw = await this.request("POST", `/workflows/${workflowId}/run`);
423
+ const inner = raw["data"];
424
+ const execId = inner?.["executionId"] ?? raw["executionId"];
425
+ if (execId === void 0 || execId === null) {
426
+ throw new ProviderError(
427
+ `n8n trigger response missing executionId \u2014 got: ${JSON.stringify(raw)}`
428
+ );
429
+ }
430
+ return String(execId);
431
+ }
432
+ async triggerWebhookTest(path) {
433
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
434
+ const url = `${this.baseUrl.replace(/\/$/, "")}/webhook-test${cleanPath}`;
435
+ this.logger.debug(`n8n POST webhook-test ${cleanPath}`);
436
+ try {
437
+ const response = await fetchWithTimeout(
438
+ url,
439
+ { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) },
440
+ REQUEST_TIMEOUT_MS
441
+ );
442
+ return response.status;
443
+ } catch (err) {
444
+ throw new ProviderError(`Webhook test request failed for path "${path}"`, err);
445
+ }
446
+ }
447
+ mapExecution(e) {
448
+ return {
449
+ id: e.id,
450
+ workflowId: e.workflowId,
451
+ status: e.status,
452
+ startedAt: e.startedAt,
453
+ ...e.stoppedAt !== void 0 ? { stoppedAt: e.stoppedAt } : {},
454
+ mode: e.mode
455
+ };
456
+ }
457
+ };
458
+
459
+ // src/providers/n8n/types.ts
460
+ var FORBIDDEN_ON_CREATE = [
461
+ "id",
462
+ "createdAt",
463
+ "updatedAt",
464
+ "versionId",
465
+ "meta",
466
+ "isArchived",
467
+ "activeVersionId",
468
+ "activeVersion",
469
+ "active",
470
+ "pinData",
471
+ "triggerCount",
472
+ "shared",
473
+ "staticData"
474
+ ];
475
+ var FORBIDDEN_ON_UPDATE = FORBIDDEN_ON_CREATE.filter((f) => f !== "id");
476
+
477
+ // src/providers/n8n/stripper.ts
478
+ var N8nFieldStripper = class {
479
+ stripForCreate(workflow) {
480
+ return this.strip(workflow, FORBIDDEN_ON_CREATE);
481
+ }
482
+ stripForUpdate(workflow) {
483
+ return this.strip(workflow, FORBIDDEN_ON_UPDATE);
484
+ }
485
+ strip(workflow, forbidden) {
486
+ const result = { ...workflow };
487
+ for (const field of forbidden) {
488
+ delete result[field];
489
+ }
490
+ return result;
491
+ }
492
+ };
493
+
494
+ // src/utils/uuid.ts
495
+ function generateUUID() {
496
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
497
+ return crypto.randomUUID();
498
+ }
499
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
500
+ const r = Math.random() * 16 | 0;
501
+ return (c === "x" ? r : r & 3 | 8).toString(16);
502
+ });
503
+ }
504
+
505
+ // src/library/null-library.ts
506
+ var NullLibrary = class {
507
+ async initialize() {
508
+ }
509
+ async search(_description, _options) {
510
+ return [];
511
+ }
512
+ async save(_workflow, _metadata) {
513
+ return generateUUID();
514
+ }
515
+ async recordDeployment(_id) {
516
+ }
517
+ async recordOutcome(_id, _outcome) {
518
+ }
519
+ async get(_id) {
520
+ return null;
521
+ }
522
+ async list(_filters) {
523
+ return [];
524
+ }
525
+ };
526
+
527
+ // src/library/file-library.ts
528
+ var import_promises = require("fs/promises");
529
+ var import_node_path = require("path");
530
+ var import_node_os = require("os");
531
+
532
+ // src/utils/thresholds.ts
533
+ var DIRECT_THRESHOLD = 0.92;
534
+ var REFERENCE_THRESHOLD = 0.72;
535
+ function scoreToMode(score) {
536
+ if (score >= DIRECT_THRESHOLD) return "direct";
537
+ if (score >= REFERENCE_THRESHOLD) return "reference";
538
+ return "scratch";
539
+ }
540
+
541
+ // src/library/scorer.ts
542
+ function loadWeights() {
543
+ const raw = {
544
+ tfidf: parseFloat(process.env["KAIROS_WEIGHT_TFIDF"] ?? ""),
545
+ nodeFingerprint: parseFloat(process.env["KAIROS_WEIGHT_JACCARD"] ?? ""),
546
+ outcome: parseFloat(process.env["KAIROS_WEIGHT_OUTCOME"] ?? ""),
547
+ deploy: parseFloat(process.env["KAIROS_WEIGHT_DEPLOY"] ?? "")
548
+ };
549
+ const defaults = { tfidf: 0.35, nodeFingerprint: 0.3, outcome: 0.2, deploy: 0.15 };
550
+ const anySet = Object.values(raw).some((v) => !isNaN(v) && v >= 0);
551
+ if (!anySet) return defaults;
552
+ const w = {
553
+ tfidf: !isNaN(raw.tfidf) && raw.tfidf >= 0 ? raw.tfidf : defaults.tfidf,
554
+ nodeFingerprint: !isNaN(raw.nodeFingerprint) && raw.nodeFingerprint >= 0 ? raw.nodeFingerprint : defaults.nodeFingerprint,
555
+ outcome: !isNaN(raw.outcome) && raw.outcome >= 0 ? raw.outcome : defaults.outcome,
556
+ deploy: !isNaN(raw.deploy) && raw.deploy >= 0 ? raw.deploy : defaults.deploy
557
+ };
558
+ const total = w.tfidf + w.nodeFingerprint + w.outcome + w.deploy;
559
+ if (total <= 0) return defaults;
560
+ return {
561
+ tfidf: w.tfidf / total,
562
+ nodeFingerprint: w.nodeFingerprint / total,
563
+ outcome: w.outcome / total,
564
+ deploy: w.deploy / total
565
+ };
566
+ }
567
+ var WEIGHTS = loadWeights();
568
+ var NODE_KEYWORDS = {
569
+ slack: ["slack", "slackApi"],
570
+ email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
571
+ webhook: ["webhook", "webhookTrigger"],
572
+ schedule: ["scheduleTrigger", "cron"],
573
+ http: ["httpRequest"],
574
+ sheets: ["googleSheets"],
575
+ github: ["github", "githubTrigger"],
576
+ telegram: ["telegram", "telegramTrigger"],
577
+ ai: ["agent", "openAi", "lmChatOpenAi", "lmChatAnthropic", "chainLlm", "chainSummarization"],
578
+ memory: ["memoryBufferWindow", "memoryXata", "memoryPostgres"],
579
+ vector: ["vectorStoreInMemory", "vectorStorePinecone", "vectorStoreQdrant"],
580
+ database: ["postgres", "mySql", "redis", "mongoDb"],
581
+ airtable: ["airtable"],
582
+ notion: ["notion"],
583
+ s3: ["awsS3"],
584
+ code: ["code"],
585
+ merge: ["merge"],
586
+ switch: ["switch"],
587
+ if: ["if"],
588
+ wait: ["wait"],
589
+ rss: ["rssFeedRead", "rssFeedReadTrigger"],
590
+ form: ["formTrigger"],
591
+ set: ["set"],
592
+ split: ["splitInBatches"],
593
+ filter: ["filter"],
594
+ telegram_trigger: ["telegramTrigger"],
595
+ stripe: ["stripe"]
596
+ };
597
+ function extractQueryFingerprint(description) {
598
+ const lower = description.toLowerCase();
599
+ const matches = /* @__PURE__ */ new Set();
600
+ for (const [keyword, nodeTypes] of Object.entries(NODE_KEYWORDS)) {
601
+ if (lower.includes(keyword)) {
602
+ for (const nt of nodeTypes) matches.add(nt);
603
+ }
604
+ }
605
+ if (/\bevery\b|\bdaily\b|\bhourly\b|\bweekly\b|\bmonthly\b|\bcron\b|\bschedule\b|\bat \d/.test(lower)) {
606
+ matches.add("scheduleTrigger");
607
+ }
608
+ if (/\bwebhook\b|\breceive\b.*\bpost\b|\bpost\b.*\brequest\b/.test(lower)) {
609
+ matches.add("webhook");
610
+ }
611
+ if (/\bchat\b|\bchatbot\b|\bconversation\b/.test(lower)) {
612
+ matches.add("chatTrigger");
613
+ }
614
+ if (/\bai\b|\bllm\b|\bgpt\b|\bclaude\b|\bagent\b|\bsummariz/.test(lower)) {
615
+ matches.add("agent");
616
+ }
617
+ return matches;
618
+ }
619
+ function extractWorkflowFingerprint(w) {
620
+ const fp = /* @__PURE__ */ new Set();
621
+ for (const node of w.workflow.nodes) {
622
+ const bare = node.type.split(".").pop() ?? "";
623
+ fp.add(bare);
624
+ }
625
+ return fp;
626
+ }
627
+ function jaccardSimilarity(a, b) {
628
+ if (a.size === 0 && b.size === 0) return 0;
629
+ let intersection = 0;
630
+ for (const item of a) {
631
+ if (b.has(item)) intersection++;
632
+ }
633
+ const union = a.size + b.size - intersection;
634
+ return union > 0 ? intersection / union : 0;
635
+ }
636
+ function outcomeScore(w) {
637
+ const stats = w.outcomeStats;
638
+ if (!stats || stats.totalUses === 0) return 0.5;
639
+ const passRate = stats.firstTryPasses / stats.totalUses;
640
+ const avgAttempts = stats.totalAttempts / stats.totalUses;
641
+ const attemptPenalty = Math.max(0, 1 - (avgAttempts - 1) * 0.3);
642
+ return passRate * 0.6 + attemptPenalty * 0.4;
643
+ }
644
+ function deployScore(w) {
645
+ return 1 + Math.log(w.deployCount + 1) * 0.1;
646
+ }
647
+ function hybridScore(queryTokens, queryDescription, workflows, docTokenArrays, idf) {
648
+ const queryFp = extractQueryFingerprint(queryDescription);
649
+ const ceiling = queryTokens.reduce((sum, qt) => sum + (idf.get(qt) ?? 0), 0) || 1;
650
+ return workflows.map((w, i) => {
651
+ const docTokens = docTokenArrays[i];
652
+ let tfidfRaw = 0;
653
+ const docFreq = /* @__PURE__ */ new Map();
654
+ for (const t of docTokens) {
655
+ docFreq.set(t, (docFreq.get(t) ?? 0) + 1);
656
+ }
657
+ for (const qt of queryTokens) {
658
+ const tf = docTokens.length > 0 ? (docFreq.get(qt) ?? 0) / docTokens.length : 0;
659
+ const idfVal = idf.get(qt) ?? 0;
660
+ tfidfRaw += tf * idfVal;
661
+ }
662
+ const tfidf = Math.min(tfidfRaw / ceiling, 1);
663
+ const workflowFp = extractWorkflowFingerprint(w);
664
+ const nodeFingerprint = queryFp.size > 0 ? jaccardSimilarity(queryFp, workflowFp) : 0;
665
+ const outcome = outcomeScore(w);
666
+ const deploy = Math.min(deployScore(w), 1.5) / 1.5;
667
+ const score = Math.min(
668
+ WEIGHTS.tfidf * tfidf + WEIGHTS.nodeFingerprint * nodeFingerprint + WEIGHTS.outcome * outcome + WEIGHTS.deploy * deploy,
669
+ 1
670
+ );
671
+ return {
672
+ workflow: w,
673
+ score,
674
+ signals: { tfidf, nodeFingerprint, outcome, deploy }
675
+ };
676
+ });
677
+ }
678
+
679
+ // src/library/cluster.ts
680
+ function getFingerprint(w) {
681
+ return w.workflow.nodes.map((n) => n.type.split(".").pop() ?? "").sort();
682
+ }
683
+ function fingerprintKey(fp) {
684
+ return fp.join("|");
685
+ }
686
+ function describePattern(fp) {
687
+ const triggers = fp.filter((n) => /trigger/i.test(n));
688
+ const outputs = fp.filter((n) => /slack|gmail|email|telegram|sheets|airtable|notion/i.test(n));
689
+ const ai = fp.filter((n) => /agent|openai|anthropic|chain|memory/i.test(n));
690
+ const core = fp.filter((n) => /httpRequest|code|merge|switch|if|set|filter/i.test(n));
691
+ const parts = [];
692
+ if (triggers.length > 0) parts.push(triggers[0]);
693
+ if (ai.length > 0) parts.push("AI");
694
+ if (core.length > 0) parts.push(core.slice(0, 2).join("+"));
695
+ if (outputs.length > 0) parts.push(outputs[0]);
696
+ return parts.length > 0 ? parts.join(" \u2192 ") : fp.slice(0, 3).join(" \u2192 ");
697
+ }
698
+ function clusterWorkflows(workflows) {
699
+ const groups = /* @__PURE__ */ new Map();
700
+ for (const w of workflows) {
701
+ const fp = getFingerprint(w);
702
+ const key = fingerprintKey(fp);
703
+ const existing = groups.get(key);
704
+ if (existing) {
705
+ existing.push(w);
706
+ } else {
707
+ groups.set(key, [w]);
708
+ }
709
+ }
710
+ const clusters = [];
711
+ for (const [, members] of groups) {
712
+ if (members.length === 0) continue;
713
+ const fp = getFingerprint(members[0]);
714
+ const withStats = members.filter((m) => m.outcomeStats && m.outcomeStats.totalUses > 0);
715
+ let avgFirstTryPassRate = 0;
716
+ let avgAttempts = 0;
717
+ if (withStats.length > 0) {
718
+ avgFirstTryPassRate = withStats.reduce((sum, m) => {
719
+ const s = m.outcomeStats;
720
+ return sum + s.firstTryPasses / s.totalUses;
721
+ }, 0) / withStats.length;
722
+ avgAttempts = withStats.reduce((sum, m) => {
723
+ const s = m.outcomeStats;
724
+ return sum + s.totalAttempts / s.totalUses;
725
+ }, 0) / withStats.length;
726
+ }
727
+ const ruleCounts = /* @__PURE__ */ new Map();
728
+ let totalFailureInstances = 0;
729
+ for (const m of withStats) {
730
+ const rules = m.outcomeStats.failedRules;
731
+ for (const [rule, count] of Object.entries(rules)) {
732
+ const r = parseInt(rule, 10);
733
+ ruleCounts.set(r, (ruleCounts.get(r) ?? 0) + count);
734
+ totalFailureInstances += count;
735
+ }
736
+ }
737
+ const commonFailedRules = [...ruleCounts.entries()].map(([rule, count]) => ({
738
+ rule,
739
+ frequency: totalFailureInstances > 0 ? count / totalFailureInstances : 0
740
+ })).filter((r) => r.frequency >= 0.1).sort((a, b) => b.frequency - a.frequency);
741
+ clusters.push({
742
+ pattern: describePattern(fp),
743
+ fingerprint: fp,
744
+ members,
745
+ avgFirstTryPassRate,
746
+ avgAttempts,
747
+ commonFailedRules
748
+ });
749
+ }
750
+ return clusters.sort((a, b) => b.members.length - a.members.length);
751
+ }
752
+ var NOVELTY_BOOST = 0.05;
753
+ var NOVELTY_PENALTY = 0.03;
754
+ function rerank(candidates, clusters) {
755
+ const clusterMap = /* @__PURE__ */ new Map();
756
+ for (const cluster of clusters) {
757
+ for (const member of cluster.members) {
758
+ clusterMap.set(member.id, cluster);
759
+ }
760
+ }
761
+ const pass1 = candidates.map((c) => {
762
+ const cluster = clusterMap.get(c.workflow.id);
763
+ let boost = 0;
764
+ if (cluster && cluster.avgFirstTryPassRate > 0) {
765
+ boost = (cluster.avgFirstTryPassRate - 0.5) * 0.1;
766
+ }
767
+ if (cluster && cluster.commonFailedRules.length > 0) {
768
+ boost -= cluster.commonFailedRules.length * 0.02;
769
+ }
770
+ return {
771
+ workflow: c.workflow,
772
+ score: Math.max(0, Math.min(1, c.score + boost)),
773
+ cluster
774
+ };
775
+ }).sort((a, b) => b.score - a.score);
776
+ const seenFingerprints = /* @__PURE__ */ new Set();
777
+ return pass1.map((c) => {
778
+ const fpKey = c.cluster ? fingerprintKey(c.cluster.fingerprint) : null;
779
+ let noveltyAdjust = 0;
780
+ if (fpKey !== null) {
781
+ if (!seenFingerprints.has(fpKey)) {
782
+ seenFingerprints.add(fpKey);
783
+ noveltyAdjust = NOVELTY_BOOST;
784
+ } else {
785
+ noveltyAdjust = -NOVELTY_PENALTY;
786
+ }
787
+ }
788
+ return {
789
+ workflow: c.workflow,
790
+ score: Math.max(0, Math.min(1, c.score + noveltyAdjust)),
791
+ ...c.cluster ? { clusterPattern: c.cluster.pattern } : {}
792
+ };
793
+ }).sort((a, b) => b.score - a.score);
794
+ }
795
+
796
+ // src/library/file-library.ts
797
+ function tokenize(text) {
798
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2);
799
+ }
800
+ function buildSearchCorpus(w) {
801
+ const nodeTokens = w.workflow.nodes.map((n) => {
802
+ const bare = n.type.split(".").pop() ?? "";
803
+ const spaced = bare.replace(/([A-Z])/g, " $1").trim().toLowerCase();
804
+ return `${bare} ${spaced}`;
805
+ });
806
+ return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
807
+ }
808
+ var _rawSize = parseInt(process.env["KAIROS_LIBRARY_SIZE"] ?? "500", 10);
809
+ var MAX_LIBRARY_SIZE = Number.isFinite(_rawSize) && _rawSize >= 10 ? _rawSize : 500;
810
+ function evictionScore(m) {
811
+ return (m.deployCount ?? 0) * 3 + (m.timesRetrieved ?? 0) + (m.outcomeStats?.totalUses ?? 0);
812
+ }
813
+ function isValidMeta(item) {
814
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
815
+ }
816
+ function isValidOldEntry(item) {
817
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
818
+ item.workflow.nodes
819
+ );
820
+ }
821
+ var FileLibrary = class {
822
+ dir;
823
+ meta = [];
824
+ initPromise = null;
825
+ writeQueue = Promise.resolve();
826
+ constructor(dir) {
827
+ this.dir = dir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "library");
828
+ }
829
+ get workflowsDir() {
830
+ return (0, import_node_path.join)(this.dir, "workflows");
831
+ }
832
+ workflowFilePath(id) {
833
+ return (0, import_node_path.join)(this.workflowsDir, `${id}.json`);
834
+ }
835
+ async initialize() {
836
+ if (!this.initPromise) {
837
+ this.initPromise = this.doInitialize();
838
+ }
839
+ return this.initPromise;
840
+ }
841
+ async doInitialize() {
842
+ await (0, import_promises.mkdir)(this.dir, { recursive: true });
843
+ const indexPath = (0, import_node_path.join)(this.dir, "index.json");
844
+ let workflowsDirExists = false;
845
+ try {
846
+ await (0, import_promises.stat)(this.workflowsDir);
847
+ workflowsDirExists = true;
848
+ } catch {
849
+ }
850
+ if (workflowsDirExists) {
851
+ try {
852
+ const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
853
+ const parsed = JSON.parse(raw);
854
+ if (Array.isArray(parsed)) {
855
+ this.meta = parsed.filter(isValidMeta);
856
+ }
857
+ } catch {
858
+ this.meta = [];
859
+ }
860
+ await this.scanForOrphansAndCleanup();
861
+ } else {
862
+ try {
863
+ const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
864
+ const parsed = JSON.parse(raw);
865
+ if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
866
+ await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
867
+ return;
868
+ }
869
+ } catch {
870
+ }
871
+ this.meta = [];
872
+ await (0, import_promises.mkdir)(this.workflowsDir, { recursive: true });
873
+ }
874
+ }
875
+ async scanForOrphansAndCleanup() {
876
+ let entries;
877
+ try {
878
+ entries = await (0, import_promises.readdir)(this.workflowsDir);
879
+ } catch {
880
+ return;
881
+ }
882
+ const indexedIds = new Set(this.meta.map((m) => m.id));
883
+ const orphanIds = [];
884
+ for (const filename of entries) {
885
+ if (filename.endsWith(".tmp")) {
886
+ await (0, import_promises.unlink)((0, import_node_path.join)(this.workflowsDir, filename)).catch(() => {
887
+ });
888
+ continue;
889
+ }
890
+ if (!filename.endsWith(".json")) continue;
891
+ const id = filename.slice(0, -5);
892
+ if (!indexedIds.has(id)) {
893
+ orphanIds.push(id);
894
+ }
895
+ }
896
+ if (orphanIds.length > 0) {
897
+ console.warn(`[FileLibrary] Found ${orphanIds.length} orphaned workflow file(s) not in index: ${orphanIds.join(", ")}`);
898
+ }
899
+ }
900
+ /**
901
+ * One-time transparent migration from v0.4.x monolithic index.json.
902
+ * Splits each stored workflow into a per-file workflow JSON and a lightweight
903
+ * meta entry. Rewrites index.json in the new format.
904
+ */
905
+ async migrateFromMonolithic(oldEntries) {
906
+ await (0, import_promises.mkdir)(this.workflowsDir, { recursive: true });
907
+ const newMeta = [];
908
+ for (const entry of oldEntries) {
909
+ const wfPath = this.workflowFilePath(entry.id);
910
+ const tmpPath = `${wfPath}.tmp`;
911
+ await (0, import_promises.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
912
+ await (0, import_promises.rename)(tmpPath, wfPath);
913
+ const { workflow, ...metaFields } = entry;
914
+ newMeta.push({
915
+ ...metaFields,
916
+ workflowName: workflow.name,
917
+ cachedNodeTypes: workflow.nodes.map((n) => n.type)
918
+ });
919
+ }
920
+ this.meta = newMeta;
921
+ await this.persistNow();
922
+ }
923
+ async loadWorkflowFile(id) {
924
+ try {
925
+ const raw = await (0, import_promises.readFile)(this.workflowFilePath(id), "utf-8");
926
+ return JSON.parse(raw);
927
+ } catch {
928
+ return null;
929
+ }
930
+ }
931
+ async writeWorkflowFile(id, workflow) {
932
+ const wfPath = this.workflowFilePath(id);
933
+ const tmpPath = `${wfPath}.tmp`;
934
+ await (0, import_promises.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
935
+ await (0, import_promises.rename)(tmpPath, wfPath);
936
+ }
937
+ /**
938
+ * Build a lightweight StoredWorkflow shell from a meta entry for use in
939
+ * scoring / clustering. Only node.type is populated in each node — no other
940
+ * node fields are used by hybridScore or clusterWorkflows.
941
+ */
942
+ makeSearchShell(m) {
943
+ return {
944
+ ...m,
945
+ workflow: {
946
+ name: m.workflowName,
947
+ nodes: m.cachedNodeTypes.map((type) => ({
948
+ id: "",
949
+ name: "",
950
+ type,
951
+ typeVersion: 1,
952
+ position: [0, 0],
953
+ parameters: {}
954
+ })),
955
+ connections: {}
956
+ }
957
+ };
958
+ }
959
+ async search(description, options) {
960
+ const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
961
+ if (filteredMeta.length === 0) return [];
962
+ const limit = options?.limit ?? 3;
963
+ const queryTokens = tokenize(description);
964
+ if (queryTokens.length === 0) return [];
965
+ const shells = filteredMeta.map((m) => this.makeSearchShell(m));
966
+ const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
967
+ const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
968
+ const docCount = shells.length;
969
+ const idf = /* @__PURE__ */ new Map();
970
+ const idfCeiling = Math.log(docCount + 1) + 1;
971
+ const allTokens = new Set(queryTokens);
972
+ for (const token of allTokens) {
973
+ const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
974
+ const rawIdf = Math.log((docCount + 1) / (docsWithToken + 1)) + 1;
975
+ idf.set(token, rawIdf / idfCeiling);
976
+ }
977
+ const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
978
+ const clusters = clusterWorkflows(shells);
979
+ const reranked = rerank(scored, clusters).slice(0, limit);
980
+ if (reranked.length === 0) return [];
981
+ for (const r of reranked) {
982
+ const m = this.meta.find((m2) => m2.id === r.workflow.id);
983
+ if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
984
+ }
985
+ this.persist();
986
+ const results = await Promise.all(
987
+ reranked.map(async (r) => {
988
+ const m = this.meta.find((meta) => meta.id === r.workflow.id);
989
+ const workflow = await this.loadWorkflowFile(r.workflow.id);
990
+ if (!workflow) return null;
991
+ return {
992
+ workflow: { ...m, workflow },
993
+ score: r.score,
994
+ mode: scoreToMode(r.score)
995
+ };
996
+ })
997
+ );
998
+ return results.filter((r) => r !== null);
999
+ }
1000
+ async save(workflow, metadata) {
1001
+ const existingByN8nId = metadata.n8nWorkflowId ? this.meta.find((m) => m.n8nWorkflowId === metadata.n8nWorkflowId) : void 0;
1002
+ const normalizedDesc = metadata.description.trim().toLowerCase();
1003
+ const existing = existingByN8nId ?? this.meta.find((m) => m.description.trim().toLowerCase() === normalizedDesc);
1004
+ if (existing) {
1005
+ existing.description = metadata.description;
1006
+ existing.workflowName = workflow.name;
1007
+ existing.cachedNodeTypes = workflow.nodes.map((n) => n.type);
1008
+ if (metadata.n8nWorkflowId) existing.n8nWorkflowId = metadata.n8nWorkflowId;
1009
+ if (metadata.generationAttempts != null) {
1010
+ existing.generationAttempts = metadata.generationAttempts;
1011
+ }
1012
+ if (metadata.failurePatterns?.length) {
1013
+ existing.failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
1014
+ }
1015
+ if (metadata.tags?.length) {
1016
+ existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...metadata.tags])];
1017
+ }
1018
+ await this.writeWorkflowFile(existing.id, workflow);
1019
+ await this.persist();
1020
+ return existing.id;
1021
+ }
1022
+ const id = generateUUID();
1023
+ await this.writeWorkflowFile(id, workflow);
1024
+ const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
1025
+ const meta = {
1026
+ id,
1027
+ description: metadata.description,
1028
+ tags: metadata.tags ?? [],
1029
+ platform: metadata.platform ?? "n8n",
1030
+ deployCount: 0,
1031
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1032
+ workflowName: workflow.name,
1033
+ cachedNodeTypes: workflow.nodes.map((n) => n.type),
1034
+ ...failurePatterns?.length ? { failurePatterns } : {},
1035
+ ...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
1036
+ ...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
1037
+ ...metadata.topMatchScore != null ? { topMatchScore: metadata.topMatchScore } : {},
1038
+ ...metadata.generationAttempts != null ? { generationAttempts: metadata.generationAttempts } : {},
1039
+ ...metadata.credentialsNeeded?.length ? { credentialsNeeded: metadata.credentialsNeeded } : {},
1040
+ ...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
1041
+ ...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
1042
+ ...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
1043
+ ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {},
1044
+ ...metadata.n8nWorkflowId ? { n8nWorkflowId: metadata.n8nWorkflowId } : {}
1045
+ };
1046
+ this.meta.push(meta);
1047
+ if (this.meta.length > MAX_LIBRARY_SIZE) {
1048
+ this.meta.sort((a, b) => {
1049
+ if (a.id === id) return -1;
1050
+ if (b.id === id) return 1;
1051
+ return evictionScore(b) - evictionScore(a);
1052
+ });
1053
+ this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
1054
+ }
1055
+ await this.persist();
1056
+ return id;
1057
+ }
1058
+ async recordDeployment(id, n8nWorkflowId) {
1059
+ const m = this.meta.find((m2) => m2.id === id);
1060
+ if (m) {
1061
+ m.deployCount++;
1062
+ m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
1063
+ if (n8nWorkflowId) m.n8nWorkflowId = n8nWorkflowId;
1064
+ await this.persist();
1065
+ }
1066
+ }
1067
+ async recordOutcome(id, outcome) {
1068
+ const m = this.meta.find((m2) => m2.id === id);
1069
+ if (!m) return;
1070
+ if (outcome.mode === "direct") {
1071
+ m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
1072
+ } else {
1073
+ m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
1074
+ }
1075
+ const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
1076
+ stats.totalUses++;
1077
+ stats.totalAttempts += outcome.attempts;
1078
+ if (outcome.firstTryPass) stats.firstTryPasses++;
1079
+ for (const rule of outcome.failedRules) {
1080
+ const key = String(rule);
1081
+ stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
1082
+ }
1083
+ m.outcomeStats = stats;
1084
+ await this.persist();
1085
+ }
1086
+ async drain() {
1087
+ await this.writeQueue;
1088
+ }
1089
+ async get(id) {
1090
+ const m = this.meta.find((m2) => m2.id === id);
1091
+ if (!m) return null;
1092
+ const workflow = await this.loadWorkflowFile(id);
1093
+ if (!workflow) return null;
1094
+ return { ...m, workflow };
1095
+ }
1096
+ async list(filters) {
1097
+ let filtered = this.meta;
1098
+ if (filters?.platform) {
1099
+ filtered = filtered.filter((m) => m.platform === filters.platform);
1100
+ }
1101
+ if (filters?.tags && filters.tags.length > 0) {
1102
+ filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
1103
+ }
1104
+ const results = await Promise.all(
1105
+ filtered.map(async (m) => {
1106
+ const workflow = await this.loadWorkflowFile(m.id);
1107
+ if (!workflow) return null;
1108
+ return { ...m, workflow };
1109
+ })
1110
+ );
1111
+ return results.filter((r) => r !== null);
1112
+ }
1113
+ deduplicateFailurePatterns(patterns) {
1114
+ if (!patterns?.length) return void 0;
1115
+ const map = /* @__PURE__ */ new Map();
1116
+ for (const fp of patterns) {
1117
+ const existing = map.get(fp.rule);
1118
+ if (existing) {
1119
+ existing.occurrences++;
1120
+ } else {
1121
+ map.set(fp.rule, { rule: fp.rule, message: fp.message, occurrences: 1 });
1122
+ }
1123
+ }
1124
+ return [...map.values()];
1125
+ }
1126
+ // ── Cross-process file locking ────────────────────────────────────────────
1127
+ // Uses O_EXCL (exclusive create) which is atomic on POSIX and Windows NTFS.
1128
+ // Protects the read-modify-write cycle in persist() from concurrent writers
1129
+ // in separate OS processes (e.g. MCP server + CLI running simultaneously).
1130
+ get lockPath() {
1131
+ return (0, import_node_path.join)(this.dir, ".index.lock");
1132
+ }
1133
+ async acquireLock(timeoutMs = 3e3) {
1134
+ const deadline = Date.now() + timeoutMs;
1135
+ let delayMs = 10;
1136
+ while (true) {
1137
+ try {
1138
+ const fh = await (0, import_promises.open)(this.lockPath, "wx");
1139
+ await fh.writeFile(String(process.pid));
1140
+ await fh.close();
1141
+ return async () => {
1142
+ await (0, import_promises.unlink)(this.lockPath).catch(() => {
1143
+ });
1144
+ };
1145
+ } catch {
1146
+ try {
1147
+ const content = await (0, import_promises.readFile)(this.lockPath, "utf-8");
1148
+ const lockPid = parseInt(content.trim(), 10);
1149
+ const fileStat = await (0, import_promises.stat)(this.lockPath);
1150
+ const ageMs = Date.now() - fileStat.mtimeMs;
1151
+ if (ageMs > 1e4) {
1152
+ await (0, import_promises.unlink)(this.lockPath).catch(() => {
1153
+ });
1154
+ continue;
1155
+ }
1156
+ if (!isNaN(lockPid)) {
1157
+ try {
1158
+ process.kill(lockPid, 0);
1159
+ } catch {
1160
+ await (0, import_promises.unlink)(this.lockPath).catch(() => {
1161
+ });
1162
+ continue;
1163
+ }
1164
+ }
1165
+ } catch {
1166
+ continue;
1167
+ }
1168
+ if (Date.now() > deadline) {
1169
+ return async () => {
1170
+ };
1171
+ }
1172
+ await new Promise((r) => setTimeout(r, delayMs));
1173
+ delayMs = Math.min(delayMs * 1.5, 200);
1174
+ }
1175
+ }
1176
+ }
1177
+ /**
1178
+ * Direct write used only during migration (before writeQueue is needed).
1179
+ */
1180
+ async persistNow() {
1181
+ const releaseLock = await this.acquireLock();
1182
+ try {
1183
+ const indexPath = (0, import_node_path.join)(this.dir, "index.json");
1184
+ const tmpPath = `${indexPath}.tmp`;
1185
+ await (0, import_promises.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
1186
+ await (0, import_promises.rename)(tmpPath, indexPath);
1187
+ } finally {
1188
+ await releaseLock();
1189
+ }
1190
+ }
1191
+ persist() {
1192
+ this.writeQueue = this.writeQueue.then(async () => {
1193
+ const releaseLock = await this.acquireLock();
1194
+ try {
1195
+ const indexPath = (0, import_node_path.join)(this.dir, "index.json");
1196
+ let onDisk = [];
1197
+ try {
1198
+ const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
1199
+ const parsed = JSON.parse(raw);
1200
+ if (Array.isArray(parsed)) {
1201
+ onDisk = parsed.filter(isValidMeta);
1202
+ }
1203
+ } catch {
1204
+ }
1205
+ const ourIds = new Set(this.meta.map((m) => m.id));
1206
+ const external = onDisk.filter((m) => !ourIds.has(m.id));
1207
+ let merged = [...this.meta, ...external];
1208
+ if (merged.length > MAX_LIBRARY_SIZE) {
1209
+ merged.sort((a, b) => evictionScore(b) - evictionScore(a));
1210
+ merged = merged.slice(0, MAX_LIBRARY_SIZE);
1211
+ }
1212
+ const tmpPath = `${indexPath}.tmp`;
1213
+ await (0, import_promises.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
1214
+ await (0, import_promises.rename)(tmpPath, indexPath);
1215
+ } finally {
1216
+ await releaseLock();
1217
+ }
1218
+ });
1219
+ return this.writeQueue;
1220
+ }
1221
+ };
1222
+
1223
+ // src/validation/registry.ts
1224
+ var DEFAULT_REGISTRY = [
1225
+ // Trigger nodes
1226
+ { type: "n8n-nodes-base.manualTrigger", safeTypeVersions: [1], requiredParams: [], isTrigger: true },
1227
+ { type: "n8n-nodes-base.scheduleTrigger", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], isTrigger: true },
1228
+ { type: "n8n-nodes-base.webhook", safeTypeVersions: [1, 1.1, 2], requiredParams: ["httpMethod", "path"], isTrigger: true },
1229
+ { type: "n8n-nodes-base.formTrigger", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [], isTrigger: true },
1230
+ { type: "n8n-nodes-base.emailReadImap", safeTypeVersions: [2], requiredParams: [], credentialType: "imap", isTrigger: true },
1231
+ { type: "n8n-nodes-base.errorTrigger", safeTypeVersions: [1], requiredParams: [], isTrigger: true },
1232
+ { type: "n8n-nodes-base.executeWorkflowTrigger", safeTypeVersions: [1, 1.1], requiredParams: [], isTrigger: true },
1233
+ { type: "n8n-nodes-base.gmailTrigger", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], credentialType: "gmailOAuth2", isTrigger: true },
1234
+ { type: "n8n-nodes-base.googleDriveTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "googleDriveOAuth2Api", isTrigger: true },
1235
+ { type: "n8n-nodes-base.googleSheetsTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "googleSheetsTriggerOAuth2Api", isTrigger: true },
1236
+ { type: "n8n-nodes-base.slackTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "slackApi", isTrigger: true },
1237
+ { type: "n8n-nodes-base.telegramTrigger", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], credentialType: "telegramApi", isTrigger: true },
1238
+ { type: "n8n-nodes-base.githubTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "githubApi", isTrigger: true },
1239
+ { type: "n8n-nodes-base.stripeTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "stripeApi", isTrigger: true },
1240
+ { type: "n8n-nodes-base.airtableTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "airtableTokenApi", isTrigger: true },
1241
+ { type: "n8n-nodes-base.notionTrigger", safeTypeVersions: [1], requiredParams: [], credentialType: "notionApi", isTrigger: true },
1242
+ { type: "@n8n/n8n-nodes-langchain.chatTrigger", safeTypeVersions: [1, 1.1], requiredParams: [], isTrigger: true },
1243
+ // Core logic nodes
1244
+ { type: "n8n-nodes-base.code", safeTypeVersions: [1, 2], requiredParams: [] },
1245
+ { type: "n8n-nodes-base.httpRequest", safeTypeVersions: [1, 2, 3, 4, 4.1, 4.2], requiredParams: ["url"] },
1246
+ { type: "n8n-nodes-base.set", safeTypeVersions: [1, 2, 3, 3.1, 3.2, 3.3, 3.4], requiredParams: [] },
1247
+ { type: "n8n-nodes-base.if", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [] },
1248
+ { type: "n8n-nodes-base.switch", safeTypeVersions: [1, 2, 3, 3.1, 3.2], requiredParams: [] },
1249
+ { type: "n8n-nodes-base.filter", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [] },
1250
+ { type: "n8n-nodes-base.merge", safeTypeVersions: [1, 2, 2.1, 3], requiredParams: [] },
1251
+ { type: "n8n-nodes-base.splitInBatches", safeTypeVersions: [1, 2, 3], requiredParams: [] },
1252
+ { type: "n8n-nodes-base.wait", safeTypeVersions: [1, 1.1], requiredParams: [] },
1253
+ { type: "n8n-nodes-base.executeWorkflow", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [] },
1254
+ { type: "n8n-nodes-base.respondToWebhook", safeTypeVersions: [1, 1.1], requiredParams: [] },
1255
+ { type: "n8n-nodes-base.noOp", safeTypeVersions: [1], requiredParams: [] },
1256
+ { type: "n8n-nodes-base.stopAndError", safeTypeVersions: [1], requiredParams: [] },
1257
+ { type: "n8n-nodes-base.splitOut", safeTypeVersions: [1], requiredParams: [] },
1258
+ { type: "n8n-nodes-base.aggregate", safeTypeVersions: [1], requiredParams: [] },
1259
+ { type: "n8n-nodes-base.stickyNote", safeTypeVersions: [1], requiredParams: [] },
1260
+ // Email / messaging
1261
+ { type: "n8n-nodes-base.emailSend", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "smtp" },
1262
+ { type: "n8n-nodes-base.slack", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [], credentialType: "slackOAuth2Api" },
1263
+ { type: "n8n-nodes-base.telegram", safeTypeVersions: [1, 1.1, 1.2], requiredParams: [], credentialType: "telegramApi" },
1264
+ { type: "n8n-nodes-base.discord", safeTypeVersions: [1, 2], requiredParams: [], credentialType: "discordWebhookApi" },
1265
+ // Google
1266
+ { type: "n8n-nodes-base.gmail", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "gmailOAuth2" },
1267
+ { type: "n8n-nodes-base.googleSheets", safeTypeVersions: [1, 2, 3, 4, 4.1, 4.2, 4.3, 4.4, 4.5], requiredParams: [], credentialType: "googleSheetsOAuth2Api" },
1268
+ { type: "n8n-nodes-base.googleDrive", safeTypeVersions: [1, 2, 3], requiredParams: [], credentialType: "googleDriveOAuth2Api" },
1269
+ { type: "n8n-nodes-base.googleCalendar", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [], credentialType: "googleCalendarOAuth2Api" },
1270
+ // Project management / CRM
1271
+ { type: "n8n-nodes-base.notion", safeTypeVersions: [1, 2, 2.1, 2.2], requiredParams: [], credentialType: "notionApi" },
1272
+ { type: "n8n-nodes-base.airtable", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "airtableTokenApi" },
1273
+ { type: "n8n-nodes-base.github", safeTypeVersions: [1, 1.1], requiredParams: [], credentialType: "githubApi" },
1274
+ { type: "n8n-nodes-base.jira", safeTypeVersions: [1], requiredParams: [], credentialType: "jiraSoftwareCloudApi" },
1275
+ { type: "n8n-nodes-base.hubspot", safeTypeVersions: [1, 2, 2.1], requiredParams: [], credentialType: "hubspotOAuth2Api" },
1276
+ // Databases
1277
+ { type: "n8n-nodes-base.postgres", safeTypeVersions: [1, 2, 2.1, 2.2, 2.3, 2.4, 2.5], requiredParams: [], credentialType: "postgres" },
1278
+ { type: "n8n-nodes-base.mySql", safeTypeVersions: [1, 2, 2.1, 2.2, 2.3, 2.4], requiredParams: [], credentialType: "mySql" },
1279
+ { type: "n8n-nodes-base.redis", safeTypeVersions: [1], requiredParams: [], credentialType: "redis" },
1280
+ { type: "n8n-nodes-base.supabase", safeTypeVersions: [1], requiredParams: [], credentialType: "supabaseApi" },
1281
+ // Cloud
1282
+ { type: "n8n-nodes-base.awsS3", safeTypeVersions: [1, 2], requiredParams: [], credentialType: "aws" },
1283
+ // Payment / commerce
1284
+ { type: "n8n-nodes-base.stripe", safeTypeVersions: [1], requiredParams: [], credentialType: "stripeApi" },
1285
+ // AI / LangChain root nodes
1286
+ { type: "@n8n/n8n-nodes-langchain.agent", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9], requiredParams: [] },
1287
+ { type: "@n8n/n8n-nodes-langchain.chainLlm", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5], requiredParams: [] },
1288
+ { type: "@n8n/n8n-nodes-langchain.chainRetrievalQa", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4], requiredParams: [] },
1289
+ { type: "@n8n/n8n-nodes-langchain.openAi", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8], requiredParams: [], credentialType: "openAiApi" },
1290
+ { type: "@n8n/n8n-nodes-langchain.anthropic", safeTypeVersions: [1], requiredParams: [], credentialType: "anthropicApi" },
1291
+ { type: "@n8n/n8n-nodes-langchain.informationExtractor", safeTypeVersions: [1], requiredParams: [] },
1292
+ { type: "@n8n/n8n-nodes-langchain.textClassifier", safeTypeVersions: [1], requiredParams: [] },
1293
+ // AI / LangChain sub-nodes (models)
1294
+ { type: "@n8n/n8n-nodes-langchain.lmChatOpenAi", safeTypeVersions: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7], requiredParams: [], credentialType: "openAiApi" },
1295
+ { type: "@n8n/n8n-nodes-langchain.lmChatAnthropic", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [], credentialType: "anthropicApi" },
1296
+ { type: "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", safeTypeVersions: [1], requiredParams: [], credentialType: "googlePalmApi" },
1297
+ // AI / LangChain sub-nodes (memory, tools, etc.)
1298
+ { type: "@n8n/n8n-nodes-langchain.memoryBufferWindow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
1299
+ { type: "@n8n/n8n-nodes-langchain.toolWorkflow", safeTypeVersions: [1, 1.1, 1.2, 1.3], requiredParams: [] },
1300
+ { type: "@n8n/n8n-nodes-langchain.toolCode", safeTypeVersions: [1, 1.1], requiredParams: [] },
1301
+ { type: "@n8n/n8n-nodes-langchain.toolHttpRequest", safeTypeVersions: [1, 1.1], requiredParams: [] },
1302
+ { type: "@n8n/n8n-nodes-langchain.toolCalculator", safeTypeVersions: [1], requiredParams: [] }
1303
+ ];
1304
+ var NodeRegistry = class {
1305
+ byType;
1306
+ constructor(definitions = DEFAULT_REGISTRY) {
1307
+ this.byType = new Map(definitions.map((d) => [d.type, d]));
1308
+ }
1309
+ get(type) {
1310
+ return this.byType.get(type);
1311
+ }
1312
+ isTrigger(type) {
1313
+ return this.byType.get(type)?.isTrigger === true;
1314
+ }
1315
+ isKnown(type) {
1316
+ return this.byType.has(type);
1317
+ }
1318
+ isVersionSafe(type, version) {
1319
+ const def = this.byType.get(type);
1320
+ if (!def) return true;
1321
+ return def.safeTypeVersions.includes(version);
1322
+ }
1323
+ // Returns true when the version is a positive integer greater than the highest
1324
+ // known safe version — indicates a newer release rather than a bad value.
1325
+ isVersionNewer(type, version) {
1326
+ const def = this.byType.get(type);
1327
+ if (!def || def.safeTypeVersions.length === 0) return false;
1328
+ const max = Math.max(...def.safeTypeVersions);
1329
+ return Number.isInteger(version) && version > max;
1330
+ }
1331
+ getRequiredParams(type) {
1332
+ return this.byType.get(type)?.requiredParams ?? [];
1333
+ }
1334
+ };
1335
+
1336
+ // src/validation/validator.ts
1337
+ var AI_CONNECTION_TYPES = [
1338
+ "ai_languageModel",
1339
+ "ai_memory",
1340
+ "ai_tool",
1341
+ "ai_outputParser",
1342
+ "ai_embedding",
1343
+ "ai_document",
1344
+ "ai_textSplitter",
1345
+ "ai_retriever",
1346
+ "ai_vectorStore"
1347
+ ];
1348
+ var TRIGGER_TYPE_PATTERNS = [/trigger/i, /Trigger$/];
1349
+ var NODE_TYPE_PATTERN = /^(@[a-z0-9-]+\/[a-z0-9-]+\.|n8n-nodes-[a-z0-9-]+\.)[a-zA-Z][a-zA-Z0-9-]+$/;
1350
+ var N8nValidator = class {
1351
+ registry;
1352
+ constructor(registry = new NodeRegistry(DEFAULT_REGISTRY)) {
1353
+ this.registry = registry;
1354
+ }
1355
+ validate(workflow) {
1356
+ const issues = [];
1357
+ this.checkRule1(workflow, issues);
1358
+ this.checkRule2(workflow, issues);
1359
+ this.checkRule3(workflow, issues);
1360
+ this.checkRule4(workflow, issues);
1361
+ this.checkRule5(workflow, issues);
1362
+ this.checkRule6(workflow, issues);
1363
+ this.checkRule7(workflow, issues);
1364
+ this.checkRule8(workflow, issues);
1365
+ this.checkRule9(workflow, issues);
1366
+ this.checkRule10(workflow, issues);
1367
+ this.checkRule11(workflow, issues);
1368
+ this.checkRule12(workflow, issues);
1369
+ this.checkRule13(workflow, issues);
1370
+ this.checkRule14(workflow, issues);
1371
+ this.checkRule15(workflow, issues);
1372
+ this.checkRule16(workflow, issues);
1373
+ this.checkRule17(workflow, issues);
1374
+ this.checkRule18(workflow, issues);
1375
+ this.checkRule19(workflow, issues);
1376
+ this.checkRule20(workflow, issues);
1377
+ this.checkRule21(workflow, issues);
1378
+ this.checkRule22(workflow, issues);
1379
+ this.checkRule23(workflow, issues);
1380
+ this.checkRule24(workflow, issues);
1381
+ this.checkRule25(workflow, issues);
1382
+ this.checkRule26(workflow, issues);
1383
+ this.checkRule27(workflow, issues);
1384
+ this.checkRule28(workflow, issues);
1385
+ this.checkRule29(workflow, issues);
1386
+ this.checkRule30(workflow, issues);
1387
+ this.checkRule31(workflow, issues);
1388
+ this.checkRule32(workflow, issues);
1389
+ this.checkRule33(workflow, issues);
1390
+ this.checkRule34(workflow, issues);
1391
+ if (Array.isArray(workflow.nodes)) {
1392
+ const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
1393
+ for (const issue of issues) {
1394
+ if (issue.nodeId && !issue.nodeType) {
1395
+ const nt = nodeById.get(issue.nodeId);
1396
+ if (nt) issue.nodeType = nt;
1397
+ }
1398
+ }
1399
+ }
1400
+ const errors = issues.filter((i) => i.severity === "error");
1401
+ return { valid: errors.length === 0, issues };
1402
+ }
1403
+ err(issues, rule, message, nodeId, nodeType) {
1404
+ const issue = { rule, severity: "error", message };
1405
+ if (nodeId !== void 0) issue.nodeId = nodeId;
1406
+ if (nodeType !== void 0) issue.nodeType = nodeType;
1407
+ issues.push(issue);
1408
+ }
1409
+ warn(issues, rule, message, nodeId, nodeType) {
1410
+ const issue = { rule, severity: "warn", message };
1411
+ if (nodeId !== void 0) issue.nodeId = nodeId;
1412
+ if (nodeType !== void 0) issue.nodeType = nodeType;
1413
+ issues.push(issue);
1414
+ }
1415
+ isTriggerNode(node) {
1416
+ if (this.registry.isTrigger(node.type)) return true;
1417
+ return TRIGGER_TYPE_PATTERNS.some((p) => p.test(node.type));
1418
+ }
1419
+ // Rule 1: name is a non-empty string
1420
+ checkRule1(w, issues) {
1421
+ if (typeof w.name !== "string" || w.name.trim() === "") {
1422
+ this.err(issues, 1, "Workflow name is required and must be a non-empty string");
1423
+ }
1424
+ }
1425
+ // Rule 2: nodes is an array with at least one element
1426
+ checkRule2(w, issues) {
1427
+ if (!Array.isArray(w.nodes) || w.nodes.length === 0) {
1428
+ this.err(issues, 2, "Workflow must have at least one node");
1429
+ }
1430
+ }
1431
+ // Rule 3: every node has a non-empty id
1432
+ checkRule3(w, issues) {
1433
+ if (!Array.isArray(w.nodes)) return;
1434
+ for (const node of w.nodes) {
1435
+ if (typeof node.id !== "string" || node.id.trim() === "") {
1436
+ this.err(issues, 3, `Node "${node.name ?? "unknown"}" is missing a valid id`, node.id);
1437
+ }
1438
+ }
1439
+ }
1440
+ // Rule 4: node ids are unique
1441
+ checkRule4(w, issues) {
1442
+ if (!Array.isArray(w.nodes)) return;
1443
+ const seen = /* @__PURE__ */ new Set();
1444
+ for (const node of w.nodes) {
1445
+ if (!node.id) continue;
1446
+ if (seen.has(node.id)) {
1447
+ this.err(issues, 4, `Duplicate node id: "${node.id}"`, node.id);
1448
+ }
1449
+ seen.add(node.id);
1450
+ }
1451
+ }
1452
+ // Rule 5: every node has a non-empty type string
1453
+ checkRule5(w, issues) {
1454
+ if (!Array.isArray(w.nodes)) return;
1455
+ for (const node of w.nodes) {
1456
+ if (typeof node.type !== "string" || node.type.trim() === "") {
1457
+ this.err(issues, 5, `Node "${node.name ?? node.id}" is missing a type`, node.id);
1458
+ }
1459
+ }
1460
+ }
1461
+ // Rule 6: every node has a positive typeVersion number
1462
+ checkRule6(w, issues) {
1463
+ if (!Array.isArray(w.nodes)) return;
1464
+ for (const node of w.nodes) {
1465
+ if (typeof node.typeVersion !== "number" || node.typeVersion <= 0) {
1466
+ this.err(issues, 6, `Node "${node.name}" has invalid typeVersion: ${String(node.typeVersion)}`, node.id);
1467
+ }
1468
+ }
1469
+ }
1470
+ // Rule 7: every node has a valid [x, y] position
1471
+ checkRule7(w, issues) {
1472
+ if (!Array.isArray(w.nodes)) return;
1473
+ for (const node of w.nodes) {
1474
+ const pos = node.position;
1475
+ if (!Array.isArray(pos) || pos.length !== 2 || typeof pos[0] !== "number" || typeof pos[1] !== "number") {
1476
+ this.err(issues, 7, `Node "${node.name}" has invalid position (must be [x, y])`, node.id);
1477
+ }
1478
+ }
1479
+ }
1480
+ // Rule 8: every node has a non-empty name
1481
+ checkRule8(w, issues) {
1482
+ if (!Array.isArray(w.nodes)) return;
1483
+ for (const node of w.nodes) {
1484
+ if (typeof node.name !== "string" || node.name.trim() === "") {
1485
+ this.err(issues, 8, `Node with id "${node.id}" is missing a name`, node.id);
1486
+ }
1487
+ }
1488
+ }
1489
+ // Rule 9: connections is a plain object
1490
+ checkRule9(w, issues) {
1491
+ if (typeof w.connections !== "object" || w.connections === null || Array.isArray(w.connections)) {
1492
+ this.err(issues, 9, "connections must be a plain object (use {} for single-node workflows)");
1493
+ }
1494
+ }
1495
+ // Rule 10: every connection target node name exists in nodes
1496
+ checkRule10(w, issues) {
1497
+ if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
1498
+ const nodeNames = new Set(w.nodes.map((n) => n.name));
1499
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
1500
+ if (!nodeNames.has(sourceName)) {
1501
+ this.err(issues, 10, `Connection source "${sourceName}" does not exist in nodes`);
1502
+ continue;
1503
+ }
1504
+ if (typeof outputs !== "object" || outputs === null) continue;
1505
+ for (const portGroup of Object.values(outputs)) {
1506
+ if (!Array.isArray(portGroup)) continue;
1507
+ for (const targets of portGroup) {
1508
+ if (!Array.isArray(targets)) continue;
1509
+ for (const target of targets) {
1510
+ const t = target;
1511
+ if (typeof t?.node === "string" && !nodeNames.has(t.node)) {
1512
+ this.err(issues, 10, `Connection target "${t.node}" does not exist in nodes`);
1513
+ }
1514
+ }
1515
+ }
1516
+ }
1517
+ }
1518
+ }
1519
+ // Rule 11 (WARN): every non-trigger node has at least one incoming connection
1520
+ checkRule11(w, issues) {
1521
+ if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
1522
+ const reachable = /* @__PURE__ */ new Set();
1523
+ const aiSubNodeSources = /* @__PURE__ */ new Set();
1524
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
1525
+ if (typeof outputs !== "object" || outputs === null) continue;
1526
+ let hasAiPort = false;
1527
+ for (const [portName, portGroup] of Object.entries(outputs)) {
1528
+ if (!Array.isArray(portGroup)) continue;
1529
+ const isAiPort = portName.startsWith("ai_");
1530
+ if (isAiPort) hasAiPort = true;
1531
+ for (const targets of portGroup) {
1532
+ if (!Array.isArray(targets)) continue;
1533
+ for (const target of targets) {
1534
+ const t = target;
1535
+ if (typeof t?.node === "string") reachable.add(t.node);
1536
+ }
1537
+ }
1538
+ }
1539
+ if (hasAiPort) aiSubNodeSources.add(sourceName);
1540
+ }
1541
+ for (const node of w.nodes) {
1542
+ if (node.type.includes("stickyNote")) continue;
1543
+ if (this.isTriggerNode(node)) continue;
1544
+ if (aiSubNodeSources.has(node.name)) continue;
1545
+ if (!reachable.has(node.name)) {
1546
+ this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
1547
+ }
1548
+ }
1549
+ }
1550
+ // Rule 12: forbidden fields absent from workflow root
1551
+ checkRule12(w, issues) {
1552
+ const wObj = w;
1553
+ for (const field of FORBIDDEN_ON_CREATE) {
1554
+ if (field in wObj) {
1555
+ this.err(issues, 12, `Forbidden field "${field}" present in workflow \u2014 remove it before deploying`);
1556
+ }
1557
+ }
1558
+ }
1559
+ // Rule 13: settings, if present, is a plain object
1560
+ checkRule13(w, issues) {
1561
+ if (w.settings !== void 0) {
1562
+ if (typeof w.settings !== "object" || w.settings === null || Array.isArray(w.settings)) {
1563
+ this.err(issues, 13, "workflow.settings must be a plain object");
1564
+ }
1565
+ }
1566
+ }
1567
+ // Rule 14: at least one trigger node is present
1568
+ checkRule14(w, issues) {
1569
+ if (!Array.isArray(w.nodes)) return;
1570
+ const hasTrigger = w.nodes.some((n) => this.isTriggerNode(n));
1571
+ if (!hasTrigger) {
1572
+ this.err(issues, 14, "Workflow must contain at least one trigger node");
1573
+ }
1574
+ }
1575
+ // Rule 15: node type string matches expected format
1576
+ checkRule15(w, issues) {
1577
+ if (!Array.isArray(w.nodes)) return;
1578
+ for (const node of w.nodes) {
1579
+ if (typeof node.type !== "string") continue;
1580
+ if (!NODE_TYPE_PATTERN.test(node.type)) {
1581
+ this.err(issues, 15, `Node "${node.name}" has malformed type string: "${node.type}"`, node.id);
1582
+ }
1583
+ }
1584
+ }
1585
+ // Rule 16: node names are unique within the workflow
1586
+ checkRule16(w, issues) {
1587
+ if (!Array.isArray(w.nodes)) return;
1588
+ const seen = /* @__PURE__ */ new Set();
1589
+ for (const node of w.nodes) {
1590
+ if (!node.name) continue;
1591
+ if (seen.has(node.name)) {
1592
+ this.err(issues, 16, `Duplicate node name: "${node.name}"`, node.id);
1593
+ }
1594
+ seen.add(node.name);
1595
+ }
1596
+ }
1597
+ // Rule 17: credentials shape — each entry has id and name
1598
+ checkRule17(w, issues) {
1599
+ if (!Array.isArray(w.nodes)) return;
1600
+ for (const node of w.nodes) {
1601
+ if (!node.credentials) continue;
1602
+ for (const [credType, credRef] of Object.entries(node.credentials)) {
1603
+ if (typeof credRef !== "object" || credRef === null) {
1604
+ this.err(issues, 17, `Node "${node.name}" credential "${credType}" must be an object with id and name`, node.id);
1605
+ continue;
1606
+ }
1607
+ const ref = credRef;
1608
+ if (typeof ref["id"] !== "string" || ref["id"].trim() === "" || typeof ref["name"] !== "string" || ref["name"].trim() === "") {
1609
+ this.err(issues, 17, `Node "${node.name}" credential "${credType}" must have non-empty string id and name fields`, node.id);
1610
+ }
1611
+ }
1612
+ }
1613
+ }
1614
+ // Rule 18 (ERROR): AI connections must originate from sub-nodes, not the agent/chain root
1615
+ checkRule18(w, issues) {
1616
+ if (typeof w.connections !== "object" || w.connections === null) return;
1617
+ const agentTypes = /* @__PURE__ */ new Set([
1618
+ "@n8n/n8n-nodes-langchain.agent",
1619
+ "@n8n/n8n-nodes-langchain.chainLlm",
1620
+ "@n8n/n8n-nodes-langchain.chainRetrievalQa",
1621
+ "@n8n/n8n-nodes-langchain.chainSummarization"
1622
+ ]);
1623
+ if (!Array.isArray(w.nodes)) return;
1624
+ const nodesByName = new Map(w.nodes.map((n) => [n.name, n]));
1625
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
1626
+ const sourceNode = nodesByName.get(sourceName);
1627
+ if (!sourceNode) continue;
1628
+ if (!agentTypes.has(sourceNode.type)) continue;
1629
+ if (typeof outputs !== "object" || outputs === null) continue;
1630
+ for (const connType of AI_CONNECTION_TYPES) {
1631
+ if (connType in outputs) {
1632
+ this.err(
1633
+ issues,
1634
+ 18,
1635
+ `Node "${sourceName}" uses AI connection type "${connType}" as a SOURCE \u2014 AI sub-nodes should be the source, not the agent/chain root`,
1636
+ sourceNode.id
1637
+ );
1638
+ }
1639
+ }
1640
+ }
1641
+ }
1642
+ // Rule 19 (WARN): typeVersion is within known safe range for registered node types.
1643
+ // In lenient mode (KAIROS_REGISTRY_STRICT != 'true'), versions higher than the known
1644
+ // max are allowed — they likely represent newer n8n releases Kairos hasn't catalogued yet.
1645
+ checkRule19(w, issues) {
1646
+ if (!Array.isArray(w.nodes)) return;
1647
+ const strict = process.env["KAIROS_REGISTRY_STRICT"] === "true";
1648
+ for (const node of w.nodes) {
1649
+ if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
1650
+ if (this.registry.isVersionSafe(node.type, node.typeVersion)) continue;
1651
+ if (!strict && this.registry.isVersionNewer(node.type, node.typeVersion)) continue;
1652
+ this.warn(
1653
+ issues,
1654
+ 19,
1655
+ `Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
1656
+ node.id
1657
+ );
1658
+ }
1659
+ }
1660
+ // Rule 20 (WARN): cycle detection — no node should be reachable from itself
1661
+ // Exempts splitInBatches loops which are an intentional n8n pattern
1662
+ checkRule20(w, issues) {
1663
+ if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
1664
+ const splitBatchNodes = new Set(
1665
+ w.nodes.filter((n) => n.type.includes("splitInBatches")).map((n) => n.name)
1666
+ );
1667
+ const adj = /* @__PURE__ */ new Map();
1668
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
1669
+ if (typeof outputs !== "object" || outputs === null) continue;
1670
+ const targets = [];
1671
+ for (const portGroup of Object.values(outputs)) {
1672
+ if (!Array.isArray(portGroup)) continue;
1673
+ for (const conns of portGroup) {
1674
+ if (!Array.isArray(conns)) continue;
1675
+ for (const conn of conns) {
1676
+ const t = conn;
1677
+ if (typeof t?.node === "string") {
1678
+ if (splitBatchNodes.has(t.node)) continue;
1679
+ targets.push(t.node);
1680
+ }
1681
+ }
1682
+ }
1683
+ }
1684
+ adj.set(sourceName, targets);
1685
+ }
1686
+ const WHITE = 0, GRAY = 1, BLACK = 2;
1687
+ const color = /* @__PURE__ */ new Map();
1688
+ for (const node of w.nodes) color.set(node.name, WHITE);
1689
+ const dfs = (name) => {
1690
+ color.set(name, GRAY);
1691
+ for (const neighbor of adj.get(name) ?? []) {
1692
+ const c = color.get(neighbor);
1693
+ if (c === GRAY) return true;
1694
+ if (c === WHITE && dfs(neighbor)) return true;
1695
+ }
1696
+ color.set(name, BLACK);
1697
+ return false;
1698
+ };
1699
+ for (const node of w.nodes) {
1700
+ if (color.get(node.name) === WHITE && dfs(node.name)) {
1701
+ this.warn(issues, 20, "Workflow contains a connection cycle \u2014 this may cause infinite loops");
1702
+ return;
1703
+ }
1704
+ }
1705
+ }
1706
+ // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
1707
+ checkRule21(w, issues) {
1708
+ if (!Array.isArray(w.nodes)) return;
1709
+ const webhooksNeedingResponse = w.nodes.filter((n) => {
1710
+ if (!n.type.includes("webhook")) return false;
1711
+ const params = n.parameters;
1712
+ return params?.responseMode === "responseNode";
1713
+ });
1714
+ if (webhooksNeedingResponse.length === 0) return;
1715
+ const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
1716
+ if (!hasRespondNode) {
1717
+ for (const wh of webhooksNeedingResponse) {
1718
+ this.warn(
1719
+ issues,
1720
+ 21,
1721
+ `Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
1722
+ wh.id
1723
+ );
1724
+ }
1725
+ }
1726
+ }
1727
+ // Rule 22 (WARN): check requiredParams from registry
1728
+ checkRule22(w, issues) {
1729
+ if (!Array.isArray(w.nodes)) return;
1730
+ for (const node of w.nodes) {
1731
+ if (typeof node.type !== "string") continue;
1732
+ const required = this.registry.getRequiredParams(node.type);
1733
+ if (required.length === 0) continue;
1734
+ const params = node.parameters ?? {};
1735
+ for (const param of required) {
1736
+ const value = params[param];
1737
+ if (value === void 0 || value === null || value === "") {
1738
+ this.warn(
1739
+ issues,
1740
+ 22,
1741
+ `Node "${node.name}" (${node.type}) is missing required parameter "${param}"`,
1742
+ node.id
1743
+ );
1744
+ }
1745
+ }
1746
+ }
1747
+ }
1748
+ // Rule 23 (WARN): unknown node types not in registry
1749
+ checkRule23(w, issues) {
1750
+ if (!Array.isArray(w.nodes)) return;
1751
+ for (const node of w.nodes) {
1752
+ if (typeof node.type !== "string") continue;
1753
+ if (node.type.includes("stickyNote")) continue;
1754
+ if (!NODE_TYPE_PATTERN.test(node.type)) continue;
1755
+ if (!this.registry.isKnown(node.type)) {
1756
+ this.warn(
1757
+ issues,
1758
+ 23,
1759
+ `Node "${node.name}" uses unknown type "${node.type}" \u2014 it may not exist in n8n`,
1760
+ node.id
1761
+ );
1762
+ }
1763
+ }
1764
+ }
1765
+ // Rule 24 (WARN): deprecated accessor syntax in expressions
1766
+ checkRule24(w, issues) {
1767
+ if (!Array.isArray(w.nodes)) return;
1768
+ const deprecated = /\$node\s*\[/;
1769
+ for (const node of w.nodes) {
1770
+ for (const expr of this.extractExpressions(node.parameters)) {
1771
+ if (deprecated.test(expr)) {
1772
+ this.warn(
1773
+ issues,
1774
+ 24,
1775
+ `Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
1776
+ node.id
1777
+ );
1778
+ break;
1779
+ }
1780
+ }
1781
+ }
1782
+ }
1783
+ // Rule 25 (WARN): wrong item index assumptions in expressions
1784
+ checkRule25(w, issues) {
1785
+ if (!Array.isArray(w.nodes)) return;
1786
+ const itemIndex = /\$json\s*\.\s*items\s*\[/;
1787
+ for (const node of w.nodes) {
1788
+ for (const expr of this.extractExpressions(node.parameters)) {
1789
+ if (itemIndex.test(expr)) {
1790
+ this.warn(
1791
+ issues,
1792
+ 25,
1793
+ `Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
1794
+ node.id
1795
+ );
1796
+ break;
1797
+ }
1798
+ }
1799
+ }
1800
+ }
1801
+ // Rule 26 (WARN): missing .first() or .all() on node references
1802
+ checkRule26(w, issues) {
1803
+ if (!Array.isArray(w.nodes)) return;
1804
+ const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
1805
+ for (const node of w.nodes) {
1806
+ for (const expr of this.extractExpressions(node.parameters)) {
1807
+ if (bareRef.test(expr)) {
1808
+ this.warn(
1809
+ issues,
1810
+ 26,
1811
+ `Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
1812
+ node.id
1813
+ );
1814
+ break;
1815
+ }
1816
+ }
1817
+ }
1818
+ }
1819
+ extractExpressions(params) {
1820
+ const expressions = [];
1821
+ const walk = (val) => {
1822
+ if (typeof val === "string") {
1823
+ if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
1824
+ expressions.push(val);
1825
+ }
1826
+ } else if (Array.isArray(val)) {
1827
+ for (const item of val) walk(item);
1828
+ } else if (val !== null && typeof val === "object") {
1829
+ for (const v of Object.values(val)) walk(v);
1830
+ }
1831
+ };
1832
+ walk(params);
1833
+ return expressions;
1834
+ }
1835
+ // Rule 27 (WARN): httpRequest URL is a placeholder
1836
+ checkRule27(w, issues) {
1837
+ if (!Array.isArray(w.nodes)) return;
1838
+ const PLACEHOLDER_RE = [
1839
+ /^https?:\/\/example\.com/i,
1840
+ /your[-_]?(api[-_]?)?url/i,
1841
+ /^https?:\/\/$/,
1842
+ /^<.+>$/,
1843
+ /placeholder/i
1844
+ ];
1845
+ for (const node of w.nodes) {
1846
+ if (node.type !== "n8n-nodes-base.httpRequest") continue;
1847
+ const params = node.parameters;
1848
+ const url = params?.["url"];
1849
+ if (typeof url !== "string" || url.trim() === "") continue;
1850
+ if (PLACEHOLDER_RE.some((re) => re.test(url.trim()))) {
1851
+ this.warn(
1852
+ issues,
1853
+ 27,
1854
+ `Node "${node.name}" httpRequest URL appears to be a placeholder: "${url}" \u2014 replace with your actual endpoint`,
1855
+ node.id
1856
+ );
1857
+ }
1858
+ }
1859
+ }
1860
+ // Rule 28 (WARN): code node with empty or comment-only code
1861
+ checkRule28(w, issues) {
1862
+ if (!Array.isArray(w.nodes)) return;
1863
+ for (const node of w.nodes) {
1864
+ if (node.type !== "n8n-nodes-base.code") continue;
1865
+ const params = node.parameters;
1866
+ const jsCode = typeof params?.["jsCode"] === "string" ? params["jsCode"] : "";
1867
+ const pythonCode = typeof params?.["pythonCode"] === "string" ? params["pythonCode"] : "";
1868
+ const code = jsCode || pythonCode;
1869
+ const stripped = code.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/#[^\n]*/g, "").trim();
1870
+ if (!stripped) {
1871
+ this.warn(issues, 28, `Node "${node.name}" code node has no executable code`, node.id);
1872
+ }
1873
+ }
1874
+ }
1875
+ // Rule 29 (WARN): slack node message operation missing channel
1876
+ checkRule29(w, issues) {
1877
+ if (!Array.isArray(w.nodes)) return;
1878
+ for (const node of w.nodes) {
1879
+ if (node.type !== "n8n-nodes-base.slack") continue;
1880
+ const params = node.parameters;
1881
+ const resource = params?.["resource"];
1882
+ const operation = params?.["operation"];
1883
+ const isMessageOp = resource === "message" || operation === "sendMessage" || operation === "post";
1884
+ if (!isMessageOp) continue;
1885
+ const channel = params?.["channel"] ?? params?.["channelId"];
1886
+ const rlValue = typeof channel === "object" && channel !== null ? channel["value"] : void 0;
1887
+ const isEmpty = channel === void 0 || channel === null || typeof channel === "string" && channel.trim() === "" || typeof channel === "object" && (!rlValue || typeof rlValue === "string" && rlValue.trim() === "");
1888
+ if (isEmpty) {
1889
+ this.warn(issues, 29, `Node "${node.name}" Slack message has no channel specified`, node.id);
1890
+ }
1891
+ }
1892
+ }
1893
+ // Rule 30 (WARN): gmail node send operation missing recipient
1894
+ checkRule30(w, issues) {
1895
+ if (!Array.isArray(w.nodes)) return;
1896
+ for (const node of w.nodes) {
1897
+ if (node.type !== "n8n-nodes-base.gmail") continue;
1898
+ const params = node.parameters;
1899
+ const operation = params?.["operation"];
1900
+ if (operation !== "send") continue;
1901
+ const to = params?.["to"] ?? params?.["toList"];
1902
+ const isEmpty = to === void 0 || to === null || typeof to === "string" && to.trim() === "" || Array.isArray(to) && to.length === 0;
1903
+ if (isEmpty) {
1904
+ this.warn(issues, 30, `Node "${node.name}" gmail send has no recipient (to) specified`, node.id);
1905
+ }
1906
+ }
1907
+ }
1908
+ // Rule 31 (WARN): if node with empty conditions
1909
+ checkRule31(w, issues) {
1910
+ if (!Array.isArray(w.nodes)) return;
1911
+ for (const node of w.nodes) {
1912
+ if (node.type !== "n8n-nodes-base.if") continue;
1913
+ const params = node.parameters;
1914
+ const conditions = params?.["conditions"];
1915
+ if (conditions === void 0 || conditions === null) {
1916
+ this.warn(issues, 31, `Node "${node.name}" if node has no conditions defined`, node.id);
1917
+ continue;
1918
+ }
1919
+ if (typeof conditions === "object" && !Array.isArray(conditions)) {
1920
+ const conds = conditions["conditions"];
1921
+ if (!Array.isArray(conds) || conds.length === 0) {
1922
+ this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
1923
+ }
1924
+ } else if (Array.isArray(conditions) && conditions.length === 0) {
1925
+ this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
1926
+ }
1927
+ }
1928
+ }
1929
+ // Rule 32 (WARN): set node with no assignments
1930
+ checkRule32(w, issues) {
1931
+ if (!Array.isArray(w.nodes)) return;
1932
+ for (const node of w.nodes) {
1933
+ if (node.type !== "n8n-nodes-base.set") continue;
1934
+ const params = node.parameters;
1935
+ const assignmentsObj = params?.["assignments"];
1936
+ const assignmentsArr = assignmentsObj?.["assignments"];
1937
+ const valuesObj = params?.["values"];
1938
+ const hasV1 = valuesObj && Object.values(valuesObj).some((v) => Array.isArray(v) && v.length > 0);
1939
+ const hasV3 = Array.isArray(assignmentsArr) && assignmentsArr.length > 0;
1940
+ if (!hasV1 && !hasV3) {
1941
+ this.warn(
1942
+ issues,
1943
+ 32,
1944
+ `Node "${node.name}" set node has no fields defined \u2014 it will pass data through unchanged`,
1945
+ node.id
1946
+ );
1947
+ }
1948
+ }
1949
+ }
1950
+ // Rule 33 (WARN): scheduleTrigger with no schedule rules
1951
+ checkRule33(w, issues) {
1952
+ if (!Array.isArray(w.nodes)) return;
1953
+ for (const node of w.nodes) {
1954
+ if (node.type !== "n8n-nodes-base.scheduleTrigger") continue;
1955
+ const params = node.parameters;
1956
+ const rule = params?.["rule"];
1957
+ const intervals = rule?.["interval"];
1958
+ if (!Array.isArray(intervals) || intervals.length === 0) {
1959
+ this.warn(issues, 33, `Node "${node.name}" scheduleTrigger has no schedule rules defined`, node.id);
1960
+ }
1961
+ }
1962
+ }
1963
+ // Rule 34 (WARN): webhook path contains spaces, starts with slash, or looks like a full URL
1964
+ checkRule34(w, issues) {
1965
+ if (!Array.isArray(w.nodes)) return;
1966
+ for (const node of w.nodes) {
1967
+ if (node.type !== "n8n-nodes-base.webhook") continue;
1968
+ const params = node.parameters;
1969
+ const path = params?.["path"];
1970
+ if (typeof path !== "string") continue;
1971
+ if (/\s/.test(path)) {
1972
+ this.warn(
1973
+ issues,
1974
+ 34,
1975
+ `Node "${node.name}" webhook path contains spaces: "${path}" \u2014 use hyphens or underscores instead`,
1976
+ node.id
1977
+ );
1978
+ } else if (/^https?:\/\//i.test(path)) {
1979
+ this.warn(
1980
+ issues,
1981
+ 34,
1982
+ `Node "${node.name}" webhook path looks like a full URL \u2014 it should be a relative path (e.g. "my-hook")`,
1983
+ node.id
1984
+ );
1985
+ } else if (path.startsWith("/")) {
1986
+ this.warn(
1987
+ issues,
1988
+ 34,
1989
+ `Node "${node.name}" webhook path starts with "/" \u2014 n8n adds the leading slash automatically`,
1990
+ node.id
1991
+ );
1992
+ }
1993
+ }
1994
+ }
1995
+ };
1996
+
1997
+ // src/errors/generation-error.ts
1998
+ var GenerationError = class extends KairosError {
1999
+ constructor(message, cause) {
2000
+ super(message, cause);
2001
+ this.name = "GenerationError";
2002
+ }
2003
+ };
2004
+
2005
+ // src/errors/response-parse-error.ts
2006
+ var ResponseParseError = class extends KairosError {
2007
+ constructor(message, cause) {
2008
+ super(message, cause);
2009
+ this.name = "ResponseParseError";
2010
+ }
2011
+ };
2012
+
2013
+ // src/errors/validation-error.ts
2014
+ var ValidationError = class extends KairosError {
2015
+ constructor(message, issues, attemptMetadata, warnedRules) {
2016
+ super(message);
2017
+ this.issues = issues;
2018
+ this.attemptMetadata = attemptMetadata;
2019
+ this.warnedRules = warnedRules;
2020
+ this.name = "ValidationError";
2021
+ }
2022
+ issues;
2023
+ attemptMetadata;
2024
+ warnedRules;
2025
+ };
2026
+
2027
+ // src/templates/safety.ts
2028
+ var BLOCKED_NODE_TYPES = /* @__PURE__ */ new Set([
2029
+ "n8n-nodes-base.code",
2030
+ "n8n-nodes-base.executeCommand",
2031
+ "n8n-nodes-base.ssh"
2032
+ ]);
2033
+ var REVIEW_NODE_TYPES = /* @__PURE__ */ new Set([
2034
+ "n8n-nodes-base.httpRequest"
2035
+ ]);
2036
+ var SECRET_PATTERNS = [
2037
+ /sk-[a-zA-Z0-9]{20,}/,
2038
+ /ghp_[a-zA-Z0-9]{36}/,
2039
+ /xoxb-[0-9]+-[0-9]+-[a-zA-Z0-9]+/,
2040
+ /AIza[a-zA-Z0-9_-]{35}/,
2041
+ /AKIA[A-Z0-9]{16}/
2042
+ ];
2043
+ var SECRET_PREFIXES = ["sk-", "ghp_", "xoxb-", "AIza", "AKIA"];
2044
+ function collectExpressionStrings(obj, out = []) {
2045
+ if (typeof obj === "string") {
2046
+ if (obj.includes("={{")) out.push(obj);
2047
+ } else if (Array.isArray(obj)) {
2048
+ for (const item of obj) collectExpressionStrings(item, out);
2049
+ } else if (obj !== null && typeof obj === "object") {
2050
+ for (const val of Object.values(obj)) {
2051
+ collectExpressionStrings(val, out);
2052
+ }
2053
+ }
2054
+ return out;
2055
+ }
2056
+ function assessTemplateSafety(workflow) {
2057
+ const reasons = [];
2058
+ let worst = "safe";
2059
+ const escalate = (level, reason) => {
2060
+ reasons.push(reason);
2061
+ if (level === "blocked") worst = "blocked";
2062
+ else if (level === "review" && worst === "safe") worst = "review";
2063
+ };
2064
+ for (const node of workflow.nodes) {
2065
+ if (BLOCKED_NODE_TYPES.has(node.type)) {
2066
+ escalate("blocked", `Contains ${node.type} node "${node.name}"`);
2067
+ }
2068
+ if (REVIEW_NODE_TYPES.has(node.type)) {
2069
+ escalate("review", `Contains ${node.type} node "${node.name}"`);
2070
+ }
2071
+ const paramStr = JSON.stringify(node.parameters);
2072
+ for (const pattern of SECRET_PATTERNS) {
2073
+ if (pattern.test(paramStr)) {
2074
+ escalate("blocked", `Node "${node.name}" parameters contain a hardcoded secret`);
2075
+ break;
2076
+ }
2077
+ }
2078
+ const expressions = collectExpressionStrings(node.parameters);
2079
+ for (const expr of expressions) {
2080
+ for (const prefix of SECRET_PREFIXES) {
2081
+ if (expr.includes(prefix)) {
2082
+ escalate("review", `Node "${node.name}" has an expression containing credential-like prefix "${prefix}"`);
2083
+ break;
2084
+ }
2085
+ }
2086
+ }
2087
+ }
2088
+ return { trustLevel: worst, reasons };
2089
+ }
2090
+
2091
+ // src/templates/syncer.ts
2092
+ var N8N_TEMPLATE_API = "https://api.n8n.io/api/templates";
2093
+ var PAGE_SIZE = 50;
2094
+ var DELAY_BETWEEN_FETCHES_MS = 200;
2095
+ var DEFAULT_SETTINGS = {
2096
+ executionOrder: "v1",
2097
+ saveManualExecutions: true,
2098
+ timezone: "UTC"
2099
+ };
2100
+ var TemplateSyncer = class {
2101
+ constructor(library, logger) {
2102
+ this.library = library;
2103
+ this.validator = new N8nValidator();
2104
+ this.logger = logger;
2105
+ }
2106
+ library;
2107
+ validator;
2108
+ logger;
2109
+ async sync(options) {
2110
+ const maxTemplates = options?.maxTemplates ?? 500;
2111
+ await this.library.initialize();
2112
+ const existing = await this.library.list();
2113
+ const existingSourceIds = new Set(
2114
+ existing.filter((w) => w.sourceKind === "n8n-template" && w.sourceId).map((w) => w.sourceId)
2115
+ );
2116
+ const progress = {
2117
+ total: 0,
2118
+ processed: 0,
2119
+ saved: 0,
2120
+ skippedPaid: 0,
2121
+ skippedDuplicate: 0,
2122
+ blocked: 0,
2123
+ reviewed: 0
2124
+ };
2125
+ const templateIds = await this.fetchTemplateIds(maxTemplates, progress);
2126
+ for (const id of templateIds) {
2127
+ if (existingSourceIds.has(String(id))) {
2128
+ progress.skippedDuplicate++;
2129
+ progress.processed++;
2130
+ options?.onProgress?.(progress);
2131
+ continue;
2132
+ }
2133
+ try {
2134
+ await this.processTemplate(id, progress);
2135
+ } catch (err) {
2136
+ this.logger.warn(`Failed to process template ${id}`, { err: String(err) });
2137
+ }
2138
+ progress.processed++;
2139
+ options?.onProgress?.(progress);
2140
+ await new Promise((resolve) => setTimeout(resolve, DELAY_BETWEEN_FETCHES_MS));
2141
+ }
2142
+ return progress;
2143
+ }
2144
+ async fetchWithBackoff(url, maxRetries = 3) {
2145
+ let delayMs = DELAY_BETWEEN_FETCHES_MS;
2146
+ let lastResponse;
2147
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
2148
+ lastResponse = await fetch(url);
2149
+ if (lastResponse.status !== 429 && lastResponse.status !== 503) return lastResponse;
2150
+ if (attempt === maxRetries) break;
2151
+ const retryAfterHeader = lastResponse.headers.get("Retry-After");
2152
+ const waitMs = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : delayMs * Math.pow(2, attempt);
2153
+ this.logger.warn(`HTTP ${lastResponse.status} from template API, retrying in ${waitMs}ms`, { url, attempt });
2154
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
2155
+ }
2156
+ return lastResponse;
2157
+ }
2158
+ async fetchTemplateIds(max, progress) {
2159
+ const ids = [];
2160
+ let page = 1;
2161
+ while (ids.length < max) {
2162
+ const url = `${N8N_TEMPLATE_API}/search?page=${page}&rows=${PAGE_SIZE}`;
2163
+ const response = await this.fetchWithBackoff(url);
2164
+ if (!response.ok) break;
2165
+ const data = await response.json();
2166
+ progress.total = Math.min(data.totalWorkflows, max);
2167
+ for (const template of data.workflows) {
2168
+ if (ids.length >= max) break;
2169
+ if (template.price && template.price > 0) {
2170
+ progress.skippedPaid++;
2171
+ continue;
2172
+ }
2173
+ ids.push(template.id);
2174
+ }
2175
+ if (data.workflows.length < PAGE_SIZE) break;
2176
+ page++;
2177
+ await new Promise((resolve) => setTimeout(resolve, DELAY_BETWEEN_FETCHES_MS));
2178
+ }
2179
+ return ids;
2180
+ }
2181
+ async processTemplate(id, progress) {
2182
+ const url = `${N8N_TEMPLATE_API}/workflows/${id}`;
2183
+ const response = await this.fetchWithBackoff(url);
2184
+ if (!response.ok) return;
2185
+ const data = await response.json();
2186
+ const templateMeta = data.workflow;
2187
+ const rawWorkflow = templateMeta.workflow;
2188
+ if (!rawWorkflow?.nodes?.length) return;
2189
+ const workflow = {
2190
+ name: templateMeta.name,
2191
+ nodes: rawWorkflow.nodes.filter((n) => n.type && n.name),
2192
+ connections: rawWorkflow.connections,
2193
+ settings: rawWorkflow.settings ? { executionOrder: "v1", ...rawWorkflow.settings } : { ...DEFAULT_SETTINGS }
2194
+ };
2195
+ const validation = this.validator.validate(workflow);
2196
+ const validationErrors = validation.issues.filter((i) => i.severity === "error");
2197
+ if (validationErrors.length > 0) {
2198
+ progress.blocked++;
2199
+ this.logger.debug(`Template ${id} blocked: ${validationErrors.length} validation errors`);
2200
+ return;
2201
+ }
2202
+ const safety = assessTemplateSafety(workflow);
2203
+ if (safety.trustLevel === "blocked") {
2204
+ progress.blocked++;
2205
+ this.logger.debug(`Template ${id} blocked: ${safety.reasons.join(", ")}`);
2206
+ return;
2207
+ }
2208
+ if (safety.trustLevel === "review") {
2209
+ progress.reviewed++;
2210
+ }
2211
+ const description = this.cleanDescription(templateMeta.description);
2212
+ const autoTags = Array.from(new Set(
2213
+ workflow.nodes.flatMap((n) => {
2214
+ const bare = n.type.split(".").pop() ?? "";
2215
+ const tags = [bare];
2216
+ if (n.type.includes("Trigger") || n.type.includes("trigger")) tags.push(`trigger:${bare}`);
2217
+ if (n.type.includes("langchain")) tags.push("ai");
2218
+ return tags;
2219
+ })
2220
+ ));
2221
+ const metadata = {
2222
+ description,
2223
+ tags: autoTags,
2224
+ sourceKind: "n8n-template",
2225
+ sourceId: String(id),
2226
+ sourceUrl: `https://n8n.io/workflows/${id}`,
2227
+ trustLevel: safety.trustLevel
2228
+ };
2229
+ await this.library.save(workflow, metadata);
2230
+ progress.saved++;
2231
+ this.logger.debug(`Template ${id} saved: "${templateMeta.name}" (${safety.trustLevel})`);
2232
+ }
2233
+ cleanDescription(raw) {
2234
+ return raw.replace(/#{1,6}\s*/g, "").replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\n{3,}/g, "\n\n").trim().slice(0, 500);
2235
+ }
2236
+ };
2237
+
2238
+ // src/utils/logger.ts
2239
+ var nullLogger = {
2240
+ debug() {
2241
+ },
2242
+ info() {
2243
+ },
2244
+ warn() {
2245
+ },
2246
+ error() {
2247
+ }
2248
+ };
2249
+
2250
+ // src/telemetry/collector.ts
2251
+ var import_promises2 = require("fs/promises");
2252
+ var import_node_path2 = require("path");
2253
+ var import_node_os2 = require("os");
2254
+
2255
+ // src/telemetry/types.ts
2256
+ var TELEMETRY_SCHEMA_VERSION = 2;
2257
+
2258
+ // src/telemetry/collector.ts
2259
+ var TelemetryCollector = class {
2260
+ dir;
2261
+ sessionId;
2262
+ dirReady = null;
2263
+ constructor(dir) {
2264
+ this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
2265
+ this.sessionId = generateUUID();
2266
+ }
2267
+ async emit(eventType, data, runId) {
2268
+ const event = {
2269
+ schemaVersion: TELEMETRY_SCHEMA_VERSION,
2270
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2271
+ sessionId: this.sessionId,
2272
+ ...runId ? { runId } : {},
2273
+ eventType,
2274
+ data
2275
+ };
2276
+ if (!this.dirReady) {
2277
+ this.dirReady = (0, import_promises2.mkdir)(this.dir, { recursive: true }).then(() => {
2278
+ });
2279
+ }
2280
+ await this.dirReady;
2281
+ const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
2282
+ const filepath = (0, import_node_path2.join)(this.dir, filename);
2283
+ await (0, import_promises2.appendFile)(filepath, JSON.stringify(event) + "\n", "utf-8");
2284
+ }
2285
+ };
2286
+
2287
+ // src/telemetry/reader.ts
2288
+ var import_node_os3 = require("os");
2289
+ var import_node_path4 = require("path");
2290
+
2291
+ // src/telemetry/event-reader.ts
2292
+ var import_promises3 = require("fs/promises");
2293
+ var import_node_fs = require("fs");
2294
+ var import_node_path3 = require("path");
2295
+ var import_node_readline = require("readline");
2296
+ async function readTelemetryEvents(dir, days) {
2297
+ let files;
2298
+ try {
2299
+ files = await (0, import_promises3.readdir)(dir);
2300
+ } catch {
2301
+ return [];
2302
+ }
2303
+ const cutoff = /* @__PURE__ */ new Date();
2304
+ cutoff.setDate(cutoff.getDate() - days);
2305
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
2306
+ const todayStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2307
+ const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
2308
+ const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr && f <= `${todayStr}.jsonl`).sort();
2309
+ const events = [];
2310
+ for (const file of recentFiles) {
2311
+ const fileDate = file.replace(".jsonl", "");
2312
+ try {
2313
+ const rl = (0, import_node_readline.createInterface)({
2314
+ input: (0, import_node_fs.createReadStream)((0, import_node_path3.join)(dir, file), "utf-8"),
2315
+ crlfDelay: Infinity
2316
+ });
2317
+ for await (const line of rl) {
2318
+ if (!line.trim()) continue;
2319
+ try {
2320
+ events.push({ ...JSON.parse(line), fileDate });
2321
+ } catch {
2322
+ }
2323
+ }
2324
+ } catch {
2325
+ }
2326
+ }
2327
+ return events;
2328
+ }
2329
+
2330
+ // src/telemetry/reader.ts
2331
+ var TelemetryReader = class {
2332
+ dir;
2333
+ cache = null;
2334
+ cacheTime = 0;
2335
+ constructor(dir) {
2336
+ this.dir = dir ?? (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".kairos", "telemetry");
2337
+ }
2338
+ async getFailureRates(days = 30) {
2339
+ const now = Date.now();
2340
+ if (this.cache && now - this.cacheTime < 5 * 60 * 1e3) {
2341
+ return this.cache;
2342
+ }
2343
+ const events = await this.readRecentEvents(days);
2344
+ const buildSessions = new Set(
2345
+ events.filter((e) => e.eventType === "build_complete").map((e) => e.runId ?? e.sessionId)
2346
+ );
2347
+ const MIN_BUILDS_FOR_RATES = 3;
2348
+ if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
2349
+ const ruleSessions = /* @__PURE__ */ new Map();
2350
+ for (const event of events) {
2351
+ if (event.eventType !== "generation_attempt") continue;
2352
+ const eventKey = event.runId ?? event.sessionId;
2353
+ if (!buildSessions.has(eventKey)) continue;
2354
+ const data = event.data;
2355
+ if (data.validationPassed || !data.issues) continue;
2356
+ for (const issue of data.issues) {
2357
+ const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
2358
+ entry.sessions.add(eventKey);
2359
+ entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
2360
+ ruleSessions.set(issue.rule, entry);
2361
+ }
2362
+ }
2363
+ const rates = [];
2364
+ for (const [rule, entry] of ruleSessions) {
2365
+ let topMessage = "";
2366
+ let topCount = 0;
2367
+ for (const [msg, count] of entry.messages) {
2368
+ if (count > topCount) {
2369
+ topMessage = msg;
2370
+ topCount = count;
2371
+ }
2372
+ }
2373
+ rates.push({
2374
+ rule,
2375
+ failureCount: entry.sessions.size,
2376
+ totalBuilds: buildSessions.size,
2377
+ rate: entry.sessions.size / buildSessions.size,
2378
+ commonMessage: topMessage
2379
+ });
2380
+ }
2381
+ rates.sort((a, b) => b.rate - a.rate);
2382
+ this.cache = rates;
2383
+ this.cacheTime = now;
2384
+ return rates;
2385
+ }
2386
+ async readRecentEvents(days) {
2387
+ return readTelemetryEvents(this.dir, days);
2388
+ }
2389
+ };
2390
+
2391
+ // src/telemetry/pattern-analyzer.ts
2392
+ var import_promises4 = require("fs/promises");
2393
+ var import_node_path5 = require("path");
2394
+ var import_node_os4 = require("os");
2395
+
2396
+ // src/validation/rule-metadata.ts
2397
+ var VALIDATOR_RULE_IDS = Array.from({ length: 34 }, (_, i) => i + 1);
2398
+ var RULE_PIPELINE_STAGES = {
2399
+ 1: "node_generation",
2400
+ 2: "node_generation",
2401
+ 3: "node_generation",
2402
+ 4: "node_generation",
2403
+ 5: "node_generation",
2404
+ 6: "node_generation",
2405
+ 7: "node_generation",
2406
+ 8: "node_generation",
2407
+ 9: "connection_wiring",
2408
+ 10: "connection_wiring",
2409
+ 11: "connection_wiring",
2410
+ 12: "workflow_structure",
2411
+ 13: "node_generation",
2412
+ 14: "workflow_structure",
2413
+ 15: "node_generation",
2414
+ 16: "node_generation",
2415
+ 17: "credential_injection",
2416
+ 18: "connection_wiring",
2417
+ 19: "node_generation",
2418
+ 20: "connection_wiring",
2419
+ 21: "workflow_structure",
2420
+ 22: "workflow_structure",
2421
+ 23: "node_generation",
2422
+ 24: "expression_syntax",
2423
+ 25: "expression_syntax",
2424
+ 26: "expression_syntax",
2425
+ 27: "node_generation",
2426
+ 28: "node_generation",
2427
+ 29: "node_generation",
2428
+ 30: "node_generation",
2429
+ 31: "node_generation",
2430
+ 32: "node_generation",
2431
+ 33: "node_generation",
2432
+ 34: "node_generation"
2433
+ };
2434
+ var RULE_MITIGATIONS = {
2435
+ 1: "Provide a non-empty workflow name string",
2436
+ 2: "Include at least one node in the nodes array",
2437
+ 3: "Every node must have a unique UUID v4 string as its id field",
2438
+ 4: "Ensure all node ids are unique \u2014 no two nodes can share the same id",
2439
+ 5: "Every node must have a non-empty type string",
2440
+ 6: "Every node must have a positive integer typeVersion",
2441
+ 7: "Every node must have a position array of exactly [x, y] numbers",
2442
+ 8: "Every node must have a non-empty name string",
2443
+ 9: "connections must be a plain object (use {} if no connections)",
2444
+ 10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
2445
+ 11: "Every non-trigger node should have at least one incoming connection",
2446
+ 12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
2447
+ 13: "workflow.settings must be a plain object if present",
2448
+ 14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
2449
+ 15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
2450
+ 16: "All node names must be unique within the workflow",
2451
+ 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',
2452
+ 18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
2453
+ 19: "Use known safe typeVersion values for each node type",
2454
+ 20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
2455
+ 21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
2456
+ 22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
2457
+ 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
2458
+ 24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
2459
+ 25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
2460
+ 26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime',
2461
+ 27: 'Replace placeholder URLs with your actual API endpoint \u2014 do not use "example.com" or "YOUR_URL" patterns',
2462
+ 28: "Add executable code to the code node \u2014 empty or comment-only code nodes do nothing at runtime",
2463
+ 29: "Set the channel parameter for Slack message operations (channelId with __rl object, or channel as string)",
2464
+ 30: "Set the to parameter for Gmail send operations with at least one recipient email address",
2465
+ 31: "Add at least one condition to the if node \u2014 conditions.conditions array must be non-empty",
2466
+ 32: "Add field assignments to the set node \u2014 assignments.assignments array must be non-empty for typeVersion 3.x",
2467
+ 33: "Add at least one schedule rule to scheduleTrigger \u2014 rule.interval array must have at least one entry",
2468
+ 34: 'Webhook path must be a relative path without spaces, leading slashes, or protocol prefixes (e.g. "my-hook")'
2469
+ };
2470
+
2471
+ // src/telemetry/pattern-analyzer.ts
2472
+ var PATTERN_SCHEMA_VERSION = 2;
2473
+ var PatternAnalyzer = class _PatternAnalyzer {
2474
+ telemetryDir;
2475
+ outputDir;
2476
+ _cachedEvents = null;
2477
+ _cachedPreviousPatterns = null;
2478
+ constructor(telemetryDir) {
2479
+ const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
2480
+ this.telemetryDir = telemetryDir ?? defaultDir;
2481
+ this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
2482
+ }
2483
+ async loadPreviousPatterns() {
2484
+ if (this._cachedPreviousPatterns !== null) return this._cachedPreviousPatterns;
2485
+ try {
2486
+ const raw = await (0, import_promises4.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
2487
+ const prev = JSON.parse(raw);
2488
+ const version = prev.schemaVersion ?? 0;
2489
+ const patterns = prev.topFailureRules ?? [];
2490
+ this._cachedPreviousPatterns = version === PATTERN_SCHEMA_VERSION ? patterns : this.migratePatterns(patterns, version);
2491
+ } catch {
2492
+ this._cachedPreviousPatterns = [];
2493
+ }
2494
+ return this._cachedPreviousPatterns;
2495
+ }
2496
+ migratePatterns(patterns, fromVersion) {
2497
+ let migrated = patterns;
2498
+ if (fromVersion < 1) {
2499
+ migrated = migrated.map((p) => ({
2500
+ ...p,
2501
+ compositeScore: p.compositeScore ?? 0,
2502
+ scoringFactors: p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
2503
+ pipelineStage: p.pipelineStage ?? "node_generation"
2504
+ }));
2505
+ }
2506
+ if (fromVersion < 2) {
2507
+ migrated = migrated.map((p) => {
2508
+ const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
2509
+ return {
2510
+ ...p,
2511
+ scoringFactors: {
2512
+ ...sf,
2513
+ stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
2514
+ }
2515
+ };
2516
+ });
2517
+ }
2518
+ return migrated;
2519
+ }
2520
+ async analyze(days = 30) {
2521
+ const previousPatterns = await this.loadPreviousPatterns();
2522
+ const events = await this.readAllEvents(days);
2523
+ this._cachedEvents = events;
2524
+ const starts = events.filter((e) => e.eventType === "build_start");
2525
+ const attempts = events.filter((e) => e.eventType === "generation_attempt");
2526
+ const passed = attempts.filter(
2527
+ (a) => a.data.validationPassed === true
2528
+ );
2529
+ const failed = attempts.filter(
2530
+ (a) => a.data.validationPassed === false
2531
+ );
2532
+ const ruleFailures = /* @__PURE__ */ new Map();
2533
+ const credentialFailures = /* @__PURE__ */ new Map();
2534
+ for (const a of failed) {
2535
+ const weight = this.recencyWeight(a.fileDate);
2536
+ const buildId = a.runId ?? a.sessionId;
2537
+ const data = a.data;
2538
+ for (const issue of data.issues ?? []) {
2539
+ if (issue.severity === "warn") continue;
2540
+ const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
2541
+ entry.count++;
2542
+ entry.sessions.add(buildId);
2543
+ entry.recencyWeights.push(weight);
2544
+ entry.allMessages.push(issue.message);
2545
+ if (data.workflowType) {
2546
+ entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
2547
+ }
2548
+ ruleFailures.set(issue.rule, entry);
2549
+ if (issue.rule === 17) {
2550
+ const credPatterns = [
2551
+ /credential\s+"([^"]+)"/,
2552
+ /credentialType[:\s]+"?([^"'\s]+)"?/,
2553
+ /missing\s+credential\s+(?:for\s+)?["']?([^"'\s]+)/i
2554
+ ];
2555
+ let credType = "unknown";
2556
+ for (const re of credPatterns) {
2557
+ const m = issue.message.match(re);
2558
+ if (m?.[1]) {
2559
+ credType = m[1];
2560
+ break;
2561
+ }
2562
+ }
2563
+ credentialFailures.set(credType, (credentialFailures.get(credType) ?? 0) + 1);
2564
+ }
2565
+ }
2566
+ }
2567
+ const failedByDate = /* @__PURE__ */ new Map();
2568
+ for (const a of failed) {
2569
+ failedByDate.set(a.fileDate, (failedByDate.get(a.fileDate) ?? 0) + 1);
2570
+ }
2571
+ const sortedFailDates = [...failedByDate.entries()].sort((a, b) => a[0].localeCompare(b[0]));
2572
+ const hasTrendData = sortedFailDates.length >= 3;
2573
+ let midDate = "";
2574
+ if (hasTrendData) {
2575
+ const halfTotal = failed.length / 2;
2576
+ let cumulative = 0;
2577
+ for (const [date, count] of sortedFailDates) {
2578
+ cumulative += count;
2579
+ if (cumulative >= halfTotal) {
2580
+ midDate = date;
2581
+ break;
2582
+ }
2583
+ }
2584
+ }
2585
+ const ruleTrends = /* @__PURE__ */ new Map();
2586
+ if (hasTrendData) {
2587
+ for (const a of failed) {
2588
+ const data = a.data;
2589
+ const isNewer = a.fileDate > midDate;
2590
+ for (const issue of data.issues ?? []) {
2591
+ const entry = ruleTrends.get(issue.rule) ?? { older: 0, newer: 0 };
2592
+ if (isNewer) entry.newer++;
2593
+ else entry.older++;
2594
+ ruleTrends.set(issue.rule, entry);
2595
+ }
2596
+ }
2597
+ }
2598
+ const sessions = /* @__PURE__ */ new Map();
2599
+ for (const a of attempts) {
2600
+ const buildId = a.runId ?? a.sessionId;
2601
+ const list = sessions.get(buildId) ?? [];
2602
+ list.push(a);
2603
+ sessions.set(buildId, list);
2604
+ }
2605
+ let firstTryPass = 0;
2606
+ let correctionNeeded = 0;
2607
+ let singleAttemptFail = 0;
2608
+ for (const sessionAttempts of sessions.values()) {
2609
+ const lastAttempt = sessionAttempts[sessionAttempts.length - 1];
2610
+ const lastPassed = lastAttempt.data.validationPassed === true;
2611
+ if (sessionAttempts.length === 1 && lastPassed) {
2612
+ firstTryPass++;
2613
+ } else if (sessionAttempts.length > 1 && lastPassed) {
2614
+ correctionNeeded++;
2615
+ } else {
2616
+ singleAttemptFail++;
2617
+ }
2618
+ }
2619
+ const durations = attempts.map((a) => a.data.durationMs).filter((d) => typeof d === "number" && d > 0);
2620
+ const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
2621
+ const totalInput = attempts.reduce((s, a) => s + (a.data.tokensInput ?? 0), 0);
2622
+ const totalOutput = attempts.reduce((s, a) => s + (a.data.tokensOutput ?? 0), 0);
2623
+ const totalSessions = Math.max(sessions.size, 1);
2624
+ const stickinessCount = /* @__PURE__ */ new Map();
2625
+ for (const sessionAttempts of sessions.values()) {
2626
+ if (sessionAttempts.length < 2) continue;
2627
+ for (let i = 0; i < sessionAttempts.length - 1; i++) {
2628
+ const curr = sessionAttempts[i].data;
2629
+ const next = sessionAttempts[i + 1].data;
2630
+ if (curr.validationPassed !== false || next.validationPassed !== false) continue;
2631
+ const currRules = new Set((curr.issues ?? []).map((iss) => iss.rule));
2632
+ const nextRules = new Set((next.issues ?? []).map((iss) => iss.rule));
2633
+ for (const rule of currRules) {
2634
+ if (nextRules.has(rule)) {
2635
+ stickinessCount.set(rule, (stickinessCount.get(rule) ?? 0) + 1);
2636
+ }
2637
+ }
2638
+ }
2639
+ }
2640
+ const CONFIRMED_THRESHOLD = 3;
2641
+ const BUILDS_SINCE_LAST_FAILURE_THRESHOLD = 5;
2642
+ const RESOLVED_TTL_DAYS = 90;
2643
+ const activePatterns = [...ruleFailures.entries()].map(([rule, entry]) => {
2644
+ const t = ruleTrends.get(rule) ?? { older: 0, newer: 0 };
2645
+ const rawConfidence = Math.min(entry.sessions.size / totalSessions, 1);
2646
+ const state = entry.count >= CONFIRMED_THRESHOLD ? "confirmed" : "draft";
2647
+ const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
2648
+ const stickiness = stickinessCount.get(rule) ?? 0;
2649
+ const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
2650
+ const pattern = {
2651
+ rule,
2652
+ failureCount: entry.count,
2653
+ confidence: Math.round(rawConfidence * 1e3) / 1e3,
2654
+ compositeScore,
2655
+ scoringFactors: factors,
2656
+ state,
2657
+ trend: this.classifyTrend(t.older, t.newer),
2658
+ pipelineStage: RULE_PIPELINE_STAGES[rule] ?? "node_generation",
2659
+ exampleMessages: this.deduplicateMessages(entry.allMessages),
2660
+ mitigation: RULE_MITIGATIONS[rule] ?? null
2661
+ };
2662
+ if (entry.workflowTypes.size > 0) {
2663
+ pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
2664
+ }
2665
+ return pattern;
2666
+ }).sort((a, b) => b.compositeScore - a.compositeScore);
2667
+ const activeRules = new Set(activePatterns.map((p) => p.rule));
2668
+ for (const p of activePatterns) {
2669
+ const prev = previousPatterns.find((pp) => pp.rule === p.rule);
2670
+ if (prev?.state === "resolved") {
2671
+ p.trend = "worsening";
2672
+ p.regressed = true;
2673
+ }
2674
+ }
2675
+ const ruleLastFailureDate = /* @__PURE__ */ new Map();
2676
+ for (const a of failed) {
2677
+ const data = a.data;
2678
+ for (const issue of data.issues ?? []) {
2679
+ const existing = ruleLastFailureDate.get(issue.rule);
2680
+ if (!existing || a.fileDate > existing) {
2681
+ ruleLastFailureDate.set(issue.rule, a.fileDate);
2682
+ }
2683
+ }
2684
+ }
2685
+ const newlyResolved = previousPatterns.filter((p) => {
2686
+ if (p.state !== "confirmed" || activeRules.has(p.rule)) return false;
2687
+ const lastFailDate = ruleLastFailureDate.get(p.rule) ?? "";
2688
+ const buildsSince = starts.filter((s) => s.fileDate > lastFailDate).length;
2689
+ return buildsSince >= BUILDS_SINCE_LAST_FAILURE_THRESHOLD;
2690
+ }).map((p) => ({
2691
+ ...p,
2692
+ state: "resolved",
2693
+ trend: "improving",
2694
+ pipelineStage: p.pipelineStage ?? RULE_PIPELINE_STAGES[p.rule] ?? "node_generation",
2695
+ confidence: 0,
2696
+ compositeScore: 0,
2697
+ scoringFactors: { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
2698
+ failureCount: 0,
2699
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
2700
+ }));
2701
+ const ttlCutoff = /* @__PURE__ */ new Date();
2702
+ ttlCutoff.setDate(ttlCutoff.getDate() - RESOLVED_TTL_DAYS);
2703
+ const ttlCutoffStr = ttlCutoff.toISOString();
2704
+ const carriedResolved = previousPatterns.filter((p) => p.state === "resolved" && !activeRules.has(p.rule) && (!p.resolvedAt || p.resolvedAt >= ttlCutoffStr)).map((p) => ({ ...p }));
2705
+ const newlyResolvedRules = new Set(newlyResolved.map((p) => p.rule));
2706
+ const pendingResolution = previousPatterns.filter((p) => p.state === "confirmed" && !activeRules.has(p.rule) && !newlyResolvedRules.has(p.rule)).map((p) => ({ ...p }));
2707
+ const deduped = [
2708
+ ...newlyResolved,
2709
+ ...carriedResolved.filter((p) => !newlyResolvedRules.has(p.rule)),
2710
+ ...pendingResolution
2711
+ ];
2712
+ const patterns = [...activePatterns, ...deduped];
2713
+ const credTypes = [...credentialFailures.entries()].sort((a, b) => b[1] - a[1]).map(([type, count]) => ({ type, count }));
2714
+ const drift = this.detectDrift(patterns);
2715
+ const warnEffMap = /* @__PURE__ */ new Map();
2716
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
2717
+ for (const bc of buildCompletes) {
2718
+ const bcData = bc.data;
2719
+ const warned = bcData.warnedRules ?? [];
2720
+ if (warned.length === 0) continue;
2721
+ const sessionFailedRules = /* @__PURE__ */ new Set();
2722
+ const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
2723
+ for (const a of sessionAttempts) {
2724
+ const ad = a.data;
2725
+ if (ad.validationPassed === false) {
2726
+ for (const issue of ad.issues ?? []) {
2727
+ sessionFailedRules.add(issue.rule);
2728
+ }
2729
+ }
2730
+ }
2731
+ for (const rule of warned) {
2732
+ const entry = warnEffMap.get(rule) ?? { warned: 0, passed: 0, failed: 0 };
2733
+ entry.warned++;
2734
+ if (sessionFailedRules.has(rule)) entry.failed++;
2735
+ else entry.passed++;
2736
+ warnEffMap.set(rule, entry);
2737
+ }
2738
+ }
2739
+ const warningEffectiveness = [...warnEffMap.entries()].map(([rule, e]) => ({
2740
+ rule,
2741
+ timesWarned: e.warned,
2742
+ timesWarnedAndPassed: e.passed,
2743
+ timesWarnedAndFailed: e.failed,
2744
+ effectivenessRate: e.warned > 0 ? Math.round(e.passed / e.warned * 1e3) / 1e3 : 0
2745
+ })).sort((a, b) => b.timesWarned - a.timesWarned);
2746
+ const coOccurrenceMap = /* @__PURE__ */ new Map();
2747
+ for (const a of failed) {
2748
+ const data = a.data;
2749
+ const rules = [...new Set((data.issues ?? []).map((i) => i.rule))].sort((x, y) => x - y);
2750
+ for (let i = 0; i < rules.length; i++) {
2751
+ for (let j = i + 1; j < rules.length; j++) {
2752
+ const key = `${rules[i]},${rules[j]}`;
2753
+ coOccurrenceMap.set(key, (coOccurrenceMap.get(key) ?? 0) + 1);
2754
+ }
2755
+ }
2756
+ }
2757
+ const ruleCoOccurrence = [...coOccurrenceMap.entries()].filter(([, count]) => count >= 3).map(([key, count]) => {
2758
+ const [a, b] = key.split(",").map(Number);
2759
+ return { rules: [a, b], count };
2760
+ }).sort((a, b) => b.count - a.count);
2761
+ const attemptDistribution = {};
2762
+ for (const sessionAttempts of sessions.values()) {
2763
+ const depth = sessionAttempts.length;
2764
+ attemptDistribution[depth] = (attemptDistribution[depth] ?? 0) + 1;
2765
+ }
2766
+ return {
2767
+ schemaVersion: PATTERN_SCHEMA_VERSION,
2768
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2769
+ summary: {
2770
+ totalBuilds: starts.length,
2771
+ totalAttempts: attempts.length,
2772
+ firstTryPassRate: Math.round(firstTryPass / totalSessions * 1e3) / 1e3,
2773
+ correctionRate: Math.round(correctionNeeded / totalSessions * 1e3) / 1e3,
2774
+ singleAttemptFailRate: Math.round(singleAttemptFail / totalSessions * 1e3) / 1e3,
2775
+ avgDurationMs: Math.round(avgDuration),
2776
+ totalTokensInput: totalInput,
2777
+ totalTokensOutput: totalOutput,
2778
+ attemptDistribution
2779
+ },
2780
+ topFailureRules: patterns,
2781
+ failingCredentialTypes: credTypes,
2782
+ drift,
2783
+ warningEffectiveness,
2784
+ ruleCoOccurrence
2785
+ };
2786
+ }
2787
+ async analyzeAndSave(days = 30) {
2788
+ const analysis = await this.analyze(days);
2789
+ await (0, import_promises4.mkdir)(this.outputDir, { recursive: true });
2790
+ const outputPath = (0, import_node_path5.join)(this.outputDir, "patterns.json");
2791
+ const tmpPath = `${outputPath}.tmp`;
2792
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
2793
+ await (0, import_promises4.rename)(tmpPath, outputPath);
2794
+ this._cachedPreviousPatterns = null;
2795
+ const historySummary = {
2796
+ timestamp: analysis.generatedAt,
2797
+ totalBuilds: analysis.summary.totalBuilds,
2798
+ firstTryPassRate: analysis.summary.firstTryPassRate,
2799
+ correctionRate: analysis.summary.correctionRate,
2800
+ singleAttemptFailRate: analysis.summary.singleAttemptFailRate,
2801
+ activePatternCount: analysis.topFailureRules.filter((p) => p.state !== "resolved").length,
2802
+ topRules: analysis.topFailureRules.filter((p) => p.state !== "resolved").slice(0, 5).map((p) => ({ rule: p.rule, compositeScore: p.compositeScore, state: p.state }))
2803
+ };
2804
+ const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
2805
+ await (0, import_promises4.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
2806
+ const sessions = await this.buildSessionSummaries(days);
2807
+ const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
2808
+ const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
2809
+ await (0, import_promises4.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
2810
+ await (0, import_promises4.rename)(sessionHistoryTmp, sessionHistoryPath);
2811
+ return analysis;
2812
+ }
2813
+ async getSessions(limit = 20) {
2814
+ try {
2815
+ const raw = await (0, import_promises4.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
2816
+ const all = JSON.parse(raw);
2817
+ return all.slice(-limit);
2818
+ } catch {
2819
+ return [];
2820
+ }
2821
+ }
2822
+ async buildSessionSummaries(days = 30) {
2823
+ const events = this._cachedEvents ?? await this.readAllEvents(days);
2824
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
2825
+ const attemptsByBuild = /* @__PURE__ */ new Map();
2826
+ for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
2827
+ const buildId = e.runId ?? e.sessionId;
2828
+ const list = attemptsByBuild.get(buildId) ?? [];
2829
+ list.push(e);
2830
+ attemptsByBuild.set(buildId, list);
2831
+ }
2832
+ const summaries = buildCompletes.map((bc) => {
2833
+ const data = bc.data;
2834
+ const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
2835
+ const failedRules = Array.from(new Set(
2836
+ sessionAttempts.flatMap((a) => {
2837
+ const ad = a.data;
2838
+ if (ad.validationPassed !== false) return [];
2839
+ return (ad.issues ?? []).map((i) => i.rule);
2840
+ })
2841
+ ));
2842
+ return {
2843
+ sessionId: bc.runId ?? bc.sessionId,
2844
+ date: bc.fileDate,
2845
+ description: data.description ?? "",
2846
+ workflowType: data.workflowType ?? null,
2847
+ attempts: data.totalAttempts ?? 1,
2848
+ success: data.success ?? false,
2849
+ failedRules,
2850
+ workflowName: data.workflowName ?? null
2851
+ };
2852
+ });
2853
+ return summaries.sort((a, b) => a.date.localeCompare(b.date));
2854
+ }
2855
+ async getHistory(limit = 20) {
2856
+ try {
2857
+ const raw = await (0, import_promises4.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
2858
+ return raw.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)).slice(-limit);
2859
+ } catch {
2860
+ return [];
2861
+ }
2862
+ }
2863
+ static fromEnv() {
2864
+ const dir = process.env["KAIROS_TELEMETRY"];
2865
+ return dir && dir !== "true" && dir !== "false" ? new _PatternAnalyzer(dir) : new _PatternAnalyzer();
2866
+ }
2867
+ detectDrift(patterns) {
2868
+ const VALIDATOR_RULES = VALIDATOR_RULE_IDS;
2869
+ const validatorRuleSet = new Set(VALIDATOR_RULES);
2870
+ const alerts = [];
2871
+ for (const p of patterns) {
2872
+ if (p.state !== "resolved" && !validatorRuleSet.has(p.rule)) {
2873
+ alerts.push({
2874
+ type: "stale_pattern",
2875
+ rule: p.rule,
2876
+ message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-34)`
2877
+ });
2878
+ }
2879
+ }
2880
+ for (const rule of VALIDATOR_RULES) {
2881
+ if (!(rule in RULE_MITIGATIONS)) {
2882
+ alerts.push({
2883
+ type: "missing_mitigation",
2884
+ rule,
2885
+ message: `Rule ${rule} has no mitigation text \u2014 if it fails, the system can't advise the LLM how to fix it`
2886
+ });
2887
+ }
2888
+ if (!(rule in RULE_PIPELINE_STAGES)) {
2889
+ alerts.push({
2890
+ type: "missing_stage_mapping",
2891
+ rule,
2892
+ message: `Rule ${rule} has no pipeline stage mapping \u2014 failures won't be grouped correctly`
2893
+ });
2894
+ }
2895
+ }
2896
+ const coveredRules = VALIDATOR_RULES.filter((r) => r in RULE_MITIGATIONS && r in RULE_PIPELINE_STAGES).length;
2897
+ return {
2898
+ healthy: alerts.length === 0,
2899
+ alerts,
2900
+ coveredRules,
2901
+ totalRules: VALIDATOR_RULES.length
2902
+ };
2903
+ }
2904
+ computeCompositeScore(rawConfidence, sampleSize, state, avgRecency, stickiness) {
2905
+ const stateWeights = { draft: 0.3, confirmed: 0.8, resolved: 0.1 };
2906
+ const stateWeight = stateWeights[state];
2907
+ const impact = (1 - Math.exp(-sampleSize / 5)) * stateWeight;
2908
+ const stickinessBoost = Math.min(0.15, stickiness * 0.05);
2909
+ const compositeScore = Math.min(Math.round(rawConfidence * impact * avgRecency * (1 + stickinessBoost) * 1e3) / 1e3, 1);
2910
+ return {
2911
+ compositeScore,
2912
+ factors: {
2913
+ rawConfidence: Math.round(rawConfidence * 1e3) / 1e3,
2914
+ impact: Math.round(impact * 1e3) / 1e3,
2915
+ recency: Math.round(avgRecency * 1e3) / 1e3,
2916
+ stickinessBoost: Math.round(stickinessBoost * 1e3) / 1e3
2917
+ }
2918
+ };
2919
+ }
2920
+ classifyTrend(older, newer) {
2921
+ const total = older + newer;
2922
+ if (total === 0) return "stable";
2923
+ if (older === 0) return "new";
2924
+ const newerRatio = newer / total;
2925
+ if (newerRatio >= 0.65) return "worsening";
2926
+ if (newerRatio <= 0.35) return "improving";
2927
+ return "stable";
2928
+ }
2929
+ deduplicateMessages(messages, maxCount = 3) {
2930
+ const normalize = (msg) => msg.replace(/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/gi, "...").replace(/\bnode\s+"[^"]+"/g, 'node "..."').replace(/\s+/g, " ").trim();
2931
+ const seen = /* @__PURE__ */ new Set();
2932
+ const unique = [];
2933
+ for (const msg of messages) {
2934
+ const key = normalize(msg);
2935
+ if (!seen.has(key) && unique.length < maxCount) {
2936
+ seen.add(key);
2937
+ unique.push(msg);
2938
+ }
2939
+ }
2940
+ return unique;
2941
+ }
2942
+ recencyWeight(fileDate, halfLifeDays = 30) {
2943
+ const daysAgo = Math.max(0, (Date.now() - (/* @__PURE__ */ new Date(fileDate + "T12:00:00Z")).getTime()) / (1e3 * 60 * 60 * 24));
2944
+ return Math.max(0.1, Math.exp(-Math.LN2 * daysAgo / halfLifeDays));
2945
+ }
2946
+ async readAllEvents(days) {
2947
+ return readTelemetryEvents(this.telemetryDir, days);
2948
+ }
2949
+ };
2950
+ // Annotate the CommonJS export names for ESM import in node:
2951
+ 0 && (module.exports = {
2952
+ ApiError,
2953
+ DEFAULT_REGISTRY,
2954
+ FileLibrary,
2955
+ GenerationError,
2956
+ GuardError,
2957
+ KairosError,
2958
+ N8nApiClient,
2959
+ N8nFieldStripper,
2960
+ N8nProvider,
2961
+ N8nValidator,
2962
+ NodeRegistry,
2963
+ NullLibrary,
2964
+ PatternAnalyzer,
2965
+ ProviderError,
2966
+ ResponseParseError,
2967
+ TelemetryCollector,
2968
+ TelemetryReader,
2969
+ TemplateSyncer,
2970
+ ValidationError,
2971
+ buildSearchCorpus,
2972
+ clusterWorkflows,
2973
+ hybridScore,
2974
+ nullLogger,
2975
+ rerank,
2976
+ tokenize
2977
+ });
2978
+ //# sourceMappingURL=standalone.cjs.map