@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.
- package/README.md +46 -11
- package/dist/{chunk-KQSNT3HZ.js → chunk-4TS6GW6O.js} +148 -368
- package/dist/chunk-4TS6GW6O.js.map +1 -0
- package/dist/chunk-6CLI43FI.js +315 -0
- package/dist/chunk-6CLI43FI.js.map +1 -0
- package/dist/chunk-6FOFWVMG.js +1 -0
- package/dist/chunk-6FOFWVMG.js.map +1 -0
- package/dist/{chunk-RYGYNOR6.js → chunk-6IXW3WCC.js} +936 -412
- package/dist/chunk-6IXW3WCC.js.map +1 -0
- package/dist/chunk-CR2NHLOH.js +523 -0
- package/dist/chunk-CR2NHLOH.js.map +1 -0
- package/dist/cli.cjs +1402 -170
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +140 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1262 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -537
- package/dist/index.d.ts +5 -537
- package/dist/index.js +8 -4
- package/dist/mcp-server.cjs +1259 -129
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +113 -8
- package/dist/mcp-server.js.map +1 -1
- package/dist/reader-CpUcHhKW.d.cts +566 -0
- package/dist/reader-CpUcHhKW.d.ts +566 -0
- package/dist/standalone.cjs +2460 -0
- package/dist/standalone.cjs.map +1 -0
- package/dist/standalone.d.cts +105 -0
- package/dist/standalone.d.ts +105 -0
- package/dist/standalone.js +58 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +6 -1
- package/dist/chunk-KQSNT3HZ.js.map +0 -1
- package/dist/chunk-RYGYNOR6.js.map +0 -1
package/dist/mcp-server.cjs
CHANGED
|
@@ -253,14 +253,28 @@ function buildSearchCorpus(w) {
|
|
|
253
253
|
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
254
254
|
}
|
|
255
255
|
var MAX_LIBRARY_SIZE = 500;
|
|
256
|
+
function isValidMeta(item) {
|
|
257
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
258
|
+
}
|
|
259
|
+
function isValidOldEntry(item) {
|
|
260
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
|
|
261
|
+
item.workflow.nodes
|
|
262
|
+
);
|
|
263
|
+
}
|
|
256
264
|
var FileLibrary = class {
|
|
257
265
|
dir;
|
|
258
|
-
|
|
266
|
+
meta = [];
|
|
259
267
|
initPromise = null;
|
|
260
268
|
writeQueue = Promise.resolve();
|
|
261
269
|
constructor(dir) {
|
|
262
270
|
this.dir = dir ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "library");
|
|
263
271
|
}
|
|
272
|
+
get workflowsDir() {
|
|
273
|
+
return (0, import_node_path.join)(this.dir, "workflows");
|
|
274
|
+
}
|
|
275
|
+
workflowFilePath(id) {
|
|
276
|
+
return (0, import_node_path.join)(this.workflowsDir, `${id}.json`);
|
|
277
|
+
}
|
|
264
278
|
async initialize() {
|
|
265
279
|
if (!this.initPromise) {
|
|
266
280
|
this.initPromise = this.doInitialize();
|
|
@@ -270,60 +284,147 @@ var FileLibrary = class {
|
|
|
270
284
|
async doInitialize() {
|
|
271
285
|
await (0, import_promises.mkdir)(this.dir, { recursive: true });
|
|
272
286
|
const indexPath = (0, import_node_path.join)(this.dir, "index.json");
|
|
287
|
+
let workflowsDirExists = false;
|
|
273
288
|
try {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
);
|
|
289
|
+
await (0, import_promises.stat)(this.workflowsDir);
|
|
290
|
+
workflowsDirExists = true;
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
if (workflowsDirExists) {
|
|
294
|
+
try {
|
|
295
|
+
const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
|
|
296
|
+
const parsed = JSON.parse(raw);
|
|
297
|
+
if (Array.isArray(parsed)) {
|
|
298
|
+
this.meta = parsed.filter(isValidMeta);
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
this.meta = [];
|
|
282
302
|
}
|
|
303
|
+
} else {
|
|
304
|
+
try {
|
|
305
|
+
const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
|
|
306
|
+
const parsed = JSON.parse(raw);
|
|
307
|
+
if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
|
|
308
|
+
await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
this.meta = [];
|
|
314
|
+
await (0, import_promises.mkdir)(this.workflowsDir, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
319
|
+
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
320
|
+
* meta entry. Rewrites index.json in the new format.
|
|
321
|
+
*/
|
|
322
|
+
async migrateFromMonolithic(oldEntries) {
|
|
323
|
+
await (0, import_promises.mkdir)(this.workflowsDir, { recursive: true });
|
|
324
|
+
const newMeta = [];
|
|
325
|
+
for (const entry of oldEntries) {
|
|
326
|
+
const wfPath = this.workflowFilePath(entry.id);
|
|
327
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
328
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
|
|
329
|
+
await (0, import_promises.rename)(tmpPath, wfPath);
|
|
330
|
+
const { workflow, ...metaFields } = entry;
|
|
331
|
+
newMeta.push({
|
|
332
|
+
...metaFields,
|
|
333
|
+
workflowName: workflow.name,
|
|
334
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type)
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
this.meta = newMeta;
|
|
338
|
+
await this.persistNow();
|
|
339
|
+
}
|
|
340
|
+
async loadWorkflowFile(id) {
|
|
341
|
+
try {
|
|
342
|
+
const raw = await (0, import_promises.readFile)(this.workflowFilePath(id), "utf-8");
|
|
343
|
+
return JSON.parse(raw);
|
|
283
344
|
} catch {
|
|
284
|
-
|
|
345
|
+
return null;
|
|
285
346
|
}
|
|
286
347
|
}
|
|
348
|
+
async writeWorkflowFile(id, workflow) {
|
|
349
|
+
const wfPath = this.workflowFilePath(id);
|
|
350
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
351
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
|
|
352
|
+
await (0, import_promises.rename)(tmpPath, wfPath);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Build a lightweight StoredWorkflow shell from a meta entry for use in
|
|
356
|
+
* scoring / clustering. Only node.type is populated in each node — no other
|
|
357
|
+
* node fields are used by hybridScore or clusterWorkflows.
|
|
358
|
+
*/
|
|
359
|
+
makeSearchShell(m) {
|
|
360
|
+
return {
|
|
361
|
+
...m,
|
|
362
|
+
workflow: {
|
|
363
|
+
name: m.workflowName,
|
|
364
|
+
nodes: m.cachedNodeTypes.map((type) => ({
|
|
365
|
+
id: "",
|
|
366
|
+
name: "",
|
|
367
|
+
type,
|
|
368
|
+
typeVersion: 1,
|
|
369
|
+
position: [0, 0],
|
|
370
|
+
parameters: {}
|
|
371
|
+
})),
|
|
372
|
+
connections: {}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
287
376
|
async search(description, options) {
|
|
288
|
-
const
|
|
289
|
-
if (
|
|
377
|
+
const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
|
|
378
|
+
if (filteredMeta.length === 0) return [];
|
|
290
379
|
const limit = options?.limit ?? 3;
|
|
291
380
|
const queryTokens = tokenize(description);
|
|
292
381
|
if (queryTokens.length === 0) return [];
|
|
293
|
-
const
|
|
382
|
+
const shells = filteredMeta.map((m) => this.makeSearchShell(m));
|
|
383
|
+
const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
|
|
294
384
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
295
|
-
const docCount =
|
|
385
|
+
const docCount = shells.length;
|
|
296
386
|
const idf = /* @__PURE__ */ new Map();
|
|
297
387
|
const allTokens = new Set(queryTokens);
|
|
298
388
|
for (const token of allTokens) {
|
|
299
389
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
300
390
|
idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
|
|
301
391
|
}
|
|
302
|
-
const scored = hybridScore(queryTokens, description,
|
|
303
|
-
const clusters = clusterWorkflows(
|
|
392
|
+
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
393
|
+
const clusters = clusterWorkflows(shells);
|
|
304
394
|
const reranked = rerank(scored, clusters).slice(0, limit);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
395
|
+
if (reranked.length === 0) return [];
|
|
396
|
+
for (const r of reranked) {
|
|
397
|
+
const m = this.meta.find((m2) => m2.id === r.workflow.id);
|
|
398
|
+
if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
|
|
399
|
+
}
|
|
400
|
+
this.persist();
|
|
401
|
+
const results = await Promise.all(
|
|
402
|
+
reranked.map(async (r) => {
|
|
403
|
+
const m = this.meta.find((meta) => meta.id === r.workflow.id);
|
|
404
|
+
const workflow = await this.loadWorkflowFile(r.workflow.id);
|
|
405
|
+
if (!workflow) return null;
|
|
406
|
+
return {
|
|
407
|
+
workflow: { ...m, workflow },
|
|
408
|
+
score: r.score,
|
|
409
|
+
mode: scoreToMode(r.score)
|
|
410
|
+
};
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
return results.filter((r) => r !== null);
|
|
315
414
|
}
|
|
316
415
|
async save(workflow, metadata) {
|
|
317
416
|
const id = generateUUID();
|
|
417
|
+
await this.writeWorkflowFile(id, workflow);
|
|
318
418
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
319
|
-
const
|
|
419
|
+
const meta = {
|
|
320
420
|
id,
|
|
321
|
-
workflow,
|
|
322
421
|
description: metadata.description,
|
|
323
422
|
tags: metadata.tags ?? [],
|
|
324
423
|
platform: metadata.platform ?? "n8n",
|
|
325
424
|
deployCount: 0,
|
|
326
425
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
426
|
+
workflowName: workflow.name,
|
|
427
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type),
|
|
327
428
|
...failurePatterns?.length ? { failurePatterns } : {},
|
|
328
429
|
...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
|
|
329
430
|
...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
|
|
@@ -335,31 +436,35 @@ var FileLibrary = class {
|
|
|
335
436
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
336
437
|
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
337
438
|
};
|
|
338
|
-
this.
|
|
339
|
-
if (this.
|
|
340
|
-
this.
|
|
341
|
-
|
|
439
|
+
this.meta.push(meta);
|
|
440
|
+
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
441
|
+
this.meta.sort((a, b) => {
|
|
442
|
+
if (a.id === id) return -1;
|
|
443
|
+
if (b.id === id) return 1;
|
|
444
|
+
return (b.deployCount ?? 0) - (a.deployCount ?? 0);
|
|
445
|
+
});
|
|
446
|
+
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
342
447
|
}
|
|
343
448
|
await this.persist();
|
|
344
449
|
return id;
|
|
345
450
|
}
|
|
346
451
|
async recordDeployment(id) {
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
452
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
453
|
+
if (m) {
|
|
454
|
+
m.deployCount++;
|
|
455
|
+
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
351
456
|
await this.persist();
|
|
352
457
|
}
|
|
353
458
|
}
|
|
354
459
|
async recordOutcome(id, outcome) {
|
|
355
|
-
const
|
|
356
|
-
if (!
|
|
460
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
461
|
+
if (!m) return;
|
|
357
462
|
if (outcome.mode === "direct") {
|
|
358
|
-
|
|
463
|
+
m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
|
|
359
464
|
} else {
|
|
360
|
-
|
|
465
|
+
m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
|
|
361
466
|
}
|
|
362
|
-
const stats =
|
|
467
|
+
const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
|
|
363
468
|
stats.totalUses++;
|
|
364
469
|
stats.totalAttempts += outcome.attempts;
|
|
365
470
|
if (outcome.firstTryPass) stats.firstTryPasses++;
|
|
@@ -367,24 +472,35 @@ var FileLibrary = class {
|
|
|
367
472
|
const key = String(rule);
|
|
368
473
|
stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
|
|
369
474
|
}
|
|
370
|
-
|
|
475
|
+
m.outcomeStats = stats;
|
|
371
476
|
await this.persist();
|
|
372
477
|
}
|
|
373
478
|
async drain() {
|
|
374
479
|
await this.writeQueue;
|
|
375
480
|
}
|
|
376
481
|
async get(id) {
|
|
377
|
-
|
|
482
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
483
|
+
if (!m) return null;
|
|
484
|
+
const workflow = await this.loadWorkflowFile(id);
|
|
485
|
+
if (!workflow) return null;
|
|
486
|
+
return { ...m, workflow };
|
|
378
487
|
}
|
|
379
488
|
async list(filters) {
|
|
380
|
-
let
|
|
489
|
+
let filtered = this.meta;
|
|
381
490
|
if (filters?.platform) {
|
|
382
|
-
|
|
491
|
+
filtered = filtered.filter((m) => m.platform === filters.platform);
|
|
383
492
|
}
|
|
384
493
|
if (filters?.tags && filters.tags.length > 0) {
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
494
|
+
filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
|
|
495
|
+
}
|
|
496
|
+
const results = await Promise.all(
|
|
497
|
+
filtered.map(async (m) => {
|
|
498
|
+
const workflow = await this.loadWorkflowFile(m.id);
|
|
499
|
+
if (!workflow) return null;
|
|
500
|
+
return { ...m, workflow };
|
|
501
|
+
})
|
|
502
|
+
);
|
|
503
|
+
return results.filter((r) => r !== null);
|
|
388
504
|
}
|
|
389
505
|
deduplicateFailurePatterns(patterns) {
|
|
390
506
|
if (!patterns?.length) return void 0;
|
|
@@ -399,11 +515,36 @@ var FileLibrary = class {
|
|
|
399
515
|
}
|
|
400
516
|
return [...map.values()];
|
|
401
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Direct write used only during migration (before writeQueue is needed).
|
|
520
|
+
*/
|
|
521
|
+
async persistNow() {
|
|
522
|
+
const indexPath = (0, import_node_path.join)(this.dir, "index.json");
|
|
523
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
524
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
525
|
+
await (0, import_promises.rename)(tmpPath, indexPath);
|
|
526
|
+
}
|
|
402
527
|
persist() {
|
|
403
528
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
404
529
|
const indexPath = (0, import_node_path.join)(this.dir, "index.json");
|
|
530
|
+
let onDisk = [];
|
|
531
|
+
try {
|
|
532
|
+
const raw = await (0, import_promises.readFile)(indexPath, "utf-8");
|
|
533
|
+
const parsed = JSON.parse(raw);
|
|
534
|
+
if (Array.isArray(parsed)) {
|
|
535
|
+
onDisk = parsed.filter(isValidMeta);
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
const ourIds = new Set(this.meta.map((m) => m.id));
|
|
540
|
+
const external = onDisk.filter((m) => !ourIds.has(m.id));
|
|
541
|
+
let merged = [...this.meta, ...external];
|
|
542
|
+
if (merged.length > MAX_LIBRARY_SIZE) {
|
|
543
|
+
merged.sort((a, b) => (b.deployCount ?? 0) - (a.deployCount ?? 0));
|
|
544
|
+
merged = merged.slice(0, MAX_LIBRARY_SIZE);
|
|
545
|
+
}
|
|
405
546
|
const tmpPath = `${indexPath}.tmp`;
|
|
406
|
-
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(
|
|
547
|
+
await (0, import_promises.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
407
548
|
await (0, import_promises.rename)(tmpPath, indexPath);
|
|
408
549
|
});
|
|
409
550
|
return this.writeQueue;
|
|
@@ -577,17 +718,31 @@ var N8nValidator = class {
|
|
|
577
718
|
this.checkRule21(workflow, issues);
|
|
578
719
|
this.checkRule22(workflow, issues);
|
|
579
720
|
this.checkRule23(workflow, issues);
|
|
721
|
+
this.checkRule24(workflow, issues);
|
|
722
|
+
this.checkRule25(workflow, issues);
|
|
723
|
+
this.checkRule26(workflow, issues);
|
|
724
|
+
if (Array.isArray(workflow.nodes)) {
|
|
725
|
+
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
726
|
+
for (const issue of issues) {
|
|
727
|
+
if (issue.nodeId && !issue.nodeType) {
|
|
728
|
+
const nt = nodeById.get(issue.nodeId);
|
|
729
|
+
if (nt) issue.nodeType = nt;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
580
733
|
const errors = issues.filter((i) => i.severity === "error");
|
|
581
734
|
return { valid: errors.length === 0, issues };
|
|
582
735
|
}
|
|
583
|
-
err(issues, rule, message, nodeId) {
|
|
736
|
+
err(issues, rule, message, nodeId, nodeType) {
|
|
584
737
|
const issue = { rule, severity: "error", message };
|
|
585
738
|
if (nodeId !== void 0) issue.nodeId = nodeId;
|
|
739
|
+
if (nodeType !== void 0) issue.nodeType = nodeType;
|
|
586
740
|
issues.push(issue);
|
|
587
741
|
}
|
|
588
|
-
warn(issues, rule, message, nodeId) {
|
|
742
|
+
warn(issues, rule, message, nodeId, nodeType) {
|
|
589
743
|
const issue = { rule, severity: "warn", message };
|
|
590
744
|
if (nodeId !== void 0) issue.nodeId = nodeId;
|
|
745
|
+
if (nodeType !== void 0) issue.nodeType = nodeType;
|
|
591
746
|
issues.push(issue);
|
|
592
747
|
}
|
|
593
748
|
isTriggerNode(node) {
|
|
@@ -698,10 +853,14 @@ var N8nValidator = class {
|
|
|
698
853
|
checkRule11(w, issues) {
|
|
699
854
|
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
700
855
|
const reachable = /* @__PURE__ */ new Set();
|
|
701
|
-
|
|
856
|
+
const aiSubNodeSources = /* @__PURE__ */ new Set();
|
|
857
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
702
858
|
if (typeof outputs !== "object" || outputs === null) continue;
|
|
703
|
-
|
|
859
|
+
let hasAiPort = false;
|
|
860
|
+
for (const [portName, portGroup] of Object.entries(outputs)) {
|
|
704
861
|
if (!Array.isArray(portGroup)) continue;
|
|
862
|
+
const isAiPort = portName.startsWith("ai_");
|
|
863
|
+
if (isAiPort) hasAiPort = true;
|
|
705
864
|
for (const targets of portGroup) {
|
|
706
865
|
if (!Array.isArray(targets)) continue;
|
|
707
866
|
for (const target of targets) {
|
|
@@ -710,10 +869,13 @@ var N8nValidator = class {
|
|
|
710
869
|
}
|
|
711
870
|
}
|
|
712
871
|
}
|
|
872
|
+
if (hasAiPort) aiSubNodeSources.add(sourceName);
|
|
713
873
|
}
|
|
714
874
|
for (const node of w.nodes) {
|
|
715
875
|
if (node.type.includes("stickyNote")) continue;
|
|
716
|
-
if (
|
|
876
|
+
if (this.isTriggerNode(node)) continue;
|
|
877
|
+
if (aiSubNodeSources.has(node.name)) continue;
|
|
878
|
+
if (!reachable.has(node.name)) {
|
|
717
879
|
this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
|
|
718
880
|
}
|
|
719
881
|
}
|
|
@@ -909,6 +1071,76 @@ var N8nValidator = class {
|
|
|
909
1071
|
}
|
|
910
1072
|
}
|
|
911
1073
|
}
|
|
1074
|
+
// Rule 24 (WARN): deprecated accessor syntax in expressions
|
|
1075
|
+
checkRule24(w, issues) {
|
|
1076
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1077
|
+
const deprecated = /\$node\s*\[/;
|
|
1078
|
+
for (const node of w.nodes) {
|
|
1079
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1080
|
+
if (deprecated.test(expr)) {
|
|
1081
|
+
this.warn(
|
|
1082
|
+
issues,
|
|
1083
|
+
24,
|
|
1084
|
+
`Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
|
|
1085
|
+
node.id
|
|
1086
|
+
);
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
// Rule 25 (WARN): wrong item index assumptions in expressions
|
|
1093
|
+
checkRule25(w, issues) {
|
|
1094
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1095
|
+
const itemIndex = /\$json\s*\.\s*items\s*\[/;
|
|
1096
|
+
for (const node of w.nodes) {
|
|
1097
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1098
|
+
if (itemIndex.test(expr)) {
|
|
1099
|
+
this.warn(
|
|
1100
|
+
issues,
|
|
1101
|
+
25,
|
|
1102
|
+
`Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
|
|
1103
|
+
node.id
|
|
1104
|
+
);
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
// Rule 26 (WARN): missing .first() or .all() on node references
|
|
1111
|
+
checkRule26(w, issues) {
|
|
1112
|
+
if (!Array.isArray(w.nodes)) return;
|
|
1113
|
+
const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
|
|
1114
|
+
for (const node of w.nodes) {
|
|
1115
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
1116
|
+
if (bareRef.test(expr)) {
|
|
1117
|
+
this.warn(
|
|
1118
|
+
issues,
|
|
1119
|
+
26,
|
|
1120
|
+
`Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
|
|
1121
|
+
node.id
|
|
1122
|
+
);
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
extractExpressions(params) {
|
|
1129
|
+
const expressions = [];
|
|
1130
|
+
const walk = (val) => {
|
|
1131
|
+
if (typeof val === "string") {
|
|
1132
|
+
if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
|
|
1133
|
+
expressions.push(val);
|
|
1134
|
+
}
|
|
1135
|
+
} else if (Array.isArray(val)) {
|
|
1136
|
+
for (const item of val) walk(item);
|
|
1137
|
+
} else if (val !== null && typeof val === "object") {
|
|
1138
|
+
for (const v of Object.values(val)) walk(v);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
walk(params);
|
|
1142
|
+
return expressions;
|
|
1143
|
+
}
|
|
912
1144
|
// Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
|
|
913
1145
|
checkRule21(w, issues) {
|
|
914
1146
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -1164,6 +1396,11 @@ var N8nApiClient = class {
|
|
|
1164
1396
|
}
|
|
1165
1397
|
};
|
|
1166
1398
|
|
|
1399
|
+
// src/generation/prompt-builder.ts
|
|
1400
|
+
var import_node_fs = require("fs");
|
|
1401
|
+
var import_node_path2 = require("path");
|
|
1402
|
+
var import_node_os2 = require("os");
|
|
1403
|
+
|
|
1167
1404
|
// src/generation/prompts/v1.ts
|
|
1168
1405
|
var SYSTEM_PROMPT_V1 = `You are a workflow generation engine for n8n. Your only output is a generate_workflow tool call containing valid n8n workflow JSON. You never respond with prose, explanations, or markdown. If you cannot fulfill the request, set the error field in the tool call.
|
|
1169
1406
|
|
|
@@ -1193,9 +1430,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
|
|
|
1193
1430
|
- Never reuse IDs, never use sequential fake IDs like "node-1"
|
|
1194
1431
|
|
|
1195
1432
|
### Credentials:
|
|
1196
|
-
-
|
|
1197
|
-
|
|
1198
|
-
-
|
|
1433
|
+
- Each credential is keyed by its type string, with an object value containing id and name:
|
|
1434
|
+
"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack Credential" } }
|
|
1435
|
+
- Use "placeholder-id" as the id \u2014 users replace this with their real credential ID from n8n after deployment
|
|
1436
|
+
- The credentialsNeeded field in your response declares what credentials the user must configure
|
|
1437
|
+
- Never put API keys or tokens directly in node parameters when a credential type exists
|
|
1199
1438
|
|
|
1200
1439
|
### Node names:
|
|
1201
1440
|
- All node names must be unique within the workflow
|
|
@@ -1242,6 +1481,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
|
|
|
1242
1481
|
|
|
1243
1482
|
---
|
|
1244
1483
|
|
|
1484
|
+
## EXPRESSION SYNTAX \u2014 how to reference upstream node data
|
|
1485
|
+
|
|
1486
|
+
### Accessing a field from an upstream node:
|
|
1487
|
+
- CORRECT: $('NodeName').item.json.field
|
|
1488
|
+
- WRONG: $node["NodeName"].json.field \u2190 deprecated accessor, fails at runtime (Rule 24)
|
|
1489
|
+
|
|
1490
|
+
### Accessing array items from $json:
|
|
1491
|
+
- CORRECT: $json.field \u2190 n8n auto-flattens items; each item is already a flat object
|
|
1492
|
+
- WRONG: $json.items[0].field \u2190 do not index into items[] (Rule 25)
|
|
1493
|
+
|
|
1494
|
+
### Calling node data \u2014 always qualify with .first() or .all():
|
|
1495
|
+
- CORRECT: $('NodeName').first().json.field \u2190 single item
|
|
1496
|
+
- CORRECT: $('NodeName').all() \u2190 array of all items
|
|
1497
|
+
- WRONG: $('NodeName').json \u2190 throws at runtime without .first() or .all() (Rule 26)
|
|
1498
|
+
|
|
1499
|
+
---
|
|
1500
|
+
|
|
1245
1501
|
## NODE CATALOG \u2014 exact type strings and safe typeVersions
|
|
1246
1502
|
|
|
1247
1503
|
### Triggers (always at least one required):
|
|
@@ -1341,14 +1597,64 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
|
|
|
1341
1597
|
5. At least one trigger node present
|
|
1342
1598
|
6. Every AI Agent has an ai_languageModel sub-node
|
|
1343
1599
|
7. settings block is complete with executionOrder: "v1"
|
|
1600
|
+
8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
|
|
1601
|
+
9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
|
|
1602
|
+
10. No bare $('NodeName').json \u2014 always use .first().json.field or .all()
|
|
1344
1603
|
|
|
1345
1604
|
---
|
|
1346
1605
|
|
|
1347
1606
|
Respond ONLY with a generate_workflow tool call. No prose. No markdown outside the tool call.
|
|
1348
1607
|
If the request is impossible or unclear, set the error field instead of generating a workflow.`;
|
|
1349
1608
|
|
|
1350
|
-
// src/
|
|
1351
|
-
var
|
|
1609
|
+
// src/validation/rule-metadata.ts
|
|
1610
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 26 }, (_, i) => i + 1);
|
|
1611
|
+
var RULE_PIPELINE_STAGES = {
|
|
1612
|
+
1: "node_generation",
|
|
1613
|
+
2: "node_generation",
|
|
1614
|
+
3: "node_generation",
|
|
1615
|
+
4: "node_generation",
|
|
1616
|
+
5: "node_generation",
|
|
1617
|
+
6: "node_generation",
|
|
1618
|
+
7: "node_generation",
|
|
1619
|
+
8: "node_generation",
|
|
1620
|
+
9: "connection_wiring",
|
|
1621
|
+
10: "connection_wiring",
|
|
1622
|
+
11: "connection_wiring",
|
|
1623
|
+
12: "workflow_structure",
|
|
1624
|
+
13: "node_generation",
|
|
1625
|
+
14: "workflow_structure",
|
|
1626
|
+
15: "node_generation",
|
|
1627
|
+
16: "node_generation",
|
|
1628
|
+
17: "credential_injection",
|
|
1629
|
+
18: "connection_wiring",
|
|
1630
|
+
19: "node_generation",
|
|
1631
|
+
20: "connection_wiring",
|
|
1632
|
+
21: "workflow_structure",
|
|
1633
|
+
22: "workflow_structure",
|
|
1634
|
+
23: "node_generation",
|
|
1635
|
+
24: "expression_syntax",
|
|
1636
|
+
25: "expression_syntax",
|
|
1637
|
+
26: "expression_syntax"
|
|
1638
|
+
};
|
|
1639
|
+
var RULE_EXAMPLES = {
|
|
1640
|
+
17: {
|
|
1641
|
+
bad: '"credentials": { "slackOAuth2Api": "my-token" }',
|
|
1642
|
+
good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
|
|
1643
|
+
},
|
|
1644
|
+
24: {
|
|
1645
|
+
bad: '$node["Fetch Data"].json.email',
|
|
1646
|
+
good: "$('Fetch Data').item.json.email"
|
|
1647
|
+
},
|
|
1648
|
+
25: {
|
|
1649
|
+
bad: "$json.items[0].email",
|
|
1650
|
+
good: "$json.email"
|
|
1651
|
+
},
|
|
1652
|
+
26: {
|
|
1653
|
+
bad: "$('Fetch Data').json.email",
|
|
1654
|
+
good: "$('Fetch Data').first().json.email"
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
var RULE_MITIGATIONS = {
|
|
1352
1658
|
1: "Provide a non-empty workflow name string",
|
|
1353
1659
|
2: "Include at least one node in the nodes array",
|
|
1354
1660
|
3: "Every node must have a unique UUID v4 string as its id field",
|
|
@@ -1359,18 +1665,51 @@ var RULE_REMEDIES = {
|
|
|
1359
1665
|
8: "Every node must have a non-empty name string",
|
|
1360
1666
|
9: "connections must be a plain object (use {} if no connections)",
|
|
1361
1667
|
10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
|
|
1668
|
+
11: "Every non-trigger node should have at least one incoming connection",
|
|
1362
1669
|
12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
|
|
1363
|
-
|
|
1670
|
+
13: "workflow.settings must be a plain object if present",
|
|
1671
|
+
14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
|
|
1364
1672
|
15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
|
|
1365
1673
|
16: "All node names must be unique within the workflow",
|
|
1366
|
-
17: '
|
|
1674
|
+
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',
|
|
1367
1675
|
18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
|
|
1368
1676
|
19: "Use known safe typeVersion values for each node type",
|
|
1369
1677
|
20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
|
|
1370
1678
|
21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
|
|
1371
|
-
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
|
|
1679
|
+
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
|
|
1680
|
+
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
1681
|
+
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
1682
|
+
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
1683
|
+
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
|
|
1372
1684
|
};
|
|
1685
|
+
|
|
1686
|
+
// src/generation/prompt-builder.ts
|
|
1687
|
+
var CRITICAL_SCORE_THRESHOLD = 0.15;
|
|
1688
|
+
function resolveProfile() {
|
|
1689
|
+
const env = process.env["KAIROS_PROMPT_PROFILE"];
|
|
1690
|
+
if (env === "minimal" || env === "standard" || env === "rich") return env;
|
|
1691
|
+
return "standard";
|
|
1692
|
+
}
|
|
1693
|
+
var PROACTIVE_EXPRESSION_GUIDANCE = `## Expression Syntax Quick Reference
|
|
1694
|
+
|
|
1695
|
+
Always use these patterns in expressions:
|
|
1696
|
+
- Access node data: $('NodeName').item.json.field (not $node["NodeName"].json)
|
|
1697
|
+
- Access JSON field: $json.field (not $json.items[0].field)
|
|
1698
|
+
- Single item: $('NodeName').first().json.field
|
|
1699
|
+
- All items: $('NodeName').all()`;
|
|
1373
1700
|
var PromptBuilder = class {
|
|
1701
|
+
patternsPath;
|
|
1702
|
+
profile;
|
|
1703
|
+
_lastActivePatterns = null;
|
|
1704
|
+
constructor(patternsPath, profile) {
|
|
1705
|
+
this.patternsPath = patternsPath ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "patterns.json");
|
|
1706
|
+
this.profile = profile ?? resolveProfile();
|
|
1707
|
+
}
|
|
1708
|
+
resolveMaxPatterns() {
|
|
1709
|
+
if (this.profile === "minimal") return 3;
|
|
1710
|
+
if (this.profile === "rich") return 15;
|
|
1711
|
+
return 10;
|
|
1712
|
+
}
|
|
1374
1713
|
build(request, matches, globalFailureRates = [], dynamicCatalog) {
|
|
1375
1714
|
const mode = this.resolveMode(matches);
|
|
1376
1715
|
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
|
|
@@ -1407,69 +1746,178 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1407
1746
|
cache_control: { type: "ephemeral" }
|
|
1408
1747
|
}
|
|
1409
1748
|
];
|
|
1410
|
-
if (
|
|
1411
|
-
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1749
|
+
if (this.profile !== "minimal") {
|
|
1750
|
+
if (mode === "reference" && matches.length > 0) {
|
|
1751
|
+
const refText = matches.slice(0, 3).map((m) => {
|
|
1752
|
+
const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1753
|
+
return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
|
|
1414
1754
|
Nodes:
|
|
1415
1755
|
${nodes}`;
|
|
1416
|
-
|
|
1417
|
-
blocks.push({
|
|
1418
|
-
type: "text",
|
|
1419
|
-
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1420
|
-
|
|
1421
|
-
${refText}`
|
|
1422
|
-
});
|
|
1423
|
-
}
|
|
1424
|
-
if (mode === "direct" && matches[0]) {
|
|
1425
|
-
const match = matches[0];
|
|
1426
|
-
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1427
|
-
if (json.length > 3e4) {
|
|
1428
|
-
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1756
|
+
}).join("\n\n");
|
|
1429
1757
|
blocks.push({
|
|
1430
1758
|
type: "text",
|
|
1431
|
-
text: `##
|
|
1759
|
+
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1760
|
+
|
|
1761
|
+
${refText}`
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
if (mode === "direct" && matches[0]) {
|
|
1765
|
+
const match = matches[0];
|
|
1766
|
+
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1767
|
+
if (json.length > 3e4) {
|
|
1768
|
+
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1769
|
+
blocks.push({
|
|
1770
|
+
type: "text",
|
|
1771
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
|
|
1432
1772
|
Nodes:
|
|
1433
1773
|
${nodes}`
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1774
|
+
});
|
|
1775
|
+
} else {
|
|
1776
|
+
blocks.push({
|
|
1777
|
+
type: "text",
|
|
1778
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
|
|
1439
1779
|
|
|
1440
1780
|
${json}`
|
|
1441
|
-
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1442
1783
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
text: `## Weak Structural Hint
|
|
1784
|
+
if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
|
|
1785
|
+
const hint = matches[0];
|
|
1786
|
+
const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
|
|
1787
|
+
blocks.push({
|
|
1788
|
+
type: "text",
|
|
1789
|
+
text: `## Weak Structural Hint
|
|
1450
1790
|
A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
|
|
1451
|
-
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1452
1793
|
}
|
|
1453
1794
|
const warnings = this.buildFailureWarnings(matches, globalFailureRates);
|
|
1454
1795
|
if (warnings) {
|
|
1455
1796
|
blocks.push({ type: "text", text: warnings });
|
|
1456
1797
|
}
|
|
1798
|
+
if (this.profile === "rich") {
|
|
1799
|
+
const expressionRules = /* @__PURE__ */ new Set([24, 25, 26]);
|
|
1800
|
+
const expressionAlreadyCovered = (this._lastActivePatterns ?? []).some((p) => expressionRules.has(p.rule));
|
|
1801
|
+
if (!expressionAlreadyCovered) {
|
|
1802
|
+
blocks.push({ type: "text", text: PROACTIVE_EXPRESSION_GUIDANCE });
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1457
1805
|
return blocks;
|
|
1458
1806
|
}
|
|
1807
|
+
loadPatterns() {
|
|
1808
|
+
try {
|
|
1809
|
+
const raw = (0, import_node_fs.readFileSync)(this.patternsPath, "utf-8");
|
|
1810
|
+
const analysis = JSON.parse(raw);
|
|
1811
|
+
const patterns = analysis.topFailureRules ?? [];
|
|
1812
|
+
return patterns.filter((p) => typeof p.pipelineStage === "string" && typeof p.state === "string");
|
|
1813
|
+
} catch {
|
|
1814
|
+
return [];
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
getWarnedRules() {
|
|
1818
|
+
const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
|
|
1819
|
+
return patterns.map((p) => p.rule);
|
|
1820
|
+
}
|
|
1821
|
+
getActivePatterns(maxCount = 10) {
|
|
1822
|
+
const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
|
|
1823
|
+
const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1824
|
+
const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1825
|
+
const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1826
|
+
return [...regressed, ...confirmed, ...drafts].slice(0, maxCount);
|
|
1827
|
+
}
|
|
1459
1828
|
buildFailureWarnings(matches, globalFailureRates) {
|
|
1829
|
+
const richPatterns = this.getActivePatterns(this.resolveMaxPatterns());
|
|
1830
|
+
this._lastActivePatterns = richPatterns;
|
|
1831
|
+
if (richPatterns.length > 0) {
|
|
1832
|
+
return this.buildStageGroupedWarnings(richPatterns, matches);
|
|
1833
|
+
}
|
|
1834
|
+
return this.buildLegacyWarnings(matches, globalFailureRates);
|
|
1835
|
+
}
|
|
1836
|
+
buildStageGroupedWarnings(patterns, matches) {
|
|
1837
|
+
const stageLabels = {
|
|
1838
|
+
credential_injection: "CREDENTIAL FORMATTING",
|
|
1839
|
+
connection_wiring: "CONNECTION WIRING",
|
|
1840
|
+
node_generation: "NODE GENERATION",
|
|
1841
|
+
workflow_structure: "WORKFLOW STRUCTURE",
|
|
1842
|
+
expression_syntax: "EXPRESSION SYNTAX"
|
|
1843
|
+
};
|
|
1844
|
+
const byStage = /* @__PURE__ */ new Map();
|
|
1845
|
+
for (const p of patterns) {
|
|
1846
|
+
const list = byStage.get(p.pipelineStage) ?? [];
|
|
1847
|
+
list.push(p);
|
|
1848
|
+
byStage.set(p.pipelineStage, list);
|
|
1849
|
+
}
|
|
1850
|
+
const sections = [];
|
|
1851
|
+
for (const [stage, stagePatterns] of byStage) {
|
|
1852
|
+
const label = stageLabels[stage] ?? stage;
|
|
1853
|
+
const byMitigation = /* @__PURE__ */ new Map();
|
|
1854
|
+
for (const p of stagePatterns) {
|
|
1855
|
+
const key = p.mitigation ?? `rule_${p.rule}`;
|
|
1856
|
+
const list = byMitigation.get(key) ?? [];
|
|
1857
|
+
list.push(p);
|
|
1858
|
+
byMitigation.set(key, list);
|
|
1859
|
+
}
|
|
1860
|
+
const lines = [];
|
|
1861
|
+
for (const group of byMitigation.values()) {
|
|
1862
|
+
if (group.length === 1) {
|
|
1863
|
+
const p = group[0];
|
|
1864
|
+
const urgency = p.regressed ? "CRITICAL REGRESSION: " : (p.compositeScore ?? 0) >= CRITICAL_SCORE_THRESHOLD ? "CRITICAL: " : "";
|
|
1865
|
+
const statePrefix = p.state === "confirmed" ? "[CONFIRMED] " : "";
|
|
1866
|
+
const trendSuffix = p.trend === "worsening" ? " (GETTING WORSE)" : p.trend === "improving" ? " (improving)" : "";
|
|
1867
|
+
const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
|
|
1868
|
+
const remedyStr = remedy ? `
|
|
1869
|
+
Fix: ${remedy}` : "";
|
|
1870
|
+
const ex = RULE_EXAMPLES[p.rule];
|
|
1871
|
+
const exampleStr = ex ? `
|
|
1872
|
+
Bad: ${ex.bad}
|
|
1873
|
+
Good: ${ex.good}` : "";
|
|
1874
|
+
lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}${exampleStr}`);
|
|
1875
|
+
} else {
|
|
1876
|
+
const ruleNums = group.map((p) => p.rule).join(", ");
|
|
1877
|
+
const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
|
|
1878
|
+
const hasConfirmed = group.some((p) => p.state === "confirmed");
|
|
1879
|
+
const statePrefix = hasConfirmed ? "[CONFIRMED] " : "";
|
|
1880
|
+
const remedy = group[0].mitigation;
|
|
1881
|
+
const remedyStr = remedy ? `
|
|
1882
|
+
Fix: ${remedy}` : "";
|
|
1883
|
+
lines.push(`- ${statePrefix}Rules ${ruleNums} (${totalFailures} failures combined): same root cause${remedyStr}`);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
sections.push(`### ${label}
|
|
1887
|
+
${lines.join("\n")}`);
|
|
1888
|
+
}
|
|
1889
|
+
for (const match of matches) {
|
|
1890
|
+
const fps = match.workflow.failurePatterns;
|
|
1891
|
+
if (!fps?.length) continue;
|
|
1892
|
+
const coveredRules = new Set(patterns.map((p) => p.rule));
|
|
1893
|
+
const extra = fps.filter((fp) => !coveredRules.has(fp.rule));
|
|
1894
|
+
for (const fp of extra) {
|
|
1895
|
+
const remedy = RULE_MITIGATIONS[fp.rule];
|
|
1896
|
+
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1897
|
+
sections.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen in similar workflows)`);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (sections.length === 0) return null;
|
|
1901
|
+
return `## Known Failure Patterns \u2014 AVOID THESE
|
|
1902
|
+
|
|
1903
|
+
Grouped by generation stage. Fix these BEFORE outputting your response:
|
|
1904
|
+
|
|
1905
|
+
${sections.join("\n\n")}`;
|
|
1906
|
+
}
|
|
1907
|
+
buildLegacyWarnings(matches, globalFailureRates) {
|
|
1460
1908
|
const lines = [];
|
|
1461
1909
|
for (const match of matches) {
|
|
1462
1910
|
const patterns = match.workflow.failurePatterns;
|
|
1463
1911
|
if (!patterns?.length) continue;
|
|
1464
1912
|
for (const fp of patterns) {
|
|
1465
|
-
const remedy =
|
|
1913
|
+
const remedy = RULE_MITIGATIONS[fp.rule];
|
|
1466
1914
|
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1467
1915
|
lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
|
|
1468
1916
|
}
|
|
1469
1917
|
}
|
|
1470
1918
|
const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
|
|
1471
1919
|
for (const rule of highFreqRules) {
|
|
1472
|
-
const remedy =
|
|
1920
|
+
const remedy = RULE_MITIGATIONS[rule.rule];
|
|
1473
1921
|
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1474
1922
|
lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
|
|
1475
1923
|
}
|
|
@@ -1488,15 +1936,55 @@ Workflow name: "${request.name}"` : "";
|
|
|
1488
1936
|
};
|
|
1489
1937
|
|
|
1490
1938
|
// src/telemetry/reader.ts
|
|
1939
|
+
var import_node_os3 = require("os");
|
|
1940
|
+
var import_node_path4 = require("path");
|
|
1941
|
+
|
|
1942
|
+
// src/telemetry/event-reader.ts
|
|
1491
1943
|
var import_promises2 = require("fs/promises");
|
|
1492
|
-
var
|
|
1493
|
-
var
|
|
1944
|
+
var import_node_fs2 = require("fs");
|
|
1945
|
+
var import_node_path3 = require("path");
|
|
1946
|
+
var import_node_readline = require("readline");
|
|
1947
|
+
async function readTelemetryEvents(dir, days) {
|
|
1948
|
+
let files;
|
|
1949
|
+
try {
|
|
1950
|
+
files = await (0, import_promises2.readdir)(dir);
|
|
1951
|
+
} catch {
|
|
1952
|
+
return [];
|
|
1953
|
+
}
|
|
1954
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
1955
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
1956
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
1957
|
+
const todayStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1958
|
+
const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
1959
|
+
const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr && f <= `${todayStr}.jsonl`).sort();
|
|
1960
|
+
const events = [];
|
|
1961
|
+
for (const file of recentFiles) {
|
|
1962
|
+
const fileDate = file.replace(".jsonl", "");
|
|
1963
|
+
try {
|
|
1964
|
+
const rl = (0, import_node_readline.createInterface)({
|
|
1965
|
+
input: (0, import_node_fs2.createReadStream)((0, import_node_path3.join)(dir, file), "utf-8"),
|
|
1966
|
+
crlfDelay: Infinity
|
|
1967
|
+
});
|
|
1968
|
+
for await (const line of rl) {
|
|
1969
|
+
if (!line.trim()) continue;
|
|
1970
|
+
try {
|
|
1971
|
+
events.push({ ...JSON.parse(line), fileDate });
|
|
1972
|
+
} catch {
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
} catch {
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return events;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// src/telemetry/reader.ts
|
|
1494
1982
|
var TelemetryReader = class {
|
|
1495
1983
|
dir;
|
|
1496
1984
|
cache = null;
|
|
1497
1985
|
cacheTime = 0;
|
|
1498
1986
|
constructor(dir) {
|
|
1499
|
-
this.dir = dir ?? (0,
|
|
1987
|
+
this.dir = dir ?? (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".kairos", "telemetry");
|
|
1500
1988
|
}
|
|
1501
1989
|
async getFailureRates(days = 30) {
|
|
1502
1990
|
const now = Date.now();
|
|
@@ -1505,9 +1993,10 @@ var TelemetryReader = class {
|
|
|
1505
1993
|
}
|
|
1506
1994
|
const events = await this.readRecentEvents(days);
|
|
1507
1995
|
const buildSessions = new Set(
|
|
1508
|
-
events.filter((e) => e.eventType === "build_complete"
|
|
1996
|
+
events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
|
|
1509
1997
|
);
|
|
1510
|
-
|
|
1998
|
+
const MIN_BUILDS_FOR_RATES = 3;
|
|
1999
|
+
if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
|
|
1511
2000
|
const ruleSessions = /* @__PURE__ */ new Map();
|
|
1512
2001
|
for (const event of events) {
|
|
1513
2002
|
if (event.eventType !== "generation_attempt") continue;
|
|
@@ -1545,32 +2034,487 @@ var TelemetryReader = class {
|
|
|
1545
2034
|
return rates;
|
|
1546
2035
|
}
|
|
1547
2036
|
async readRecentEvents(days) {
|
|
1548
|
-
|
|
2037
|
+
return readTelemetryEvents(this.dir, days);
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
|
|
2041
|
+
// src/telemetry/pattern-analyzer.ts
|
|
2042
|
+
var import_promises3 = require("fs/promises");
|
|
2043
|
+
var import_node_path5 = require("path");
|
|
2044
|
+
var import_node_os4 = require("os");
|
|
2045
|
+
var PATTERN_SCHEMA_VERSION = 2;
|
|
2046
|
+
var PatternAnalyzer = class _PatternAnalyzer {
|
|
2047
|
+
telemetryDir;
|
|
2048
|
+
outputDir;
|
|
2049
|
+
_cachedEvents = null;
|
|
2050
|
+
constructor(telemetryDir) {
|
|
2051
|
+
const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
|
|
2052
|
+
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
2053
|
+
this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
|
|
2054
|
+
}
|
|
2055
|
+
async loadPreviousPatterns() {
|
|
1549
2056
|
try {
|
|
1550
|
-
|
|
2057
|
+
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
|
|
2058
|
+
const prev = JSON.parse(raw);
|
|
2059
|
+
const version = prev.schemaVersion ?? 0;
|
|
2060
|
+
const patterns = prev.topFailureRules ?? [];
|
|
2061
|
+
if (version === PATTERN_SCHEMA_VERSION) return patterns;
|
|
2062
|
+
return this.migratePatterns(patterns, version);
|
|
1551
2063
|
} catch {
|
|
1552
2064
|
return [];
|
|
1553
2065
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
2066
|
+
}
|
|
2067
|
+
migratePatterns(patterns, fromVersion) {
|
|
2068
|
+
let migrated = patterns;
|
|
2069
|
+
if (fromVersion < 1) {
|
|
2070
|
+
migrated = migrated.map((p) => ({
|
|
2071
|
+
...p,
|
|
2072
|
+
compositeScore: p.compositeScore ?? 0,
|
|
2073
|
+
scoringFactors: p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
|
|
2074
|
+
pipelineStage: p.pipelineStage ?? "node_generation"
|
|
2075
|
+
}));
|
|
2076
|
+
}
|
|
2077
|
+
if (fromVersion < 2) {
|
|
2078
|
+
migrated = migrated.map((p) => {
|
|
2079
|
+
const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
|
|
2080
|
+
return {
|
|
2081
|
+
...p,
|
|
2082
|
+
scoringFactors: {
|
|
2083
|
+
...sf,
|
|
2084
|
+
stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
return migrated;
|
|
2090
|
+
}
|
|
2091
|
+
async analyze(days = 30) {
|
|
2092
|
+
const previousPatterns = await this.loadPreviousPatterns();
|
|
2093
|
+
const events = await this.readAllEvents(days);
|
|
2094
|
+
this._cachedEvents = events;
|
|
2095
|
+
const starts = events.filter((e) => e.eventType === "build_start");
|
|
2096
|
+
const attempts = events.filter((e) => e.eventType === "generation_attempt");
|
|
2097
|
+
const passed = attempts.filter(
|
|
2098
|
+
(a) => a.data.validationPassed === true
|
|
2099
|
+
);
|
|
2100
|
+
const failed = attempts.filter(
|
|
2101
|
+
(a) => a.data.validationPassed === false
|
|
2102
|
+
);
|
|
2103
|
+
const ruleFailures = /* @__PURE__ */ new Map();
|
|
2104
|
+
const credentialFailures = /* @__PURE__ */ new Map();
|
|
2105
|
+
for (const a of failed) {
|
|
2106
|
+
const weight = this.recencyWeight(a.fileDate);
|
|
2107
|
+
const buildId = a.runId ?? a.sessionId;
|
|
2108
|
+
const data = a.data;
|
|
2109
|
+
for (const issue of data.issues ?? []) {
|
|
2110
|
+
if (issue.severity === "warn") continue;
|
|
2111
|
+
const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
|
|
2112
|
+
entry.count++;
|
|
2113
|
+
entry.sessions.add(buildId);
|
|
2114
|
+
entry.recencyWeights.push(weight);
|
|
2115
|
+
entry.allMessages.push(issue.message);
|
|
2116
|
+
if (data.workflowType) {
|
|
2117
|
+
entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
|
|
2118
|
+
}
|
|
2119
|
+
ruleFailures.set(issue.rule, entry);
|
|
2120
|
+
if (issue.rule === 17) {
|
|
2121
|
+
const credPatterns = [
|
|
2122
|
+
/credential\s+"([^"]+)"/,
|
|
2123
|
+
/credentialType[:\s]+"?([^"'\s]+)"?/,
|
|
2124
|
+
/missing\s+credential\s+(?:for\s+)?["']?([^"'\s]+)/i
|
|
2125
|
+
];
|
|
2126
|
+
let credType = "unknown";
|
|
2127
|
+
for (const re of credPatterns) {
|
|
2128
|
+
const m = issue.message.match(re);
|
|
2129
|
+
if (m?.[1]) {
|
|
2130
|
+
credType = m[1];
|
|
2131
|
+
break;
|
|
2132
|
+
}
|
|
1568
2133
|
}
|
|
2134
|
+
credentialFailures.set(credType, (credentialFailures.get(credType) ?? 0) + 1);
|
|
1569
2135
|
}
|
|
1570
|
-
} catch {
|
|
1571
2136
|
}
|
|
1572
2137
|
}
|
|
1573
|
-
|
|
2138
|
+
const failedByDate = /* @__PURE__ */ new Map();
|
|
2139
|
+
for (const a of failed) {
|
|
2140
|
+
failedByDate.set(a.fileDate, (failedByDate.get(a.fileDate) ?? 0) + 1);
|
|
2141
|
+
}
|
|
2142
|
+
const sortedFailDates = [...failedByDate.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
2143
|
+
const hasTrendData = sortedFailDates.length >= 3;
|
|
2144
|
+
let midDate = "";
|
|
2145
|
+
if (hasTrendData) {
|
|
2146
|
+
const halfTotal = failed.length / 2;
|
|
2147
|
+
let cumulative = 0;
|
|
2148
|
+
for (const [date, count] of sortedFailDates) {
|
|
2149
|
+
cumulative += count;
|
|
2150
|
+
if (cumulative >= halfTotal) {
|
|
2151
|
+
midDate = date;
|
|
2152
|
+
break;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
const ruleTrends = /* @__PURE__ */ new Map();
|
|
2157
|
+
if (hasTrendData) {
|
|
2158
|
+
for (const a of failed) {
|
|
2159
|
+
const data = a.data;
|
|
2160
|
+
const isNewer = a.fileDate > midDate;
|
|
2161
|
+
for (const issue of data.issues ?? []) {
|
|
2162
|
+
const entry = ruleTrends.get(issue.rule) ?? { older: 0, newer: 0 };
|
|
2163
|
+
if (isNewer) entry.newer++;
|
|
2164
|
+
else entry.older++;
|
|
2165
|
+
ruleTrends.set(issue.rule, entry);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
2170
|
+
for (const a of attempts) {
|
|
2171
|
+
const buildId = a.runId ?? a.sessionId;
|
|
2172
|
+
const list = sessions.get(buildId) ?? [];
|
|
2173
|
+
list.push(a);
|
|
2174
|
+
sessions.set(buildId, list);
|
|
2175
|
+
}
|
|
2176
|
+
let firstTryPass = 0;
|
|
2177
|
+
let correctionNeeded = 0;
|
|
2178
|
+
let singleAttemptFail = 0;
|
|
2179
|
+
for (const sessionAttempts of sessions.values()) {
|
|
2180
|
+
const lastAttempt = sessionAttempts[sessionAttempts.length - 1];
|
|
2181
|
+
const lastPassed = lastAttempt.data.validationPassed === true;
|
|
2182
|
+
if (sessionAttempts.length === 1 && lastPassed) {
|
|
2183
|
+
firstTryPass++;
|
|
2184
|
+
} else if (sessionAttempts.length > 1 && lastPassed) {
|
|
2185
|
+
correctionNeeded++;
|
|
2186
|
+
} else {
|
|
2187
|
+
singleAttemptFail++;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
const durations = attempts.map((a) => a.data.durationMs).filter((d) => typeof d === "number" && d > 0);
|
|
2191
|
+
const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
|
|
2192
|
+
const totalInput = attempts.reduce((s, a) => s + (a.data.tokensInput ?? 0), 0);
|
|
2193
|
+
const totalOutput = attempts.reduce((s, a) => s + (a.data.tokensOutput ?? 0), 0);
|
|
2194
|
+
const totalSessions = Math.max(sessions.size, 1);
|
|
2195
|
+
const stickinessCount = /* @__PURE__ */ new Map();
|
|
2196
|
+
for (const sessionAttempts of sessions.values()) {
|
|
2197
|
+
if (sessionAttempts.length < 2) continue;
|
|
2198
|
+
for (let i = 0; i < sessionAttempts.length - 1; i++) {
|
|
2199
|
+
const curr = sessionAttempts[i].data;
|
|
2200
|
+
const next = sessionAttempts[i + 1].data;
|
|
2201
|
+
if (curr.validationPassed !== false || next.validationPassed !== false) continue;
|
|
2202
|
+
const currRules = new Set((curr.issues ?? []).map((iss) => iss.rule));
|
|
2203
|
+
const nextRules = new Set((next.issues ?? []).map((iss) => iss.rule));
|
|
2204
|
+
for (const rule of currRules) {
|
|
2205
|
+
if (nextRules.has(rule)) {
|
|
2206
|
+
stickinessCount.set(rule, (stickinessCount.get(rule) ?? 0) + 1);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
const CONFIRMED_THRESHOLD = 3;
|
|
2212
|
+
const BUILDS_SINCE_LAST_FAILURE_THRESHOLD = 5;
|
|
2213
|
+
const RESOLVED_TTL_DAYS = 90;
|
|
2214
|
+
const activePatterns = [...ruleFailures.entries()].map(([rule, entry]) => {
|
|
2215
|
+
const t = ruleTrends.get(rule) ?? { older: 0, newer: 0 };
|
|
2216
|
+
const rawConfidence = Math.min(entry.sessions.size / totalSessions, 1);
|
|
2217
|
+
const state = entry.count >= CONFIRMED_THRESHOLD ? "confirmed" : "draft";
|
|
2218
|
+
const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
|
|
2219
|
+
const stickiness = stickinessCount.get(rule) ?? 0;
|
|
2220
|
+
const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
|
|
2221
|
+
const pattern = {
|
|
2222
|
+
rule,
|
|
2223
|
+
failureCount: entry.count,
|
|
2224
|
+
confidence: Math.round(rawConfidence * 1e3) / 1e3,
|
|
2225
|
+
compositeScore,
|
|
2226
|
+
scoringFactors: factors,
|
|
2227
|
+
state,
|
|
2228
|
+
trend: this.classifyTrend(t.older, t.newer),
|
|
2229
|
+
pipelineStage: RULE_PIPELINE_STAGES[rule] ?? "node_generation",
|
|
2230
|
+
exampleMessages: this.deduplicateMessages(entry.allMessages),
|
|
2231
|
+
mitigation: RULE_MITIGATIONS[rule] ?? null
|
|
2232
|
+
};
|
|
2233
|
+
if (entry.workflowTypes.size > 0) {
|
|
2234
|
+
pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
|
|
2235
|
+
}
|
|
2236
|
+
return pattern;
|
|
2237
|
+
}).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
2238
|
+
const activeRules = new Set(activePatterns.map((p) => p.rule));
|
|
2239
|
+
for (const p of activePatterns) {
|
|
2240
|
+
const prev = previousPatterns.find((pp) => pp.rule === p.rule);
|
|
2241
|
+
if (prev?.state === "resolved") {
|
|
2242
|
+
p.trend = "worsening";
|
|
2243
|
+
p.regressed = true;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
const ruleLastFailureDate = /* @__PURE__ */ new Map();
|
|
2247
|
+
for (const a of failed) {
|
|
2248
|
+
const data = a.data;
|
|
2249
|
+
for (const issue of data.issues ?? []) {
|
|
2250
|
+
const existing = ruleLastFailureDate.get(issue.rule);
|
|
2251
|
+
if (!existing || a.fileDate > existing) {
|
|
2252
|
+
ruleLastFailureDate.set(issue.rule, a.fileDate);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
const newlyResolved = previousPatterns.filter((p) => {
|
|
2257
|
+
if (p.state !== "confirmed" || activeRules.has(p.rule)) return false;
|
|
2258
|
+
const lastFailDate = ruleLastFailureDate.get(p.rule) ?? "";
|
|
2259
|
+
const buildsSince = starts.filter((s) => s.fileDate > lastFailDate).length;
|
|
2260
|
+
return buildsSince >= BUILDS_SINCE_LAST_FAILURE_THRESHOLD;
|
|
2261
|
+
}).map((p) => ({
|
|
2262
|
+
...p,
|
|
2263
|
+
state: "resolved",
|
|
2264
|
+
trend: "improving",
|
|
2265
|
+
pipelineStage: p.pipelineStage ?? RULE_PIPELINE_STAGES[p.rule] ?? "node_generation",
|
|
2266
|
+
confidence: 0,
|
|
2267
|
+
compositeScore: 0,
|
|
2268
|
+
scoringFactors: { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
|
|
2269
|
+
failureCount: 0,
|
|
2270
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2271
|
+
}));
|
|
2272
|
+
const ttlCutoff = /* @__PURE__ */ new Date();
|
|
2273
|
+
ttlCutoff.setDate(ttlCutoff.getDate() - RESOLVED_TTL_DAYS);
|
|
2274
|
+
const ttlCutoffStr = ttlCutoff.toISOString();
|
|
2275
|
+
const carriedResolved = previousPatterns.filter((p) => p.state === "resolved" && !activeRules.has(p.rule) && (!p.resolvedAt || p.resolvedAt >= ttlCutoffStr)).map((p) => ({ ...p }));
|
|
2276
|
+
const newlyResolvedRules = new Set(newlyResolved.map((p) => p.rule));
|
|
2277
|
+
const pendingResolution = previousPatterns.filter((p) => p.state === "confirmed" && !activeRules.has(p.rule) && !newlyResolvedRules.has(p.rule)).map((p) => ({ ...p }));
|
|
2278
|
+
const deduped = [
|
|
2279
|
+
...newlyResolved,
|
|
2280
|
+
...carriedResolved.filter((p) => !newlyResolvedRules.has(p.rule)),
|
|
2281
|
+
...pendingResolution
|
|
2282
|
+
];
|
|
2283
|
+
const patterns = [...activePatterns, ...deduped];
|
|
2284
|
+
const credTypes = [...credentialFailures.entries()].sort((a, b) => b[1] - a[1]).map(([type, count]) => ({ type, count }));
|
|
2285
|
+
const drift = this.detectDrift(patterns);
|
|
2286
|
+
const warnEffMap = /* @__PURE__ */ new Map();
|
|
2287
|
+
const buildCompletes = events.filter((e) => e.eventType === "build_complete");
|
|
2288
|
+
for (const bc of buildCompletes) {
|
|
2289
|
+
const bcData = bc.data;
|
|
2290
|
+
const warned = bcData.warnedRules ?? [];
|
|
2291
|
+
if (warned.length === 0) continue;
|
|
2292
|
+
const sessionFailedRules = /* @__PURE__ */ new Set();
|
|
2293
|
+
const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
|
|
2294
|
+
for (const a of sessionAttempts) {
|
|
2295
|
+
const ad = a.data;
|
|
2296
|
+
if (ad.validationPassed === false) {
|
|
2297
|
+
for (const issue of ad.issues ?? []) {
|
|
2298
|
+
sessionFailedRules.add(issue.rule);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
for (const rule of warned) {
|
|
2303
|
+
const entry = warnEffMap.get(rule) ?? { warned: 0, passed: 0, failed: 0 };
|
|
2304
|
+
entry.warned++;
|
|
2305
|
+
if (sessionFailedRules.has(rule)) entry.failed++;
|
|
2306
|
+
else entry.passed++;
|
|
2307
|
+
warnEffMap.set(rule, entry);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
const warningEffectiveness = [...warnEffMap.entries()].map(([rule, e]) => ({
|
|
2311
|
+
rule,
|
|
2312
|
+
timesWarned: e.warned,
|
|
2313
|
+
timesWarnedAndPassed: e.passed,
|
|
2314
|
+
timesWarnedAndFailed: e.failed,
|
|
2315
|
+
effectivenessRate: e.warned > 0 ? Math.round(e.passed / e.warned * 1e3) / 1e3 : 0
|
|
2316
|
+
})).sort((a, b) => b.timesWarned - a.timesWarned);
|
|
2317
|
+
const coOccurrenceMap = /* @__PURE__ */ new Map();
|
|
2318
|
+
for (const a of failed) {
|
|
2319
|
+
const data = a.data;
|
|
2320
|
+
const rules = [...new Set((data.issues ?? []).map((i) => i.rule))].sort((x, y) => x - y);
|
|
2321
|
+
for (let i = 0; i < rules.length; i++) {
|
|
2322
|
+
for (let j = i + 1; j < rules.length; j++) {
|
|
2323
|
+
const key = `${rules[i]},${rules[j]}`;
|
|
2324
|
+
coOccurrenceMap.set(key, (coOccurrenceMap.get(key) ?? 0) + 1);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
const ruleCoOccurrence = [...coOccurrenceMap.entries()].filter(([, count]) => count >= 3).map(([key, count]) => {
|
|
2329
|
+
const [a, b] = key.split(",").map(Number);
|
|
2330
|
+
return { rules: [a, b], count };
|
|
2331
|
+
}).sort((a, b) => b.count - a.count);
|
|
2332
|
+
const attemptDistribution = {};
|
|
2333
|
+
for (const sessionAttempts of sessions.values()) {
|
|
2334
|
+
const depth = sessionAttempts.length;
|
|
2335
|
+
attemptDistribution[depth] = (attemptDistribution[depth] ?? 0) + 1;
|
|
2336
|
+
}
|
|
2337
|
+
return {
|
|
2338
|
+
schemaVersion: PATTERN_SCHEMA_VERSION,
|
|
2339
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2340
|
+
summary: {
|
|
2341
|
+
totalBuilds: starts.length,
|
|
2342
|
+
totalAttempts: attempts.length,
|
|
2343
|
+
firstTryPassRate: Math.round(firstTryPass / totalSessions * 1e3) / 1e3,
|
|
2344
|
+
correctionRate: Math.round(correctionNeeded / totalSessions * 1e3) / 1e3,
|
|
2345
|
+
singleAttemptFailRate: Math.round(singleAttemptFail / totalSessions * 1e3) / 1e3,
|
|
2346
|
+
avgDurationMs: Math.round(avgDuration),
|
|
2347
|
+
totalTokensInput: totalInput,
|
|
2348
|
+
totalTokensOutput: totalOutput,
|
|
2349
|
+
attemptDistribution
|
|
2350
|
+
},
|
|
2351
|
+
topFailureRules: patterns,
|
|
2352
|
+
failingCredentialTypes: credTypes,
|
|
2353
|
+
drift,
|
|
2354
|
+
warningEffectiveness,
|
|
2355
|
+
ruleCoOccurrence
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
async analyzeAndSave(days = 30) {
|
|
2359
|
+
const analysis = await this.analyze(days);
|
|
2360
|
+
await (0, import_promises3.mkdir)(this.outputDir, { recursive: true });
|
|
2361
|
+
const outputPath = (0, import_node_path5.join)(this.outputDir, "patterns.json");
|
|
2362
|
+
const tmpPath = `${outputPath}.tmp`;
|
|
2363
|
+
await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
2364
|
+
await (0, import_promises3.rename)(tmpPath, outputPath);
|
|
2365
|
+
const historySummary = {
|
|
2366
|
+
timestamp: analysis.generatedAt,
|
|
2367
|
+
totalBuilds: analysis.summary.totalBuilds,
|
|
2368
|
+
firstTryPassRate: analysis.summary.firstTryPassRate,
|
|
2369
|
+
correctionRate: analysis.summary.correctionRate,
|
|
2370
|
+
singleAttemptFailRate: analysis.summary.singleAttemptFailRate,
|
|
2371
|
+
activePatternCount: analysis.topFailureRules.filter((p) => p.state !== "resolved").length,
|
|
2372
|
+
topRules: analysis.topFailureRules.filter((p) => p.state !== "resolved").slice(0, 5).map((p) => ({ rule: p.rule, compositeScore: p.compositeScore, state: p.state }))
|
|
2373
|
+
};
|
|
2374
|
+
const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
|
|
2375
|
+
await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
|
|
2376
|
+
const sessions = await this.buildSessionSummaries(days);
|
|
2377
|
+
const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
|
|
2378
|
+
const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
|
|
2379
|
+
await (0, import_promises3.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
|
|
2380
|
+
await (0, import_promises3.rename)(sessionHistoryTmp, sessionHistoryPath);
|
|
2381
|
+
return analysis;
|
|
2382
|
+
}
|
|
2383
|
+
async getSessions(limit = 20) {
|
|
2384
|
+
try {
|
|
2385
|
+
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
|
|
2386
|
+
const all = JSON.parse(raw);
|
|
2387
|
+
return all.slice(-limit);
|
|
2388
|
+
} catch {
|
|
2389
|
+
return [];
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
async buildSessionSummaries(days = 30) {
|
|
2393
|
+
const events = this._cachedEvents ?? await this.readAllEvents(days);
|
|
2394
|
+
const buildCompletes = events.filter((e) => e.eventType === "build_complete");
|
|
2395
|
+
const attemptsByBuild = /* @__PURE__ */ new Map();
|
|
2396
|
+
for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
|
|
2397
|
+
const buildId = e.runId ?? e.sessionId;
|
|
2398
|
+
const list = attemptsByBuild.get(buildId) ?? [];
|
|
2399
|
+
list.push(e);
|
|
2400
|
+
attemptsByBuild.set(buildId, list);
|
|
2401
|
+
}
|
|
2402
|
+
const summaries = buildCompletes.map((bc) => {
|
|
2403
|
+
const data = bc.data;
|
|
2404
|
+
const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
|
|
2405
|
+
const failedRules = Array.from(new Set(
|
|
2406
|
+
sessionAttempts.flatMap((a) => {
|
|
2407
|
+
const ad = a.data;
|
|
2408
|
+
if (ad.validationPassed !== false) return [];
|
|
2409
|
+
return (ad.issues ?? []).map((i) => i.rule);
|
|
2410
|
+
})
|
|
2411
|
+
));
|
|
2412
|
+
return {
|
|
2413
|
+
sessionId: bc.sessionId,
|
|
2414
|
+
date: bc.fileDate,
|
|
2415
|
+
description: data.description ?? "",
|
|
2416
|
+
workflowType: data.workflowType ?? null,
|
|
2417
|
+
attempts: data.totalAttempts ?? 1,
|
|
2418
|
+
success: data.success ?? false,
|
|
2419
|
+
failedRules,
|
|
2420
|
+
workflowName: data.workflowName ?? null
|
|
2421
|
+
};
|
|
2422
|
+
});
|
|
2423
|
+
return summaries.sort((a, b) => a.date.localeCompare(b.date));
|
|
2424
|
+
}
|
|
2425
|
+
async getHistory(limit = 20) {
|
|
2426
|
+
try {
|
|
2427
|
+
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
|
|
2428
|
+
return raw.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)).slice(-limit);
|
|
2429
|
+
} catch {
|
|
2430
|
+
return [];
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
static fromEnv() {
|
|
2434
|
+
const dir = process.env["KAIROS_TELEMETRY"];
|
|
2435
|
+
return dir && dir !== "true" && dir !== "false" ? new _PatternAnalyzer(dir) : new _PatternAnalyzer();
|
|
2436
|
+
}
|
|
2437
|
+
detectDrift(patterns) {
|
|
2438
|
+
const VALIDATOR_RULES = VALIDATOR_RULE_IDS;
|
|
2439
|
+
const validatorRuleSet = new Set(VALIDATOR_RULES);
|
|
2440
|
+
const alerts = [];
|
|
2441
|
+
for (const p of patterns) {
|
|
2442
|
+
if (p.state !== "resolved" && !validatorRuleSet.has(p.rule)) {
|
|
2443
|
+
alerts.push({
|
|
2444
|
+
type: "stale_pattern",
|
|
2445
|
+
rule: p.rule,
|
|
2446
|
+
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-26)`
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
for (const rule of VALIDATOR_RULES) {
|
|
2451
|
+
if (!(rule in RULE_MITIGATIONS)) {
|
|
2452
|
+
alerts.push({
|
|
2453
|
+
type: "missing_mitigation",
|
|
2454
|
+
rule,
|
|
2455
|
+
message: `Rule ${rule} has no mitigation text \u2014 if it fails, the system can't advise the LLM how to fix it`
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
if (!(rule in RULE_PIPELINE_STAGES)) {
|
|
2459
|
+
alerts.push({
|
|
2460
|
+
type: "missing_stage_mapping",
|
|
2461
|
+
rule,
|
|
2462
|
+
message: `Rule ${rule} has no pipeline stage mapping \u2014 failures won't be grouped correctly`
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
const coveredRules = VALIDATOR_RULES.filter((r) => r in RULE_MITIGATIONS && r in RULE_PIPELINE_STAGES).length;
|
|
2467
|
+
return {
|
|
2468
|
+
healthy: alerts.length === 0,
|
|
2469
|
+
alerts,
|
|
2470
|
+
coveredRules,
|
|
2471
|
+
totalRules: VALIDATOR_RULES.length
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
computeCompositeScore(rawConfidence, sampleSize, state, avgRecency, stickiness) {
|
|
2475
|
+
const stateWeights = { draft: 0.3, confirmed: 0.8, resolved: 0.1 };
|
|
2476
|
+
const stateWeight = stateWeights[state];
|
|
2477
|
+
const impact = (1 - Math.exp(-sampleSize / 5)) * stateWeight;
|
|
2478
|
+
const stickinessBoost = Math.min(0.15, stickiness * 0.05);
|
|
2479
|
+
const compositeScore = Math.min(Math.round(rawConfidence * impact * avgRecency * (1 + stickinessBoost) * 1e3) / 1e3, 1);
|
|
2480
|
+
return {
|
|
2481
|
+
compositeScore,
|
|
2482
|
+
factors: {
|
|
2483
|
+
rawConfidence: Math.round(rawConfidence * 1e3) / 1e3,
|
|
2484
|
+
impact: Math.round(impact * 1e3) / 1e3,
|
|
2485
|
+
recency: Math.round(avgRecency * 1e3) / 1e3,
|
|
2486
|
+
stickinessBoost: Math.round(stickinessBoost * 1e3) / 1e3
|
|
2487
|
+
}
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
classifyTrend(older, newer) {
|
|
2491
|
+
const total = older + newer;
|
|
2492
|
+
if (total === 0) return "stable";
|
|
2493
|
+
if (older === 0) return "new";
|
|
2494
|
+
const newerRatio = newer / total;
|
|
2495
|
+
if (newerRatio >= 0.65) return "worsening";
|
|
2496
|
+
if (newerRatio <= 0.35) return "improving";
|
|
2497
|
+
return "stable";
|
|
2498
|
+
}
|
|
2499
|
+
deduplicateMessages(messages, maxCount = 3) {
|
|
2500
|
+
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();
|
|
2501
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2502
|
+
const unique = [];
|
|
2503
|
+
for (const msg of messages) {
|
|
2504
|
+
const key = normalize(msg);
|
|
2505
|
+
if (!seen.has(key) && unique.length < maxCount) {
|
|
2506
|
+
seen.add(key);
|
|
2507
|
+
unique.push(msg);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return unique;
|
|
2511
|
+
}
|
|
2512
|
+
recencyWeight(fileDate, halfLifeDays = 30) {
|
|
2513
|
+
const daysAgo = Math.max(0, (Date.now() - (/* @__PURE__ */ new Date(fileDate + "T12:00:00Z")).getTime()) / (1e3 * 60 * 60 * 24));
|
|
2514
|
+
return Math.max(0.1, Math.exp(-Math.LN2 * daysAgo / halfLifeDays));
|
|
2515
|
+
}
|
|
2516
|
+
async readAllEvents(days) {
|
|
2517
|
+
return readTelemetryEvents(this.telemetryDir, days);
|
|
1574
2518
|
}
|
|
1575
2519
|
};
|
|
1576
2520
|
|
|
@@ -1631,6 +2575,43 @@ ${regularLines}`;
|
|
|
1631
2575
|
}
|
|
1632
2576
|
};
|
|
1633
2577
|
|
|
2578
|
+
// src/telemetry/collector.ts
|
|
2579
|
+
var import_promises4 = require("fs/promises");
|
|
2580
|
+
var import_node_path6 = require("path");
|
|
2581
|
+
var import_node_os5 = require("os");
|
|
2582
|
+
|
|
2583
|
+
// src/telemetry/types.ts
|
|
2584
|
+
var TELEMETRY_SCHEMA_VERSION = 2;
|
|
2585
|
+
|
|
2586
|
+
// src/telemetry/collector.ts
|
|
2587
|
+
var TelemetryCollector = class {
|
|
2588
|
+
dir;
|
|
2589
|
+
sessionId;
|
|
2590
|
+
dirReady = null;
|
|
2591
|
+
constructor(dir) {
|
|
2592
|
+
this.dir = dir ?? (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".kairos", "telemetry");
|
|
2593
|
+
this.sessionId = generateUUID();
|
|
2594
|
+
}
|
|
2595
|
+
async emit(eventType, data, runId) {
|
|
2596
|
+
const event = {
|
|
2597
|
+
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
2598
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2599
|
+
sessionId: this.sessionId,
|
|
2600
|
+
...runId ? { runId } : {},
|
|
2601
|
+
eventType,
|
|
2602
|
+
data
|
|
2603
|
+
};
|
|
2604
|
+
if (!this.dirReady) {
|
|
2605
|
+
this.dirReady = (0, import_promises4.mkdir)(this.dir, { recursive: true }).then(() => {
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
await this.dirReady;
|
|
2609
|
+
const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
|
|
2610
|
+
const filepath = (0, import_node_path6.join)(this.dir, filename);
|
|
2611
|
+
await (0, import_promises4.appendFile)(filepath, JSON.stringify(event) + "\n", "utf-8");
|
|
2612
|
+
}
|
|
2613
|
+
};
|
|
2614
|
+
|
|
1634
2615
|
// src/utils/logger.ts
|
|
1635
2616
|
var nullLogger = {
|
|
1636
2617
|
debug() {
|
|
@@ -1643,19 +2624,91 @@ var nullLogger = {
|
|
|
1643
2624
|
}
|
|
1644
2625
|
};
|
|
1645
2626
|
|
|
2627
|
+
// src/utils/workflow-type.ts
|
|
2628
|
+
var TYPE_KEYWORDS = [
|
|
2629
|
+
["gmail", "email"],
|
|
2630
|
+
["imap", "email"],
|
|
2631
|
+
["smtp", "email"],
|
|
2632
|
+
[" email", "email"],
|
|
2633
|
+
["slack", "slack"],
|
|
2634
|
+
["telegram", "messaging"],
|
|
2635
|
+
["discord", "messaging"],
|
|
2636
|
+
[" sms", "messaging"],
|
|
2637
|
+
["twilio", "messaging"],
|
|
2638
|
+
["webhook", "webhook"],
|
|
2639
|
+
["google sheets", "data"],
|
|
2640
|
+
["spreadsheet", "data"],
|
|
2641
|
+
["airtable", "data"],
|
|
2642
|
+
["notion", "data"],
|
|
2643
|
+
["github", "devops"],
|
|
2644
|
+
["gitlab", "devops"],
|
|
2645
|
+
["schedule", "schedule"],
|
|
2646
|
+
[" cron", "schedule"],
|
|
2647
|
+
["daily", "schedule"],
|
|
2648
|
+
["weekly", "schedule"],
|
|
2649
|
+
["hourly", "schedule"],
|
|
2650
|
+
["every day", "schedule"],
|
|
2651
|
+
["every hour", "schedule"],
|
|
2652
|
+
["every morning", "schedule"],
|
|
2653
|
+
["postgres", "database"],
|
|
2654
|
+
["mysql", "database"],
|
|
2655
|
+
["supabase", "database"],
|
|
2656
|
+
["redis", "database"],
|
|
2657
|
+
[" database", "database"],
|
|
2658
|
+
[" llm", "ai"],
|
|
2659
|
+
[" gpt", "ai"],
|
|
2660
|
+
["claude", "ai"],
|
|
2661
|
+
[" agent", "ai"],
|
|
2662
|
+
["langchain", "ai"],
|
|
2663
|
+
[" ai ", "ai"],
|
|
2664
|
+
[" ai", "ai"],
|
|
2665
|
+
["http request", "api"],
|
|
2666
|
+
["rest api", "api"],
|
|
2667
|
+
[" api", "api"]
|
|
2668
|
+
];
|
|
2669
|
+
function inferWorkflowType(description) {
|
|
2670
|
+
const lower = " " + description.toLowerCase();
|
|
2671
|
+
for (const [keyword, type] of TYPE_KEYWORDS) {
|
|
2672
|
+
if (lower.includes(keyword)) return type;
|
|
2673
|
+
}
|
|
2674
|
+
return null;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
1646
2677
|
// src/mcp-server.ts
|
|
1647
|
-
var
|
|
1648
|
-
var
|
|
2678
|
+
var import_node_fs3 = require("fs");
|
|
2679
|
+
var import_node_path7 = require("path");
|
|
2680
|
+
var import_node_os6 = require("os");
|
|
1649
2681
|
var import_node_url = require("url");
|
|
1650
2682
|
var import_meta = {};
|
|
1651
|
-
var __dirname = (0,
|
|
1652
|
-
var pkg = JSON.parse((0,
|
|
2683
|
+
var __dirname = (0, import_node_path7.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
2684
|
+
var pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path7.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
1653
2685
|
var library = new FileLibrary();
|
|
1654
2686
|
var validator = new N8nValidator();
|
|
1655
2687
|
var nodeSyncer = new NodeSyncer();
|
|
1656
2688
|
var lastSync = null;
|
|
1657
2689
|
var stripper = new N8nFieldStripper();
|
|
1658
|
-
var promptBuilder = new PromptBuilder();
|
|
2690
|
+
var promptBuilder = new PromptBuilder(getMcpPatternsPath());
|
|
2691
|
+
function getMcpTelemetry() {
|
|
2692
|
+
const val = process.env["KAIROS_TELEMETRY"];
|
|
2693
|
+
if (!val || val === "false") return null;
|
|
2694
|
+
return val === "true" ? new TelemetryCollector() : new TelemetryCollector(val);
|
|
2695
|
+
}
|
|
2696
|
+
function getMcpPatternsPath() {
|
|
2697
|
+
const val = process.env["KAIROS_TELEMETRY"];
|
|
2698
|
+
if (val && val !== "false" && val !== "true") {
|
|
2699
|
+
return (0, import_node_path7.join)(val, "..", "patterns.json");
|
|
2700
|
+
}
|
|
2701
|
+
return (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".kairos", "patterns.json");
|
|
2702
|
+
}
|
|
2703
|
+
var mcpTelemetry = getMcpTelemetry();
|
|
2704
|
+
var mcpSessions = /* @__PURE__ */ new Map();
|
|
2705
|
+
var SESSION_TTL_MS = 60 * 60 * 1e3;
|
|
2706
|
+
function evictStaleSessions() {
|
|
2707
|
+
const cutoff = Date.now() - SESSION_TTL_MS;
|
|
2708
|
+
for (const [id, session] of mcpSessions) {
|
|
2709
|
+
if (session.startTime < cutoff) mcpSessions.delete(id);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
1659
2712
|
function getTelemetryReader() {
|
|
1660
2713
|
try {
|
|
1661
2714
|
return new TelemetryReader();
|
|
@@ -1703,6 +2756,7 @@ server.tool(
|
|
|
1703
2756
|
name: import_zod.z.string().optional().describe("Optional workflow name override")
|
|
1704
2757
|
},
|
|
1705
2758
|
async ({ description, name }) => {
|
|
2759
|
+
evictStaleSessions();
|
|
1706
2760
|
const baseUrl = process.env["N8N_BASE_URL"];
|
|
1707
2761
|
const apiKey = process.env["N8N_API_KEY"];
|
|
1708
2762
|
if (!baseUrl || !apiKey) {
|
|
@@ -1714,6 +2768,8 @@ server.tool(
|
|
|
1714
2768
|
isError: true
|
|
1715
2769
|
};
|
|
1716
2770
|
}
|
|
2771
|
+
const runId = generateUUID();
|
|
2772
|
+
const workflowType = inferWorkflowType(description);
|
|
1717
2773
|
await library.initialize();
|
|
1718
2774
|
const syncResult = await autoSync();
|
|
1719
2775
|
const matches = await library.search(description);
|
|
@@ -1721,11 +2777,22 @@ server.tool(
|
|
|
1721
2777
|
const failureRates = await telemetryReader?.getFailureRates() ?? [];
|
|
1722
2778
|
const request = { description, ...name ? { name } : {} };
|
|
1723
2779
|
const built = promptBuilder.build(request, matches, failureRates, syncResult?.catalogText);
|
|
2780
|
+
if (mcpTelemetry) {
|
|
2781
|
+
mcpSessions.set(runId, {
|
|
2782
|
+
description,
|
|
2783
|
+
startTime: Date.now(),
|
|
2784
|
+
validateAttempts: 0,
|
|
2785
|
+
warnedRules: promptBuilder.getWarnedRules(),
|
|
2786
|
+
workflowType
|
|
2787
|
+
});
|
|
2788
|
+
await mcpTelemetry.emit("build_start", { description, model: "mcp-decomposed", dryRun: false }, runId);
|
|
2789
|
+
}
|
|
1724
2790
|
const systemText = built.system.map((block) => block.text).join("\n\n---\n\n");
|
|
1725
2791
|
return {
|
|
1726
2792
|
content: [{
|
|
1727
2793
|
type: "text",
|
|
1728
2794
|
text: JSON.stringify({
|
|
2795
|
+
kairos_run_id: runId,
|
|
1729
2796
|
mode: built.mode,
|
|
1730
2797
|
matchCount: matches.length,
|
|
1731
2798
|
topMatchScore: matches[0]?.score ?? null,
|
|
@@ -1757,11 +2824,12 @@ server.tool(
|
|
|
1757
2824
|
);
|
|
1758
2825
|
server.tool(
|
|
1759
2826
|
"kairos_validate",
|
|
1760
|
-
"Validate n8n workflow JSON against
|
|
2827
|
+
"Validate n8n workflow JSON against 26 structural rules. Returns pass/fail with specific issues. If validation fails, fix the issues and call this again. Errors block deployment; warnings are advisory.",
|
|
1761
2828
|
{
|
|
1762
|
-
workflow: import_zod.z.string().describe("The workflow JSON string to validate")
|
|
2829
|
+
workflow: import_zod.z.string().describe("The workflow JSON string to validate"),
|
|
2830
|
+
kairos_run_id: import_zod.z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation")
|
|
1763
2831
|
},
|
|
1764
|
-
async ({ workflow: workflowStr }) => {
|
|
2832
|
+
async ({ workflow: workflowStr, kairos_run_id }) => {
|
|
1765
2833
|
let parsed;
|
|
1766
2834
|
try {
|
|
1767
2835
|
parsed = JSON.parse(workflowStr);
|
|
@@ -1779,6 +2847,24 @@ server.tool(
|
|
|
1779
2847
|
const result = validator.validate(parsed);
|
|
1780
2848
|
const errors = result.issues.filter((i) => i.severity === "error");
|
|
1781
2849
|
const warnings = result.issues.filter((i) => i.severity === "warn");
|
|
2850
|
+
if (mcpTelemetry && kairos_run_id) {
|
|
2851
|
+
const session = mcpSessions.get(kairos_run_id);
|
|
2852
|
+
if (session) {
|
|
2853
|
+
session.validateAttempts++;
|
|
2854
|
+
await mcpTelemetry.emit("generation_attempt", {
|
|
2855
|
+
description: session.description,
|
|
2856
|
+
attempt: session.validateAttempts,
|
|
2857
|
+
temperature: 0,
|
|
2858
|
+
durationMs: 0,
|
|
2859
|
+
tokensInput: 0,
|
|
2860
|
+
tokensOutput: 0,
|
|
2861
|
+
validationPassed: result.valid,
|
|
2862
|
+
issueCount: result.issues.length,
|
|
2863
|
+
issues: result.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null })),
|
|
2864
|
+
workflowType: session.workflowType
|
|
2865
|
+
}, kairos_run_id);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
1782
2868
|
return {
|
|
1783
2869
|
content: [{
|
|
1784
2870
|
type: "text",
|
|
@@ -1807,9 +2893,10 @@ server.tool(
|
|
|
1807
2893
|
"Deploy a validated workflow to n8n. Pass the workflow JSON that passed kairos_validate. Strips server-assigned fields automatically. Requires N8N_BASE_URL and N8N_API_KEY.",
|
|
1808
2894
|
{
|
|
1809
2895
|
workflow: import_zod.z.string().describe("The validated workflow JSON string to deploy"),
|
|
1810
|
-
activate: import_zod.z.boolean().default(false).describe("Activate the workflow immediately after deployment")
|
|
2896
|
+
activate: import_zod.z.boolean().default(false).describe("Activate the workflow immediately after deployment"),
|
|
2897
|
+
kairos_run_id: import_zod.z.string().optional().describe("Run ID from kairos_prompt \u2014 enables telemetry correlation")
|
|
1811
2898
|
},
|
|
1812
|
-
async ({ workflow: workflowStr, activate }) => {
|
|
2899
|
+
async ({ workflow: workflowStr, activate, kairos_run_id }) => {
|
|
1813
2900
|
if (!isAllowed("deploy")) {
|
|
1814
2901
|
return {
|
|
1815
2902
|
content: [{
|
|
@@ -1869,6 +2956,28 @@ server.tool(
|
|
|
1869
2956
|
generationMode: "scratch",
|
|
1870
2957
|
generationAttempts: 1
|
|
1871
2958
|
});
|
|
2959
|
+
if (mcpTelemetry && kairos_run_id) {
|
|
2960
|
+
const session = mcpSessions.get(kairos_run_id);
|
|
2961
|
+
if (session) {
|
|
2962
|
+
await mcpTelemetry.emit("build_complete", {
|
|
2963
|
+
description: session.description,
|
|
2964
|
+
success: true,
|
|
2965
|
+
totalAttempts: session.validateAttempts,
|
|
2966
|
+
totalDurationMs: Date.now() - session.startTime,
|
|
2967
|
+
totalTokensInput: 0,
|
|
2968
|
+
totalTokensOutput: 0,
|
|
2969
|
+
workflowName: response.name,
|
|
2970
|
+
workflowId: response.id,
|
|
2971
|
+
dryRun: false,
|
|
2972
|
+
credentialsNeeded: 0,
|
|
2973
|
+
warnedRules: session.warnedRules,
|
|
2974
|
+
workflowType: session.workflowType
|
|
2975
|
+
}, kairos_run_id);
|
|
2976
|
+
mcpSessions.delete(kairos_run_id);
|
|
2977
|
+
PatternAnalyzer.fromEnv().analyzeAndSave().catch(() => {
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
1872
2981
|
return {
|
|
1873
2982
|
content: [{
|
|
1874
2983
|
type: "text",
|
|
@@ -1951,6 +3060,27 @@ server.tool(
|
|
|
1951
3060
|
};
|
|
1952
3061
|
}
|
|
1953
3062
|
);
|
|
3063
|
+
server.tool(
|
|
3064
|
+
"kairos_patterns",
|
|
3065
|
+
"Analyze telemetry data and return failure patterns, build stats, and credential breakdowns. Useful for understanding what goes wrong most often and how to prevent it.",
|
|
3066
|
+
{
|
|
3067
|
+
days: import_zod.z.number().default(30).describe("Number of days of telemetry to analyze"),
|
|
3068
|
+
limit: import_zod.z.number().optional().describe("Maximum number of failure patterns to return")
|
|
3069
|
+
},
|
|
3070
|
+
async ({ days, limit }) => {
|
|
3071
|
+
const analyzer = PatternAnalyzer.fromEnv();
|
|
3072
|
+
const analysis = await analyzer.analyzeAndSave(days);
|
|
3073
|
+
if (limit !== void 0 && limit > 0) {
|
|
3074
|
+
analysis.topFailureRules = analysis.topFailureRules.slice(0, limit);
|
|
3075
|
+
}
|
|
3076
|
+
return {
|
|
3077
|
+
content: [{
|
|
3078
|
+
type: "text",
|
|
3079
|
+
text: JSON.stringify(analysis, null, 2)
|
|
3080
|
+
}]
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
);
|
|
1954
3084
|
server.tool(
|
|
1955
3085
|
"kairos_list",
|
|
1956
3086
|
"List all workflows deployed on the connected n8n instance.",
|