@kairos-sdk/core 0.3.2 → 0.4.5

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