@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/cli.cjs
CHANGED
|
@@ -524,17 +524,31 @@ var N8nValidator = class {
|
|
|
524
524
|
this.checkRule21(workflow, issues);
|
|
525
525
|
this.checkRule22(workflow, issues);
|
|
526
526
|
this.checkRule23(workflow, issues);
|
|
527
|
+
this.checkRule24(workflow, issues);
|
|
528
|
+
this.checkRule25(workflow, issues);
|
|
529
|
+
this.checkRule26(workflow, issues);
|
|
530
|
+
if (Array.isArray(workflow.nodes)) {
|
|
531
|
+
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
532
|
+
for (const issue of issues) {
|
|
533
|
+
if (issue.nodeId && !issue.nodeType) {
|
|
534
|
+
const nt = nodeById.get(issue.nodeId);
|
|
535
|
+
if (nt) issue.nodeType = nt;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
527
539
|
const errors = issues.filter((i) => i.severity === "error");
|
|
528
540
|
return { valid: errors.length === 0, issues };
|
|
529
541
|
}
|
|
530
|
-
err(issues, rule, message, nodeId) {
|
|
542
|
+
err(issues, rule, message, nodeId, nodeType) {
|
|
531
543
|
const issue = { rule, severity: "error", message };
|
|
532
544
|
if (nodeId !== void 0) issue.nodeId = nodeId;
|
|
545
|
+
if (nodeType !== void 0) issue.nodeType = nodeType;
|
|
533
546
|
issues.push(issue);
|
|
534
547
|
}
|
|
535
|
-
warn(issues, rule, message, nodeId) {
|
|
548
|
+
warn(issues, rule, message, nodeId, nodeType) {
|
|
536
549
|
const issue = { rule, severity: "warn", message };
|
|
537
550
|
if (nodeId !== void 0) issue.nodeId = nodeId;
|
|
551
|
+
if (nodeType !== void 0) issue.nodeType = nodeType;
|
|
538
552
|
issues.push(issue);
|
|
539
553
|
}
|
|
540
554
|
isTriggerNode(node) {
|
|
@@ -645,10 +659,14 @@ var N8nValidator = class {
|
|
|
645
659
|
checkRule11(w, issues) {
|
|
646
660
|
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
647
661
|
const reachable = /* @__PURE__ */ new Set();
|
|
648
|
-
|
|
662
|
+
const aiSubNodeSources = /* @__PURE__ */ new Set();
|
|
663
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
649
664
|
if (typeof outputs !== "object" || outputs === null) continue;
|
|
650
|
-
|
|
665
|
+
let hasAiPort = false;
|
|
666
|
+
for (const [portName, portGroup] of Object.entries(outputs)) {
|
|
651
667
|
if (!Array.isArray(portGroup)) continue;
|
|
668
|
+
const isAiPort = portName.startsWith("ai_");
|
|
669
|
+
if (isAiPort) hasAiPort = true;
|
|
652
670
|
for (const targets of portGroup) {
|
|
653
671
|
if (!Array.isArray(targets)) continue;
|
|
654
672
|
for (const target of targets) {
|
|
@@ -657,10 +675,13 @@ var N8nValidator = class {
|
|
|
657
675
|
}
|
|
658
676
|
}
|
|
659
677
|
}
|
|
678
|
+
if (hasAiPort) aiSubNodeSources.add(sourceName);
|
|
660
679
|
}
|
|
661
680
|
for (const node of w.nodes) {
|
|
662
681
|
if (node.type.includes("stickyNote")) continue;
|
|
663
|
-
if (
|
|
682
|
+
if (this.isTriggerNode(node)) continue;
|
|
683
|
+
if (aiSubNodeSources.has(node.name)) continue;
|
|
684
|
+
if (!reachable.has(node.name)) {
|
|
664
685
|
this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
|
|
665
686
|
}
|
|
666
687
|
}
|
|
@@ -856,6 +877,76 @@ var N8nValidator = class {
|
|
|
856
877
|
}
|
|
857
878
|
}
|
|
858
879
|
}
|
|
880
|
+
// Rule 24 (WARN): deprecated accessor syntax in expressions
|
|
881
|
+
checkRule24(w, issues) {
|
|
882
|
+
if (!Array.isArray(w.nodes)) return;
|
|
883
|
+
const deprecated = /\$node\s*\[/;
|
|
884
|
+
for (const node of w.nodes) {
|
|
885
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
886
|
+
if (deprecated.test(expr)) {
|
|
887
|
+
this.warn(
|
|
888
|
+
issues,
|
|
889
|
+
24,
|
|
890
|
+
`Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
|
|
891
|
+
node.id
|
|
892
|
+
);
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
// Rule 25 (WARN): wrong item index assumptions in expressions
|
|
899
|
+
checkRule25(w, issues) {
|
|
900
|
+
if (!Array.isArray(w.nodes)) return;
|
|
901
|
+
const itemIndex = /\$json\s*\.\s*items\s*\[/;
|
|
902
|
+
for (const node of w.nodes) {
|
|
903
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
904
|
+
if (itemIndex.test(expr)) {
|
|
905
|
+
this.warn(
|
|
906
|
+
issues,
|
|
907
|
+
25,
|
|
908
|
+
`Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
|
|
909
|
+
node.id
|
|
910
|
+
);
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// Rule 26 (WARN): missing .first() or .all() on node references
|
|
917
|
+
checkRule26(w, issues) {
|
|
918
|
+
if (!Array.isArray(w.nodes)) return;
|
|
919
|
+
const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
|
|
920
|
+
for (const node of w.nodes) {
|
|
921
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
922
|
+
if (bareRef.test(expr)) {
|
|
923
|
+
this.warn(
|
|
924
|
+
issues,
|
|
925
|
+
26,
|
|
926
|
+
`Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
|
|
927
|
+
node.id
|
|
928
|
+
);
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
extractExpressions(params) {
|
|
935
|
+
const expressions = [];
|
|
936
|
+
const walk = (val) => {
|
|
937
|
+
if (typeof val === "string") {
|
|
938
|
+
if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
|
|
939
|
+
expressions.push(val);
|
|
940
|
+
}
|
|
941
|
+
} else if (Array.isArray(val)) {
|
|
942
|
+
for (const item of val) walk(item);
|
|
943
|
+
} else if (val !== null && typeof val === "object") {
|
|
944
|
+
for (const v of Object.values(val)) walk(v);
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
walk(params);
|
|
948
|
+
return expressions;
|
|
949
|
+
}
|
|
859
950
|
// Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
|
|
860
951
|
checkRule21(w, issues) {
|
|
861
952
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -897,14 +988,23 @@ var ResponseParseError = class extends KairosError {
|
|
|
897
988
|
|
|
898
989
|
// src/errors/validation-error.ts
|
|
899
990
|
var ValidationError = class extends KairosError {
|
|
900
|
-
constructor(message, issues) {
|
|
991
|
+
constructor(message, issues, attemptMetadata, warnedRules) {
|
|
901
992
|
super(message);
|
|
902
993
|
this.issues = issues;
|
|
994
|
+
this.attemptMetadata = attemptMetadata;
|
|
995
|
+
this.warnedRules = warnedRules;
|
|
903
996
|
this.name = "ValidationError";
|
|
904
997
|
}
|
|
905
998
|
issues;
|
|
999
|
+
attemptMetadata;
|
|
1000
|
+
warnedRules;
|
|
906
1001
|
};
|
|
907
1002
|
|
|
1003
|
+
// src/generation/prompt-builder.ts
|
|
1004
|
+
var import_node_fs = require("fs");
|
|
1005
|
+
var import_node_path = require("path");
|
|
1006
|
+
var import_node_os = require("os");
|
|
1007
|
+
|
|
908
1008
|
// src/generation/prompts/v1.ts
|
|
909
1009
|
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.
|
|
910
1010
|
|
|
@@ -934,9 +1034,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
|
|
|
934
1034
|
- Never reuse IDs, never use sequential fake IDs like "node-1"
|
|
935
1035
|
|
|
936
1036
|
### Credentials:
|
|
937
|
-
-
|
|
938
|
-
|
|
939
|
-
-
|
|
1037
|
+
- Each credential is keyed by its type string, with an object value containing id and name:
|
|
1038
|
+
"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack Credential" } }
|
|
1039
|
+
- Use "placeholder-id" as the id \u2014 users replace this with their real credential ID from n8n after deployment
|
|
1040
|
+
- The credentialsNeeded field in your response declares what credentials the user must configure
|
|
1041
|
+
- Never put API keys or tokens directly in node parameters when a credential type exists
|
|
940
1042
|
|
|
941
1043
|
### Node names:
|
|
942
1044
|
- All node names must be unique within the workflow
|
|
@@ -983,6 +1085,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
|
|
|
983
1085
|
|
|
984
1086
|
---
|
|
985
1087
|
|
|
1088
|
+
## EXPRESSION SYNTAX \u2014 how to reference upstream node data
|
|
1089
|
+
|
|
1090
|
+
### Accessing a field from an upstream node:
|
|
1091
|
+
- CORRECT: $('NodeName').item.json.field
|
|
1092
|
+
- WRONG: $node["NodeName"].json.field \u2190 deprecated accessor, fails at runtime (Rule 24)
|
|
1093
|
+
|
|
1094
|
+
### Accessing array items from $json:
|
|
1095
|
+
- CORRECT: $json.field \u2190 n8n auto-flattens items; each item is already a flat object
|
|
1096
|
+
- WRONG: $json.items[0].field \u2190 do not index into items[] (Rule 25)
|
|
1097
|
+
|
|
1098
|
+
### Calling node data \u2014 always qualify with .first() or .all():
|
|
1099
|
+
- CORRECT: $('NodeName').first().json.field \u2190 single item
|
|
1100
|
+
- CORRECT: $('NodeName').all() \u2190 array of all items
|
|
1101
|
+
- WRONG: $('NodeName').json \u2190 throws at runtime without .first() or .all() (Rule 26)
|
|
1102
|
+
|
|
1103
|
+
---
|
|
1104
|
+
|
|
986
1105
|
## NODE CATALOG \u2014 exact type strings and safe typeVersions
|
|
987
1106
|
|
|
988
1107
|
### Triggers (always at least one required):
|
|
@@ -1082,6 +1201,9 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
|
|
|
1082
1201
|
5. At least one trigger node present
|
|
1083
1202
|
6. Every AI Agent has an ai_languageModel sub-node
|
|
1084
1203
|
7. settings block is complete with executionOrder: "v1"
|
|
1204
|
+
8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
|
|
1205
|
+
9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
|
|
1206
|
+
10. No bare $('NodeName').json \u2014 always use .first().json.field or .all()
|
|
1085
1207
|
|
|
1086
1208
|
---
|
|
1087
1209
|
|
|
@@ -1097,8 +1219,55 @@ function scoreToMode(score) {
|
|
|
1097
1219
|
return "scratch";
|
|
1098
1220
|
}
|
|
1099
1221
|
|
|
1100
|
-
// src/
|
|
1101
|
-
var
|
|
1222
|
+
// src/validation/rule-metadata.ts
|
|
1223
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 26 }, (_, i) => i + 1);
|
|
1224
|
+
var RULE_PIPELINE_STAGES = {
|
|
1225
|
+
1: "node_generation",
|
|
1226
|
+
2: "node_generation",
|
|
1227
|
+
3: "node_generation",
|
|
1228
|
+
4: "node_generation",
|
|
1229
|
+
5: "node_generation",
|
|
1230
|
+
6: "node_generation",
|
|
1231
|
+
7: "node_generation",
|
|
1232
|
+
8: "node_generation",
|
|
1233
|
+
9: "connection_wiring",
|
|
1234
|
+
10: "connection_wiring",
|
|
1235
|
+
11: "connection_wiring",
|
|
1236
|
+
12: "workflow_structure",
|
|
1237
|
+
13: "node_generation",
|
|
1238
|
+
14: "workflow_structure",
|
|
1239
|
+
15: "node_generation",
|
|
1240
|
+
16: "node_generation",
|
|
1241
|
+
17: "credential_injection",
|
|
1242
|
+
18: "connection_wiring",
|
|
1243
|
+
19: "node_generation",
|
|
1244
|
+
20: "connection_wiring",
|
|
1245
|
+
21: "workflow_structure",
|
|
1246
|
+
22: "workflow_structure",
|
|
1247
|
+
23: "node_generation",
|
|
1248
|
+
24: "expression_syntax",
|
|
1249
|
+
25: "expression_syntax",
|
|
1250
|
+
26: "expression_syntax"
|
|
1251
|
+
};
|
|
1252
|
+
var RULE_EXAMPLES = {
|
|
1253
|
+
17: {
|
|
1254
|
+
bad: '"credentials": { "slackOAuth2Api": "my-token" }',
|
|
1255
|
+
good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
|
|
1256
|
+
},
|
|
1257
|
+
24: {
|
|
1258
|
+
bad: '$node["Fetch Data"].json.email',
|
|
1259
|
+
good: "$('Fetch Data').item.json.email"
|
|
1260
|
+
},
|
|
1261
|
+
25: {
|
|
1262
|
+
bad: "$json.items[0].email",
|
|
1263
|
+
good: "$json.email"
|
|
1264
|
+
},
|
|
1265
|
+
26: {
|
|
1266
|
+
bad: "$('Fetch Data').json.email",
|
|
1267
|
+
good: "$('Fetch Data').first().json.email"
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
var RULE_MITIGATIONS = {
|
|
1102
1271
|
1: "Provide a non-empty workflow name string",
|
|
1103
1272
|
2: "Include at least one node in the nodes array",
|
|
1104
1273
|
3: "Every node must have a unique UUID v4 string as its id field",
|
|
@@ -1109,18 +1278,51 @@ var RULE_REMEDIES = {
|
|
|
1109
1278
|
8: "Every node must have a non-empty name string",
|
|
1110
1279
|
9: "connections must be a plain object (use {} if no connections)",
|
|
1111
1280
|
10: "Every node name in connections (source and target) must exactly match a name in the nodes array",
|
|
1281
|
+
11: "Every non-trigger node should have at least one incoming connection",
|
|
1112
1282
|
12: "Remove forbidden fields: id, active, createdAt, updatedAt, versionId, meta, tags \u2014 these are server-assigned",
|
|
1113
|
-
|
|
1283
|
+
13: "workflow.settings must be a plain object if present",
|
|
1284
|
+
14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
|
|
1114
1285
|
15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
|
|
1115
1286
|
16: "All node names must be unique within the workflow",
|
|
1116
|
-
17: '
|
|
1287
|
+
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',
|
|
1117
1288
|
18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
|
|
1118
1289
|
19: "Use known safe typeVersion values for each node type",
|
|
1119
1290
|
20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
|
|
1120
1291
|
21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
|
|
1121
|
-
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)"
|
|
1292
|
+
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
|
|
1293
|
+
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
1294
|
+
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
1295
|
+
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
1296
|
+
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
|
|
1122
1297
|
};
|
|
1298
|
+
|
|
1299
|
+
// src/generation/prompt-builder.ts
|
|
1300
|
+
var CRITICAL_SCORE_THRESHOLD = 0.15;
|
|
1301
|
+
function resolveProfile() {
|
|
1302
|
+
const env = process.env["KAIROS_PROMPT_PROFILE"];
|
|
1303
|
+
if (env === "minimal" || env === "standard" || env === "rich") return env;
|
|
1304
|
+
return "standard";
|
|
1305
|
+
}
|
|
1306
|
+
var PROACTIVE_EXPRESSION_GUIDANCE = `## Expression Syntax Quick Reference
|
|
1307
|
+
|
|
1308
|
+
Always use these patterns in expressions:
|
|
1309
|
+
- Access node data: $('NodeName').item.json.field (not $node["NodeName"].json)
|
|
1310
|
+
- Access JSON field: $json.field (not $json.items[0].field)
|
|
1311
|
+
- Single item: $('NodeName').first().json.field
|
|
1312
|
+
- All items: $('NodeName').all()`;
|
|
1123
1313
|
var PromptBuilder = class {
|
|
1314
|
+
patternsPath;
|
|
1315
|
+
profile;
|
|
1316
|
+
_lastActivePatterns = null;
|
|
1317
|
+
constructor(patternsPath, profile) {
|
|
1318
|
+
this.patternsPath = patternsPath ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "patterns.json");
|
|
1319
|
+
this.profile = profile ?? resolveProfile();
|
|
1320
|
+
}
|
|
1321
|
+
resolveMaxPatterns() {
|
|
1322
|
+
if (this.profile === "minimal") return 3;
|
|
1323
|
+
if (this.profile === "rich") return 15;
|
|
1324
|
+
return 10;
|
|
1325
|
+
}
|
|
1124
1326
|
build(request, matches, globalFailureRates = [], dynamicCatalog) {
|
|
1125
1327
|
const mode = this.resolveMode(matches);
|
|
1126
1328
|
const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
|
|
@@ -1157,69 +1359,178 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1157
1359
|
cache_control: { type: "ephemeral" }
|
|
1158
1360
|
}
|
|
1159
1361
|
];
|
|
1160
|
-
if (
|
|
1161
|
-
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1362
|
+
if (this.profile !== "minimal") {
|
|
1363
|
+
if (mode === "reference" && matches.length > 0) {
|
|
1364
|
+
const refText = matches.slice(0, 3).map((m) => {
|
|
1365
|
+
const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1366
|
+
return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
|
|
1164
1367
|
Nodes:
|
|
1165
1368
|
${nodes}`;
|
|
1166
|
-
|
|
1167
|
-
blocks.push({
|
|
1168
|
-
type: "text",
|
|
1169
|
-
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1170
|
-
|
|
1171
|
-
${refText}`
|
|
1172
|
-
});
|
|
1173
|
-
}
|
|
1174
|
-
if (mode === "direct" && matches[0]) {
|
|
1175
|
-
const match = matches[0];
|
|
1176
|
-
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1177
|
-
if (json.length > 3e4) {
|
|
1178
|
-
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1369
|
+
}).join("\n\n");
|
|
1179
1370
|
blocks.push({
|
|
1180
1371
|
type: "text",
|
|
1181
|
-
text: `##
|
|
1372
|
+
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1373
|
+
|
|
1374
|
+
${refText}`
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
if (mode === "direct" && matches[0]) {
|
|
1378
|
+
const match = matches[0];
|
|
1379
|
+
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1380
|
+
if (json.length > 3e4) {
|
|
1381
|
+
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1382
|
+
blocks.push({
|
|
1383
|
+
type: "text",
|
|
1384
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
|
|
1182
1385
|
Nodes:
|
|
1183
1386
|
${nodes}`
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1387
|
+
});
|
|
1388
|
+
} else {
|
|
1389
|
+
blocks.push({
|
|
1390
|
+
type: "text",
|
|
1391
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
|
|
1189
1392
|
|
|
1190
1393
|
${json}`
|
|
1191
|
-
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1192
1396
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
text: `## Weak Structural Hint
|
|
1397
|
+
if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
|
|
1398
|
+
const hint = matches[0];
|
|
1399
|
+
const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
|
|
1400
|
+
blocks.push({
|
|
1401
|
+
type: "text",
|
|
1402
|
+
text: `## Weak Structural Hint
|
|
1200
1403
|
A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
|
|
1201
|
-
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1202
1406
|
}
|
|
1203
1407
|
const warnings = this.buildFailureWarnings(matches, globalFailureRates);
|
|
1204
1408
|
if (warnings) {
|
|
1205
1409
|
blocks.push({ type: "text", text: warnings });
|
|
1206
1410
|
}
|
|
1411
|
+
if (this.profile === "rich") {
|
|
1412
|
+
const expressionRules = /* @__PURE__ */ new Set([24, 25, 26]);
|
|
1413
|
+
const expressionAlreadyCovered = (this._lastActivePatterns ?? []).some((p) => expressionRules.has(p.rule));
|
|
1414
|
+
if (!expressionAlreadyCovered) {
|
|
1415
|
+
blocks.push({ type: "text", text: PROACTIVE_EXPRESSION_GUIDANCE });
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1207
1418
|
return blocks;
|
|
1208
1419
|
}
|
|
1420
|
+
loadPatterns() {
|
|
1421
|
+
try {
|
|
1422
|
+
const raw = (0, import_node_fs.readFileSync)(this.patternsPath, "utf-8");
|
|
1423
|
+
const analysis = JSON.parse(raw);
|
|
1424
|
+
const patterns = analysis.topFailureRules ?? [];
|
|
1425
|
+
return patterns.filter((p) => typeof p.pipelineStage === "string" && typeof p.state === "string");
|
|
1426
|
+
} catch {
|
|
1427
|
+
return [];
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
getWarnedRules() {
|
|
1431
|
+
const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
|
|
1432
|
+
return patterns.map((p) => p.rule);
|
|
1433
|
+
}
|
|
1434
|
+
getActivePatterns(maxCount = 10) {
|
|
1435
|
+
const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
|
|
1436
|
+
const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1437
|
+
const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1438
|
+
const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1439
|
+
return [...regressed, ...confirmed, ...drafts].slice(0, maxCount);
|
|
1440
|
+
}
|
|
1209
1441
|
buildFailureWarnings(matches, globalFailureRates) {
|
|
1442
|
+
const richPatterns = this.getActivePatterns(this.resolveMaxPatterns());
|
|
1443
|
+
this._lastActivePatterns = richPatterns;
|
|
1444
|
+
if (richPatterns.length > 0) {
|
|
1445
|
+
return this.buildStageGroupedWarnings(richPatterns, matches);
|
|
1446
|
+
}
|
|
1447
|
+
return this.buildLegacyWarnings(matches, globalFailureRates);
|
|
1448
|
+
}
|
|
1449
|
+
buildStageGroupedWarnings(patterns, matches) {
|
|
1450
|
+
const stageLabels = {
|
|
1451
|
+
credential_injection: "CREDENTIAL FORMATTING",
|
|
1452
|
+
connection_wiring: "CONNECTION WIRING",
|
|
1453
|
+
node_generation: "NODE GENERATION",
|
|
1454
|
+
workflow_structure: "WORKFLOW STRUCTURE",
|
|
1455
|
+
expression_syntax: "EXPRESSION SYNTAX"
|
|
1456
|
+
};
|
|
1457
|
+
const byStage = /* @__PURE__ */ new Map();
|
|
1458
|
+
for (const p of patterns) {
|
|
1459
|
+
const list = byStage.get(p.pipelineStage) ?? [];
|
|
1460
|
+
list.push(p);
|
|
1461
|
+
byStage.set(p.pipelineStage, list);
|
|
1462
|
+
}
|
|
1463
|
+
const sections = [];
|
|
1464
|
+
for (const [stage, stagePatterns] of byStage) {
|
|
1465
|
+
const label = stageLabels[stage] ?? stage;
|
|
1466
|
+
const byMitigation = /* @__PURE__ */ new Map();
|
|
1467
|
+
for (const p of stagePatterns) {
|
|
1468
|
+
const key = p.mitigation ?? `rule_${p.rule}`;
|
|
1469
|
+
const list = byMitigation.get(key) ?? [];
|
|
1470
|
+
list.push(p);
|
|
1471
|
+
byMitigation.set(key, list);
|
|
1472
|
+
}
|
|
1473
|
+
const lines = [];
|
|
1474
|
+
for (const group of byMitigation.values()) {
|
|
1475
|
+
if (group.length === 1) {
|
|
1476
|
+
const p = group[0];
|
|
1477
|
+
const urgency = p.regressed ? "CRITICAL REGRESSION: " : (p.compositeScore ?? 0) >= CRITICAL_SCORE_THRESHOLD ? "CRITICAL: " : "";
|
|
1478
|
+
const statePrefix = p.state === "confirmed" ? "[CONFIRMED] " : "";
|
|
1479
|
+
const trendSuffix = p.trend === "worsening" ? " (GETTING WORSE)" : p.trend === "improving" ? " (improving)" : "";
|
|
1480
|
+
const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
|
|
1481
|
+
const remedyStr = remedy ? `
|
|
1482
|
+
Fix: ${remedy}` : "";
|
|
1483
|
+
const ex = RULE_EXAMPLES[p.rule];
|
|
1484
|
+
const exampleStr = ex ? `
|
|
1485
|
+
Bad: ${ex.bad}
|
|
1486
|
+
Good: ${ex.good}` : "";
|
|
1487
|
+
lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}${exampleStr}`);
|
|
1488
|
+
} else {
|
|
1489
|
+
const ruleNums = group.map((p) => p.rule).join(", ");
|
|
1490
|
+
const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
|
|
1491
|
+
const hasConfirmed = group.some((p) => p.state === "confirmed");
|
|
1492
|
+
const statePrefix = hasConfirmed ? "[CONFIRMED] " : "";
|
|
1493
|
+
const remedy = group[0].mitigation;
|
|
1494
|
+
const remedyStr = remedy ? `
|
|
1495
|
+
Fix: ${remedy}` : "";
|
|
1496
|
+
lines.push(`- ${statePrefix}Rules ${ruleNums} (${totalFailures} failures combined): same root cause${remedyStr}`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
sections.push(`### ${label}
|
|
1500
|
+
${lines.join("\n")}`);
|
|
1501
|
+
}
|
|
1502
|
+
for (const match of matches) {
|
|
1503
|
+
const fps = match.workflow.failurePatterns;
|
|
1504
|
+
if (!fps?.length) continue;
|
|
1505
|
+
const coveredRules = new Set(patterns.map((p) => p.rule));
|
|
1506
|
+
const extra = fps.filter((fp) => !coveredRules.has(fp.rule));
|
|
1507
|
+
for (const fp of extra) {
|
|
1508
|
+
const remedy = RULE_MITIGATIONS[fp.rule];
|
|
1509
|
+
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1510
|
+
sections.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen in similar workflows)`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (sections.length === 0) return null;
|
|
1514
|
+
return `## Known Failure Patterns \u2014 AVOID THESE
|
|
1515
|
+
|
|
1516
|
+
Grouped by generation stage. Fix these BEFORE outputting your response:
|
|
1517
|
+
|
|
1518
|
+
${sections.join("\n\n")}`;
|
|
1519
|
+
}
|
|
1520
|
+
buildLegacyWarnings(matches, globalFailureRates) {
|
|
1210
1521
|
const lines = [];
|
|
1211
1522
|
for (const match of matches) {
|
|
1212
1523
|
const patterns = match.workflow.failurePatterns;
|
|
1213
1524
|
if (!patterns?.length) continue;
|
|
1214
1525
|
for (const fp of patterns) {
|
|
1215
|
-
const remedy =
|
|
1526
|
+
const remedy = RULE_MITIGATIONS[fp.rule];
|
|
1216
1527
|
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1217
1528
|
lines.push(`- Rule ${fp.rule}: "${fp.message}"${remedyStr} (seen ${fp.occurrences}x in similar workflows)`);
|
|
1218
1529
|
}
|
|
1219
1530
|
}
|
|
1220
1531
|
const highFreqRules = globalFailureRates.filter((r) => r.rate >= 0.15);
|
|
1221
1532
|
for (const rule of highFreqRules) {
|
|
1222
|
-
const remedy =
|
|
1533
|
+
const remedy = RULE_MITIGATIONS[rule.rule];
|
|
1223
1534
|
const remedyStr = remedy ? ` \u2014 Fix: ${remedy}` : "";
|
|
1224
1535
|
lines.push(`- Rule ${rule.rule}: "${rule.commonMessage}"${remedyStr} (fails in ${Math.round(rule.rate * 100)}% of all builds)`);
|
|
1225
1536
|
}
|
|
@@ -1280,12 +1591,12 @@ var GENERATE_WORKFLOW_TOOL = {
|
|
|
1280
1591
|
}
|
|
1281
1592
|
};
|
|
1282
1593
|
var WorkflowDesigner = class {
|
|
1283
|
-
constructor(anthropic, model, logger) {
|
|
1594
|
+
constructor(anthropic, model, logger, patternsPath) {
|
|
1284
1595
|
this.anthropic = anthropic;
|
|
1285
1596
|
this.model = model;
|
|
1286
1597
|
this.logger = logger;
|
|
1287
1598
|
this.validator = new N8nValidator();
|
|
1288
|
-
this.promptBuilder = new PromptBuilder();
|
|
1599
|
+
this.promptBuilder = new PromptBuilder(patternsPath);
|
|
1289
1600
|
}
|
|
1290
1601
|
anthropic;
|
|
1291
1602
|
model;
|
|
@@ -1330,7 +1641,7 @@ var WorkflowDesigner = class {
|
|
|
1330
1641
|
issues: validation.issues
|
|
1331
1642
|
});
|
|
1332
1643
|
if (validation.valid) {
|
|
1333
|
-
return { workflow: parsed.workflow, credentialsNeeded: parsed.credentialsNeeded, attempts, attemptMetadata };
|
|
1644
|
+
return { workflow: parsed.workflow, credentialsNeeded: parsed.credentialsNeeded, attempts, attemptMetadata, warnedRules: this.promptBuilder.getWarnedRules() };
|
|
1334
1645
|
}
|
|
1335
1646
|
lastErrors = errors;
|
|
1336
1647
|
this.logger.warn(`WorkflowDesigner: validation failed on attempt ${attempt}`, {
|
|
@@ -1340,7 +1651,9 @@ var WorkflowDesigner = class {
|
|
|
1340
1651
|
const finalIssues = attemptMetadata.at(-1)?.issues ?? lastErrors;
|
|
1341
1652
|
throw new ValidationError(
|
|
1342
1653
|
`Workflow failed validation after ${MAX_ATTEMPTS} attempts`,
|
|
1343
|
-
finalIssues
|
|
1654
|
+
finalIssues,
|
|
1655
|
+
attemptMetadata,
|
|
1656
|
+
this.promptBuilder.getWarnedRules()
|
|
1344
1657
|
);
|
|
1345
1658
|
}
|
|
1346
1659
|
async callClaude(system, userMessage, temperature) {
|
|
@@ -1367,6 +1680,11 @@ var WorkflowDesigner = class {
|
|
|
1367
1680
|
}
|
|
1368
1681
|
}
|
|
1369
1682
|
extractToolUse(message) {
|
|
1683
|
+
if (message.stop_reason === "max_tokens") {
|
|
1684
|
+
throw new GenerationError(
|
|
1685
|
+
"Claude response was truncated (max_tokens reached) \u2014 the workflow may be too large. Try a simpler description or break it into smaller workflows."
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1370
1688
|
const toolUseBlock = message.content.find(
|
|
1371
1689
|
(block) => block.type === "tool_use"
|
|
1372
1690
|
);
|
|
@@ -1394,8 +1712,8 @@ var WorkflowDesigner = class {
|
|
|
1394
1712
|
|
|
1395
1713
|
// src/telemetry/collector.ts
|
|
1396
1714
|
var import_promises = require("fs/promises");
|
|
1397
|
-
var
|
|
1398
|
-
var
|
|
1715
|
+
var import_node_path2 = require("path");
|
|
1716
|
+
var import_node_os2 = require("os");
|
|
1399
1717
|
|
|
1400
1718
|
// src/telemetry/types.ts
|
|
1401
1719
|
var TELEMETRY_SCHEMA_VERSION = 2;
|
|
@@ -1406,14 +1724,15 @@ var TelemetryCollector = class {
|
|
|
1406
1724
|
sessionId;
|
|
1407
1725
|
dirReady = null;
|
|
1408
1726
|
constructor(dir) {
|
|
1409
|
-
this.dir = dir ?? (0,
|
|
1727
|
+
this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
|
|
1410
1728
|
this.sessionId = generateUUID();
|
|
1411
1729
|
}
|
|
1412
|
-
async emit(eventType, data) {
|
|
1730
|
+
async emit(eventType, data, runId) {
|
|
1413
1731
|
const event = {
|
|
1414
1732
|
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
1415
1733
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1416
1734
|
sessionId: this.sessionId,
|
|
1735
|
+
...runId ? { runId } : {},
|
|
1417
1736
|
eventType,
|
|
1418
1737
|
data
|
|
1419
1738
|
};
|
|
@@ -1423,21 +1742,61 @@ var TelemetryCollector = class {
|
|
|
1423
1742
|
}
|
|
1424
1743
|
await this.dirReady;
|
|
1425
1744
|
const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
|
|
1426
|
-
const filepath = (0,
|
|
1745
|
+
const filepath = (0, import_node_path2.join)(this.dir, filename);
|
|
1427
1746
|
await (0, import_promises.appendFile)(filepath, JSON.stringify(event) + "\n", "utf-8");
|
|
1428
1747
|
}
|
|
1429
1748
|
};
|
|
1430
1749
|
|
|
1431
1750
|
// src/telemetry/reader.ts
|
|
1751
|
+
var import_node_os3 = require("os");
|
|
1752
|
+
var import_node_path4 = require("path");
|
|
1753
|
+
|
|
1754
|
+
// src/telemetry/event-reader.ts
|
|
1432
1755
|
var import_promises2 = require("fs/promises");
|
|
1433
|
-
var
|
|
1434
|
-
var
|
|
1756
|
+
var import_node_fs2 = require("fs");
|
|
1757
|
+
var import_node_path3 = require("path");
|
|
1758
|
+
var import_node_readline = require("readline");
|
|
1759
|
+
async function readTelemetryEvents(dir, days) {
|
|
1760
|
+
let files;
|
|
1761
|
+
try {
|
|
1762
|
+
files = await (0, import_promises2.readdir)(dir);
|
|
1763
|
+
} catch {
|
|
1764
|
+
return [];
|
|
1765
|
+
}
|
|
1766
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
1767
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
1768
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
1769
|
+
const todayStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1770
|
+
const datePattern = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
1771
|
+
const recentFiles = files.filter((f) => datePattern.test(f) && f >= cutoffStr && f <= `${todayStr}.jsonl`).sort();
|
|
1772
|
+
const events = [];
|
|
1773
|
+
for (const file of recentFiles) {
|
|
1774
|
+
const fileDate = file.replace(".jsonl", "");
|
|
1775
|
+
try {
|
|
1776
|
+
const rl = (0, import_node_readline.createInterface)({
|
|
1777
|
+
input: (0, import_node_fs2.createReadStream)((0, import_node_path3.join)(dir, file), "utf-8"),
|
|
1778
|
+
crlfDelay: Infinity
|
|
1779
|
+
});
|
|
1780
|
+
for await (const line of rl) {
|
|
1781
|
+
if (!line.trim()) continue;
|
|
1782
|
+
try {
|
|
1783
|
+
events.push({ ...JSON.parse(line), fileDate });
|
|
1784
|
+
} catch {
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
} catch {
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return events;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/telemetry/reader.ts
|
|
1435
1794
|
var TelemetryReader = class {
|
|
1436
1795
|
dir;
|
|
1437
1796
|
cache = null;
|
|
1438
1797
|
cacheTime = 0;
|
|
1439
1798
|
constructor(dir) {
|
|
1440
|
-
this.dir = dir ?? (0,
|
|
1799
|
+
this.dir = dir ?? (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".kairos", "telemetry");
|
|
1441
1800
|
}
|
|
1442
1801
|
async getFailureRates(days = 30) {
|
|
1443
1802
|
const now = Date.now();
|
|
@@ -1446,9 +1805,10 @@ var TelemetryReader = class {
|
|
|
1446
1805
|
}
|
|
1447
1806
|
const events = await this.readRecentEvents(days);
|
|
1448
1807
|
const buildSessions = new Set(
|
|
1449
|
-
events.filter((e) => e.eventType === "build_complete"
|
|
1808
|
+
events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
|
|
1450
1809
|
);
|
|
1451
|
-
|
|
1810
|
+
const MIN_BUILDS_FOR_RATES = 3;
|
|
1811
|
+
if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
|
|
1452
1812
|
const ruleSessions = /* @__PURE__ */ new Map();
|
|
1453
1813
|
for (const event of events) {
|
|
1454
1814
|
if (event.eventType !== "generation_attempt") continue;
|
|
@@ -1486,32 +1846,487 @@ var TelemetryReader = class {
|
|
|
1486
1846
|
return rates;
|
|
1487
1847
|
}
|
|
1488
1848
|
async readRecentEvents(days) {
|
|
1489
|
-
|
|
1849
|
+
return readTelemetryEvents(this.dir, days);
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
|
|
1853
|
+
// src/telemetry/pattern-analyzer.ts
|
|
1854
|
+
var import_promises3 = require("fs/promises");
|
|
1855
|
+
var import_node_path5 = require("path");
|
|
1856
|
+
var import_node_os4 = require("os");
|
|
1857
|
+
var PATTERN_SCHEMA_VERSION = 2;
|
|
1858
|
+
var PatternAnalyzer = class _PatternAnalyzer {
|
|
1859
|
+
telemetryDir;
|
|
1860
|
+
outputDir;
|
|
1861
|
+
_cachedEvents = null;
|
|
1862
|
+
constructor(telemetryDir) {
|
|
1863
|
+
const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
|
|
1864
|
+
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
1865
|
+
this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
|
|
1866
|
+
}
|
|
1867
|
+
async loadPreviousPatterns() {
|
|
1490
1868
|
try {
|
|
1491
|
-
|
|
1869
|
+
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
|
|
1870
|
+
const prev = JSON.parse(raw);
|
|
1871
|
+
const version = prev.schemaVersion ?? 0;
|
|
1872
|
+
const patterns = prev.topFailureRules ?? [];
|
|
1873
|
+
if (version === PATTERN_SCHEMA_VERSION) return patterns;
|
|
1874
|
+
return this.migratePatterns(patterns, version);
|
|
1492
1875
|
} catch {
|
|
1493
1876
|
return [];
|
|
1494
1877
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1878
|
+
}
|
|
1879
|
+
migratePatterns(patterns, fromVersion) {
|
|
1880
|
+
let migrated = patterns;
|
|
1881
|
+
if (fromVersion < 1) {
|
|
1882
|
+
migrated = migrated.map((p) => ({
|
|
1883
|
+
...p,
|
|
1884
|
+
compositeScore: p.compositeScore ?? 0,
|
|
1885
|
+
scoringFactors: p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
|
|
1886
|
+
pipelineStage: p.pipelineStage ?? "node_generation"
|
|
1887
|
+
}));
|
|
1888
|
+
}
|
|
1889
|
+
if (fromVersion < 2) {
|
|
1890
|
+
migrated = migrated.map((p) => {
|
|
1891
|
+
const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
|
|
1892
|
+
return {
|
|
1893
|
+
...p,
|
|
1894
|
+
scoringFactors: {
|
|
1895
|
+
...sf,
|
|
1896
|
+
stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
|
|
1509
1897
|
}
|
|
1898
|
+
};
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
return migrated;
|
|
1902
|
+
}
|
|
1903
|
+
async analyze(days = 30) {
|
|
1904
|
+
const previousPatterns = await this.loadPreviousPatterns();
|
|
1905
|
+
const events = await this.readAllEvents(days);
|
|
1906
|
+
this._cachedEvents = events;
|
|
1907
|
+
const starts = events.filter((e) => e.eventType === "build_start");
|
|
1908
|
+
const attempts = events.filter((e) => e.eventType === "generation_attempt");
|
|
1909
|
+
const passed = attempts.filter(
|
|
1910
|
+
(a) => a.data.validationPassed === true
|
|
1911
|
+
);
|
|
1912
|
+
const failed = attempts.filter(
|
|
1913
|
+
(a) => a.data.validationPassed === false
|
|
1914
|
+
);
|
|
1915
|
+
const ruleFailures = /* @__PURE__ */ new Map();
|
|
1916
|
+
const credentialFailures = /* @__PURE__ */ new Map();
|
|
1917
|
+
for (const a of failed) {
|
|
1918
|
+
const weight = this.recencyWeight(a.fileDate);
|
|
1919
|
+
const buildId = a.runId ?? a.sessionId;
|
|
1920
|
+
const data = a.data;
|
|
1921
|
+
for (const issue of data.issues ?? []) {
|
|
1922
|
+
if (issue.severity === "warn") continue;
|
|
1923
|
+
const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
|
|
1924
|
+
entry.count++;
|
|
1925
|
+
entry.sessions.add(buildId);
|
|
1926
|
+
entry.recencyWeights.push(weight);
|
|
1927
|
+
entry.allMessages.push(issue.message);
|
|
1928
|
+
if (data.workflowType) {
|
|
1929
|
+
entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
|
|
1930
|
+
}
|
|
1931
|
+
ruleFailures.set(issue.rule, entry);
|
|
1932
|
+
if (issue.rule === 17) {
|
|
1933
|
+
const credPatterns = [
|
|
1934
|
+
/credential\s+"([^"]+)"/,
|
|
1935
|
+
/credentialType[:\s]+"?([^"'\s]+)"?/,
|
|
1936
|
+
/missing\s+credential\s+(?:for\s+)?["']?([^"'\s]+)/i
|
|
1937
|
+
];
|
|
1938
|
+
let credType = "unknown";
|
|
1939
|
+
for (const re of credPatterns) {
|
|
1940
|
+
const m = issue.message.match(re);
|
|
1941
|
+
if (m?.[1]) {
|
|
1942
|
+
credType = m[1];
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
credentialFailures.set(credType, (credentialFailures.get(credType) ?? 0) + 1);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
const failedByDate = /* @__PURE__ */ new Map();
|
|
1951
|
+
for (const a of failed) {
|
|
1952
|
+
failedByDate.set(a.fileDate, (failedByDate.get(a.fileDate) ?? 0) + 1);
|
|
1953
|
+
}
|
|
1954
|
+
const sortedFailDates = [...failedByDate.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1955
|
+
const hasTrendData = sortedFailDates.length >= 3;
|
|
1956
|
+
let midDate = "";
|
|
1957
|
+
if (hasTrendData) {
|
|
1958
|
+
const halfTotal = failed.length / 2;
|
|
1959
|
+
let cumulative = 0;
|
|
1960
|
+
for (const [date, count] of sortedFailDates) {
|
|
1961
|
+
cumulative += count;
|
|
1962
|
+
if (cumulative >= halfTotal) {
|
|
1963
|
+
midDate = date;
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
const ruleTrends = /* @__PURE__ */ new Map();
|
|
1969
|
+
if (hasTrendData) {
|
|
1970
|
+
for (const a of failed) {
|
|
1971
|
+
const data = a.data;
|
|
1972
|
+
const isNewer = a.fileDate > midDate;
|
|
1973
|
+
for (const issue of data.issues ?? []) {
|
|
1974
|
+
const entry = ruleTrends.get(issue.rule) ?? { older: 0, newer: 0 };
|
|
1975
|
+
if (isNewer) entry.newer++;
|
|
1976
|
+
else entry.older++;
|
|
1977
|
+
ruleTrends.set(issue.rule, entry);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1982
|
+
for (const a of attempts) {
|
|
1983
|
+
const buildId = a.runId ?? a.sessionId;
|
|
1984
|
+
const list = sessions.get(buildId) ?? [];
|
|
1985
|
+
list.push(a);
|
|
1986
|
+
sessions.set(buildId, list);
|
|
1987
|
+
}
|
|
1988
|
+
let firstTryPass = 0;
|
|
1989
|
+
let correctionNeeded = 0;
|
|
1990
|
+
let singleAttemptFail = 0;
|
|
1991
|
+
for (const sessionAttempts of sessions.values()) {
|
|
1992
|
+
const lastAttempt = sessionAttempts[sessionAttempts.length - 1];
|
|
1993
|
+
const lastPassed = lastAttempt.data.validationPassed === true;
|
|
1994
|
+
if (sessionAttempts.length === 1 && lastPassed) {
|
|
1995
|
+
firstTryPass++;
|
|
1996
|
+
} else if (sessionAttempts.length > 1 && lastPassed) {
|
|
1997
|
+
correctionNeeded++;
|
|
1998
|
+
} else {
|
|
1999
|
+
singleAttemptFail++;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
const durations = attempts.map((a) => a.data.durationMs).filter((d) => typeof d === "number" && d > 0);
|
|
2003
|
+
const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
|
|
2004
|
+
const totalInput = attempts.reduce((s, a) => s + (a.data.tokensInput ?? 0), 0);
|
|
2005
|
+
const totalOutput = attempts.reduce((s, a) => s + (a.data.tokensOutput ?? 0), 0);
|
|
2006
|
+
const totalSessions = Math.max(sessions.size, 1);
|
|
2007
|
+
const stickinessCount = /* @__PURE__ */ new Map();
|
|
2008
|
+
for (const sessionAttempts of sessions.values()) {
|
|
2009
|
+
if (sessionAttempts.length < 2) continue;
|
|
2010
|
+
for (let i = 0; i < sessionAttempts.length - 1; i++) {
|
|
2011
|
+
const curr = sessionAttempts[i].data;
|
|
2012
|
+
const next = sessionAttempts[i + 1].data;
|
|
2013
|
+
if (curr.validationPassed !== false || next.validationPassed !== false) continue;
|
|
2014
|
+
const currRules = new Set((curr.issues ?? []).map((iss) => iss.rule));
|
|
2015
|
+
const nextRules = new Set((next.issues ?? []).map((iss) => iss.rule));
|
|
2016
|
+
for (const rule of currRules) {
|
|
2017
|
+
if (nextRules.has(rule)) {
|
|
2018
|
+
stickinessCount.set(rule, (stickinessCount.get(rule) ?? 0) + 1);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
const CONFIRMED_THRESHOLD = 3;
|
|
2024
|
+
const BUILDS_SINCE_LAST_FAILURE_THRESHOLD = 5;
|
|
2025
|
+
const RESOLVED_TTL_DAYS = 90;
|
|
2026
|
+
const activePatterns = [...ruleFailures.entries()].map(([rule, entry]) => {
|
|
2027
|
+
const t = ruleTrends.get(rule) ?? { older: 0, newer: 0 };
|
|
2028
|
+
const rawConfidence = Math.min(entry.sessions.size / totalSessions, 1);
|
|
2029
|
+
const state = entry.count >= CONFIRMED_THRESHOLD ? "confirmed" : "draft";
|
|
2030
|
+
const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
|
|
2031
|
+
const stickiness = stickinessCount.get(rule) ?? 0;
|
|
2032
|
+
const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
|
|
2033
|
+
const pattern = {
|
|
2034
|
+
rule,
|
|
2035
|
+
failureCount: entry.count,
|
|
2036
|
+
confidence: Math.round(rawConfidence * 1e3) / 1e3,
|
|
2037
|
+
compositeScore,
|
|
2038
|
+
scoringFactors: factors,
|
|
2039
|
+
state,
|
|
2040
|
+
trend: this.classifyTrend(t.older, t.newer),
|
|
2041
|
+
pipelineStage: RULE_PIPELINE_STAGES[rule] ?? "node_generation",
|
|
2042
|
+
exampleMessages: this.deduplicateMessages(entry.allMessages),
|
|
2043
|
+
mitigation: RULE_MITIGATIONS[rule] ?? null
|
|
2044
|
+
};
|
|
2045
|
+
if (entry.workflowTypes.size > 0) {
|
|
2046
|
+
pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
|
|
2047
|
+
}
|
|
2048
|
+
return pattern;
|
|
2049
|
+
}).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
2050
|
+
const activeRules = new Set(activePatterns.map((p) => p.rule));
|
|
2051
|
+
for (const p of activePatterns) {
|
|
2052
|
+
const prev = previousPatterns.find((pp) => pp.rule === p.rule);
|
|
2053
|
+
if (prev?.state === "resolved") {
|
|
2054
|
+
p.trend = "worsening";
|
|
2055
|
+
p.regressed = true;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
const ruleLastFailureDate = /* @__PURE__ */ new Map();
|
|
2059
|
+
for (const a of failed) {
|
|
2060
|
+
const data = a.data;
|
|
2061
|
+
for (const issue of data.issues ?? []) {
|
|
2062
|
+
const existing = ruleLastFailureDate.get(issue.rule);
|
|
2063
|
+
if (!existing || a.fileDate > existing) {
|
|
2064
|
+
ruleLastFailureDate.set(issue.rule, a.fileDate);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
const newlyResolved = previousPatterns.filter((p) => {
|
|
2069
|
+
if (p.state !== "confirmed" || activeRules.has(p.rule)) return false;
|
|
2070
|
+
const lastFailDate = ruleLastFailureDate.get(p.rule) ?? "";
|
|
2071
|
+
const buildsSince = starts.filter((s) => s.fileDate > lastFailDate).length;
|
|
2072
|
+
return buildsSince >= BUILDS_SINCE_LAST_FAILURE_THRESHOLD;
|
|
2073
|
+
}).map((p) => ({
|
|
2074
|
+
...p,
|
|
2075
|
+
state: "resolved",
|
|
2076
|
+
trend: "improving",
|
|
2077
|
+
pipelineStage: p.pipelineStage ?? RULE_PIPELINE_STAGES[p.rule] ?? "node_generation",
|
|
2078
|
+
confidence: 0,
|
|
2079
|
+
compositeScore: 0,
|
|
2080
|
+
scoringFactors: { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 },
|
|
2081
|
+
failureCount: 0,
|
|
2082
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2083
|
+
}));
|
|
2084
|
+
const ttlCutoff = /* @__PURE__ */ new Date();
|
|
2085
|
+
ttlCutoff.setDate(ttlCutoff.getDate() - RESOLVED_TTL_DAYS);
|
|
2086
|
+
const ttlCutoffStr = ttlCutoff.toISOString();
|
|
2087
|
+
const carriedResolved = previousPatterns.filter((p) => p.state === "resolved" && !activeRules.has(p.rule) && (!p.resolvedAt || p.resolvedAt >= ttlCutoffStr)).map((p) => ({ ...p }));
|
|
2088
|
+
const newlyResolvedRules = new Set(newlyResolved.map((p) => p.rule));
|
|
2089
|
+
const pendingResolution = previousPatterns.filter((p) => p.state === "confirmed" && !activeRules.has(p.rule) && !newlyResolvedRules.has(p.rule)).map((p) => ({ ...p }));
|
|
2090
|
+
const deduped = [
|
|
2091
|
+
...newlyResolved,
|
|
2092
|
+
...carriedResolved.filter((p) => !newlyResolvedRules.has(p.rule)),
|
|
2093
|
+
...pendingResolution
|
|
2094
|
+
];
|
|
2095
|
+
const patterns = [...activePatterns, ...deduped];
|
|
2096
|
+
const credTypes = [...credentialFailures.entries()].sort((a, b) => b[1] - a[1]).map(([type, count]) => ({ type, count }));
|
|
2097
|
+
const drift = this.detectDrift(patterns);
|
|
2098
|
+
const warnEffMap = /* @__PURE__ */ new Map();
|
|
2099
|
+
const buildCompletes = events.filter((e) => e.eventType === "build_complete");
|
|
2100
|
+
for (const bc of buildCompletes) {
|
|
2101
|
+
const bcData = bc.data;
|
|
2102
|
+
const warned = bcData.warnedRules ?? [];
|
|
2103
|
+
if (warned.length === 0) continue;
|
|
2104
|
+
const sessionFailedRules = /* @__PURE__ */ new Set();
|
|
2105
|
+
const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
|
|
2106
|
+
for (const a of sessionAttempts) {
|
|
2107
|
+
const ad = a.data;
|
|
2108
|
+
if (ad.validationPassed === false) {
|
|
2109
|
+
for (const issue of ad.issues ?? []) {
|
|
2110
|
+
sessionFailedRules.add(issue.rule);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
for (const rule of warned) {
|
|
2115
|
+
const entry = warnEffMap.get(rule) ?? { warned: 0, passed: 0, failed: 0 };
|
|
2116
|
+
entry.warned++;
|
|
2117
|
+
if (sessionFailedRules.has(rule)) entry.failed++;
|
|
2118
|
+
else entry.passed++;
|
|
2119
|
+
warnEffMap.set(rule, entry);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
const warningEffectiveness = [...warnEffMap.entries()].map(([rule, e]) => ({
|
|
2123
|
+
rule,
|
|
2124
|
+
timesWarned: e.warned,
|
|
2125
|
+
timesWarnedAndPassed: e.passed,
|
|
2126
|
+
timesWarnedAndFailed: e.failed,
|
|
2127
|
+
effectivenessRate: e.warned > 0 ? Math.round(e.passed / e.warned * 1e3) / 1e3 : 0
|
|
2128
|
+
})).sort((a, b) => b.timesWarned - a.timesWarned);
|
|
2129
|
+
const coOccurrenceMap = /* @__PURE__ */ new Map();
|
|
2130
|
+
for (const a of failed) {
|
|
2131
|
+
const data = a.data;
|
|
2132
|
+
const rules = [...new Set((data.issues ?? []).map((i) => i.rule))].sort((x, y) => x - y);
|
|
2133
|
+
for (let i = 0; i < rules.length; i++) {
|
|
2134
|
+
for (let j = i + 1; j < rules.length; j++) {
|
|
2135
|
+
const key = `${rules[i]},${rules[j]}`;
|
|
2136
|
+
coOccurrenceMap.set(key, (coOccurrenceMap.get(key) ?? 0) + 1);
|
|
1510
2137
|
}
|
|
1511
|
-
} catch {
|
|
1512
2138
|
}
|
|
1513
2139
|
}
|
|
1514
|
-
|
|
2140
|
+
const ruleCoOccurrence = [...coOccurrenceMap.entries()].filter(([, count]) => count >= 3).map(([key, count]) => {
|
|
2141
|
+
const [a, b] = key.split(",").map(Number);
|
|
2142
|
+
return { rules: [a, b], count };
|
|
2143
|
+
}).sort((a, b) => b.count - a.count);
|
|
2144
|
+
const attemptDistribution = {};
|
|
2145
|
+
for (const sessionAttempts of sessions.values()) {
|
|
2146
|
+
const depth = sessionAttempts.length;
|
|
2147
|
+
attemptDistribution[depth] = (attemptDistribution[depth] ?? 0) + 1;
|
|
2148
|
+
}
|
|
2149
|
+
return {
|
|
2150
|
+
schemaVersion: PATTERN_SCHEMA_VERSION,
|
|
2151
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2152
|
+
summary: {
|
|
2153
|
+
totalBuilds: starts.length,
|
|
2154
|
+
totalAttempts: attempts.length,
|
|
2155
|
+
firstTryPassRate: Math.round(firstTryPass / totalSessions * 1e3) / 1e3,
|
|
2156
|
+
correctionRate: Math.round(correctionNeeded / totalSessions * 1e3) / 1e3,
|
|
2157
|
+
singleAttemptFailRate: Math.round(singleAttemptFail / totalSessions * 1e3) / 1e3,
|
|
2158
|
+
avgDurationMs: Math.round(avgDuration),
|
|
2159
|
+
totalTokensInput: totalInput,
|
|
2160
|
+
totalTokensOutput: totalOutput,
|
|
2161
|
+
attemptDistribution
|
|
2162
|
+
},
|
|
2163
|
+
topFailureRules: patterns,
|
|
2164
|
+
failingCredentialTypes: credTypes,
|
|
2165
|
+
drift,
|
|
2166
|
+
warningEffectiveness,
|
|
2167
|
+
ruleCoOccurrence
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
async analyzeAndSave(days = 30) {
|
|
2171
|
+
const analysis = await this.analyze(days);
|
|
2172
|
+
await (0, import_promises3.mkdir)(this.outputDir, { recursive: true });
|
|
2173
|
+
const outputPath = (0, import_node_path5.join)(this.outputDir, "patterns.json");
|
|
2174
|
+
const tmpPath = `${outputPath}.tmp`;
|
|
2175
|
+
await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
|
|
2176
|
+
await (0, import_promises3.rename)(tmpPath, outputPath);
|
|
2177
|
+
const historySummary = {
|
|
2178
|
+
timestamp: analysis.generatedAt,
|
|
2179
|
+
totalBuilds: analysis.summary.totalBuilds,
|
|
2180
|
+
firstTryPassRate: analysis.summary.firstTryPassRate,
|
|
2181
|
+
correctionRate: analysis.summary.correctionRate,
|
|
2182
|
+
singleAttemptFailRate: analysis.summary.singleAttemptFailRate,
|
|
2183
|
+
activePatternCount: analysis.topFailureRules.filter((p) => p.state !== "resolved").length,
|
|
2184
|
+
topRules: analysis.topFailureRules.filter((p) => p.state !== "resolved").slice(0, 5).map((p) => ({ rule: p.rule, compositeScore: p.compositeScore, state: p.state }))
|
|
2185
|
+
};
|
|
2186
|
+
const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
|
|
2187
|
+
await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
|
|
2188
|
+
const sessions = await this.buildSessionSummaries(days);
|
|
2189
|
+
const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
|
|
2190
|
+
const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
|
|
2191
|
+
await (0, import_promises3.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
|
|
2192
|
+
await (0, import_promises3.rename)(sessionHistoryTmp, sessionHistoryPath);
|
|
2193
|
+
return analysis;
|
|
2194
|
+
}
|
|
2195
|
+
async getSessions(limit = 20) {
|
|
2196
|
+
try {
|
|
2197
|
+
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
|
|
2198
|
+
const all = JSON.parse(raw);
|
|
2199
|
+
return all.slice(-limit);
|
|
2200
|
+
} catch {
|
|
2201
|
+
return [];
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
async buildSessionSummaries(days = 30) {
|
|
2205
|
+
const events = this._cachedEvents ?? await this.readAllEvents(days);
|
|
2206
|
+
const buildCompletes = events.filter((e) => e.eventType === "build_complete");
|
|
2207
|
+
const attemptsByBuild = /* @__PURE__ */ new Map();
|
|
2208
|
+
for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
|
|
2209
|
+
const buildId = e.runId ?? e.sessionId;
|
|
2210
|
+
const list = attemptsByBuild.get(buildId) ?? [];
|
|
2211
|
+
list.push(e);
|
|
2212
|
+
attemptsByBuild.set(buildId, list);
|
|
2213
|
+
}
|
|
2214
|
+
const summaries = buildCompletes.map((bc) => {
|
|
2215
|
+
const data = bc.data;
|
|
2216
|
+
const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
|
|
2217
|
+
const failedRules = Array.from(new Set(
|
|
2218
|
+
sessionAttempts.flatMap((a) => {
|
|
2219
|
+
const ad = a.data;
|
|
2220
|
+
if (ad.validationPassed !== false) return [];
|
|
2221
|
+
return (ad.issues ?? []).map((i) => i.rule);
|
|
2222
|
+
})
|
|
2223
|
+
));
|
|
2224
|
+
return {
|
|
2225
|
+
sessionId: bc.sessionId,
|
|
2226
|
+
date: bc.fileDate,
|
|
2227
|
+
description: data.description ?? "",
|
|
2228
|
+
workflowType: data.workflowType ?? null,
|
|
2229
|
+
attempts: data.totalAttempts ?? 1,
|
|
2230
|
+
success: data.success ?? false,
|
|
2231
|
+
failedRules,
|
|
2232
|
+
workflowName: data.workflowName ?? null
|
|
2233
|
+
};
|
|
2234
|
+
});
|
|
2235
|
+
return summaries.sort((a, b) => a.date.localeCompare(b.date));
|
|
2236
|
+
}
|
|
2237
|
+
async getHistory(limit = 20) {
|
|
2238
|
+
try {
|
|
2239
|
+
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
|
|
2240
|
+
return raw.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)).slice(-limit);
|
|
2241
|
+
} catch {
|
|
2242
|
+
return [];
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
static fromEnv() {
|
|
2246
|
+
const dir = process.env["KAIROS_TELEMETRY"];
|
|
2247
|
+
return dir && dir !== "true" && dir !== "false" ? new _PatternAnalyzer(dir) : new _PatternAnalyzer();
|
|
2248
|
+
}
|
|
2249
|
+
detectDrift(patterns) {
|
|
2250
|
+
const VALIDATOR_RULES = VALIDATOR_RULE_IDS;
|
|
2251
|
+
const validatorRuleSet = new Set(VALIDATOR_RULES);
|
|
2252
|
+
const alerts = [];
|
|
2253
|
+
for (const p of patterns) {
|
|
2254
|
+
if (p.state !== "resolved" && !validatorRuleSet.has(p.rule)) {
|
|
2255
|
+
alerts.push({
|
|
2256
|
+
type: "stale_pattern",
|
|
2257
|
+
rule: p.rule,
|
|
2258
|
+
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-26)`
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
for (const rule of VALIDATOR_RULES) {
|
|
2263
|
+
if (!(rule in RULE_MITIGATIONS)) {
|
|
2264
|
+
alerts.push({
|
|
2265
|
+
type: "missing_mitigation",
|
|
2266
|
+
rule,
|
|
2267
|
+
message: `Rule ${rule} has no mitigation text \u2014 if it fails, the system can't advise the LLM how to fix it`
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
if (!(rule in RULE_PIPELINE_STAGES)) {
|
|
2271
|
+
alerts.push({
|
|
2272
|
+
type: "missing_stage_mapping",
|
|
2273
|
+
rule,
|
|
2274
|
+
message: `Rule ${rule} has no pipeline stage mapping \u2014 failures won't be grouped correctly`
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
const coveredRules = VALIDATOR_RULES.filter((r) => r in RULE_MITIGATIONS && r in RULE_PIPELINE_STAGES).length;
|
|
2279
|
+
return {
|
|
2280
|
+
healthy: alerts.length === 0,
|
|
2281
|
+
alerts,
|
|
2282
|
+
coveredRules,
|
|
2283
|
+
totalRules: VALIDATOR_RULES.length
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
computeCompositeScore(rawConfidence, sampleSize, state, avgRecency, stickiness) {
|
|
2287
|
+
const stateWeights = { draft: 0.3, confirmed: 0.8, resolved: 0.1 };
|
|
2288
|
+
const stateWeight = stateWeights[state];
|
|
2289
|
+
const impact = (1 - Math.exp(-sampleSize / 5)) * stateWeight;
|
|
2290
|
+
const stickinessBoost = Math.min(0.15, stickiness * 0.05);
|
|
2291
|
+
const compositeScore = Math.min(Math.round(rawConfidence * impact * avgRecency * (1 + stickinessBoost) * 1e3) / 1e3, 1);
|
|
2292
|
+
return {
|
|
2293
|
+
compositeScore,
|
|
2294
|
+
factors: {
|
|
2295
|
+
rawConfidence: Math.round(rawConfidence * 1e3) / 1e3,
|
|
2296
|
+
impact: Math.round(impact * 1e3) / 1e3,
|
|
2297
|
+
recency: Math.round(avgRecency * 1e3) / 1e3,
|
|
2298
|
+
stickinessBoost: Math.round(stickinessBoost * 1e3) / 1e3
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
classifyTrend(older, newer) {
|
|
2303
|
+
const total = older + newer;
|
|
2304
|
+
if (total === 0) return "stable";
|
|
2305
|
+
if (older === 0) return "new";
|
|
2306
|
+
const newerRatio = newer / total;
|
|
2307
|
+
if (newerRatio >= 0.65) return "worsening";
|
|
2308
|
+
if (newerRatio <= 0.35) return "improving";
|
|
2309
|
+
return "stable";
|
|
2310
|
+
}
|
|
2311
|
+
deduplicateMessages(messages, maxCount = 3) {
|
|
2312
|
+
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();
|
|
2313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2314
|
+
const unique = [];
|
|
2315
|
+
for (const msg of messages) {
|
|
2316
|
+
const key = normalize(msg);
|
|
2317
|
+
if (!seen.has(key) && unique.length < maxCount) {
|
|
2318
|
+
seen.add(key);
|
|
2319
|
+
unique.push(msg);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return unique;
|
|
2323
|
+
}
|
|
2324
|
+
recencyWeight(fileDate, halfLifeDays = 30) {
|
|
2325
|
+
const daysAgo = Math.max(0, (Date.now() - (/* @__PURE__ */ new Date(fileDate + "T12:00:00Z")).getTime()) / (1e3 * 60 * 60 * 24));
|
|
2326
|
+
return Math.max(0.1, Math.exp(-Math.LN2 * daysAgo / halfLifeDays));
|
|
2327
|
+
}
|
|
2328
|
+
async readAllEvents(days) {
|
|
2329
|
+
return readTelemetryEvents(this.telemetryDir, days);
|
|
1515
2330
|
}
|
|
1516
2331
|
};
|
|
1517
2332
|
|
|
@@ -1527,7 +2342,59 @@ var nullLogger = {
|
|
|
1527
2342
|
}
|
|
1528
2343
|
};
|
|
1529
2344
|
|
|
2345
|
+
// src/utils/workflow-type.ts
|
|
2346
|
+
var TYPE_KEYWORDS = [
|
|
2347
|
+
["gmail", "email"],
|
|
2348
|
+
["imap", "email"],
|
|
2349
|
+
["smtp", "email"],
|
|
2350
|
+
[" email", "email"],
|
|
2351
|
+
["slack", "slack"],
|
|
2352
|
+
["telegram", "messaging"],
|
|
2353
|
+
["discord", "messaging"],
|
|
2354
|
+
[" sms", "messaging"],
|
|
2355
|
+
["twilio", "messaging"],
|
|
2356
|
+
["webhook", "webhook"],
|
|
2357
|
+
["google sheets", "data"],
|
|
2358
|
+
["spreadsheet", "data"],
|
|
2359
|
+
["airtable", "data"],
|
|
2360
|
+
["notion", "data"],
|
|
2361
|
+
["github", "devops"],
|
|
2362
|
+
["gitlab", "devops"],
|
|
2363
|
+
["schedule", "schedule"],
|
|
2364
|
+
[" cron", "schedule"],
|
|
2365
|
+
["daily", "schedule"],
|
|
2366
|
+
["weekly", "schedule"],
|
|
2367
|
+
["hourly", "schedule"],
|
|
2368
|
+
["every day", "schedule"],
|
|
2369
|
+
["every hour", "schedule"],
|
|
2370
|
+
["every morning", "schedule"],
|
|
2371
|
+
["postgres", "database"],
|
|
2372
|
+
["mysql", "database"],
|
|
2373
|
+
["supabase", "database"],
|
|
2374
|
+
["redis", "database"],
|
|
2375
|
+
[" database", "database"],
|
|
2376
|
+
[" llm", "ai"],
|
|
2377
|
+
[" gpt", "ai"],
|
|
2378
|
+
["claude", "ai"],
|
|
2379
|
+
[" agent", "ai"],
|
|
2380
|
+
["langchain", "ai"],
|
|
2381
|
+
[" ai ", "ai"],
|
|
2382
|
+
[" ai", "ai"],
|
|
2383
|
+
["http request", "api"],
|
|
2384
|
+
["rest api", "api"],
|
|
2385
|
+
[" api", "api"]
|
|
2386
|
+
];
|
|
2387
|
+
function inferWorkflowType(description) {
|
|
2388
|
+
const lower = " " + description.toLowerCase();
|
|
2389
|
+
for (const [keyword, type] of TYPE_KEYWORDS) {
|
|
2390
|
+
if (lower.includes(keyword)) return type;
|
|
2391
|
+
}
|
|
2392
|
+
return null;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
1530
2395
|
// src/client.ts
|
|
2396
|
+
var import_node_os5 = require("os");
|
|
2397
|
+
var import_node_path6 = require("path");
|
|
1531
2398
|
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1532
2399
|
var Kairos = class {
|
|
1533
2400
|
provider;
|
|
@@ -1537,6 +2404,7 @@ var Kairos = class {
|
|
|
1537
2404
|
logger;
|
|
1538
2405
|
telemetry;
|
|
1539
2406
|
telemetryReader;
|
|
2407
|
+
patternAnalyzer;
|
|
1540
2408
|
model;
|
|
1541
2409
|
saveQueue = Promise.resolve(null);
|
|
1542
2410
|
constructor(options) {
|
|
@@ -1555,19 +2423,23 @@ var Kairos = class {
|
|
|
1555
2423
|
this.provider = null;
|
|
1556
2424
|
}
|
|
1557
2425
|
const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
|
|
1558
|
-
|
|
2426
|
+
const patternsPath = typeof options.telemetry === "string" ? (0, import_node_path6.join)(options.telemetry, "..", "patterns.json") : (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".kairos", "patterns.json");
|
|
2427
|
+
this.designer = new WorkflowDesigner(anthropic, this.model, logger, patternsPath);
|
|
1559
2428
|
this.validator = new N8nValidator();
|
|
1560
2429
|
this.library = options.library ?? new NullLibrary();
|
|
1561
2430
|
this.logger = logger;
|
|
1562
2431
|
if (options.telemetry === true) {
|
|
1563
2432
|
this.telemetry = new TelemetryCollector();
|
|
1564
2433
|
this.telemetryReader = new TelemetryReader();
|
|
2434
|
+
this.patternAnalyzer = new PatternAnalyzer();
|
|
1565
2435
|
} else if (typeof options.telemetry === "string") {
|
|
1566
2436
|
this.telemetry = new TelemetryCollector(options.telemetry);
|
|
1567
2437
|
this.telemetryReader = new TelemetryReader(options.telemetry);
|
|
2438
|
+
this.patternAnalyzer = new PatternAnalyzer(options.telemetry);
|
|
1568
2439
|
} else {
|
|
1569
2440
|
this.telemetry = null;
|
|
1570
2441
|
this.telemetryReader = null;
|
|
2442
|
+
this.patternAnalyzer = null;
|
|
1571
2443
|
}
|
|
1572
2444
|
}
|
|
1573
2445
|
requireProvider() {
|
|
@@ -1585,11 +2457,13 @@ var Kairos = class {
|
|
|
1585
2457
|
this.validateDescription(description);
|
|
1586
2458
|
this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
|
|
1587
2459
|
const buildStart = Date.now();
|
|
2460
|
+
const runId = generateUUID();
|
|
2461
|
+
const workflowType = inferWorkflowType(description);
|
|
1588
2462
|
await this.telemetry?.emit("build_start", {
|
|
1589
2463
|
description,
|
|
1590
2464
|
model: this.model,
|
|
1591
2465
|
dryRun: options?.dryRun ?? false
|
|
1592
|
-
});
|
|
2466
|
+
}, runId);
|
|
1593
2467
|
await this.library.initialize();
|
|
1594
2468
|
const matches = await this.library.search(description);
|
|
1595
2469
|
if (matches.length > 0) {
|
|
@@ -1605,12 +2479,48 @@ var Kairos = class {
|
|
|
1605
2479
|
this.logger.info(`Telemetry: ${highFreq.length} high-frequency failure rule(s) will be warned about`);
|
|
1606
2480
|
}
|
|
1607
2481
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
2482
|
+
let designResult;
|
|
2483
|
+
try {
|
|
2484
|
+
designResult = await this.designer.design(
|
|
2485
|
+
{ description, ...options?.name ? { name: options.name } : {} },
|
|
2486
|
+
matches,
|
|
2487
|
+
globalFailureRates
|
|
2488
|
+
);
|
|
2489
|
+
} catch (err) {
|
|
2490
|
+
if (err instanceof ValidationError && err.attemptMetadata) {
|
|
2491
|
+
for (const meta of err.attemptMetadata) {
|
|
2492
|
+
await this.telemetry?.emit("generation_attempt", {
|
|
2493
|
+
description,
|
|
2494
|
+
attempt: meta.attempt,
|
|
2495
|
+
temperature: meta.temperature,
|
|
2496
|
+
durationMs: meta.durationMs,
|
|
2497
|
+
tokensInput: meta.tokensInput,
|
|
2498
|
+
tokensOutput: meta.tokensOutput,
|
|
2499
|
+
validationPassed: meta.validationPassed,
|
|
2500
|
+
issueCount: meta.issues.length,
|
|
2501
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
2502
|
+
workflowType
|
|
2503
|
+
}, runId);
|
|
2504
|
+
}
|
|
2505
|
+
await this.telemetry?.emit("build_complete", {
|
|
2506
|
+
description,
|
|
2507
|
+
success: false,
|
|
2508
|
+
totalAttempts: err.attemptMetadata.length,
|
|
2509
|
+
totalDurationMs: Date.now() - buildStart,
|
|
2510
|
+
totalTokensInput: err.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0),
|
|
2511
|
+
totalTokensOutput: err.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0),
|
|
2512
|
+
workflowName: null,
|
|
2513
|
+
workflowId: null,
|
|
2514
|
+
dryRun: options?.dryRun ?? false,
|
|
2515
|
+
credentialsNeeded: 0,
|
|
2516
|
+
warnedRules: err.warnedRules ?? [],
|
|
2517
|
+
workflowType
|
|
2518
|
+
}, runId);
|
|
2519
|
+
this.updatePatterns();
|
|
2520
|
+
}
|
|
2521
|
+
throw err;
|
|
2522
|
+
}
|
|
2523
|
+
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
1614
2524
|
const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
|
|
1615
2525
|
this.saveToLibrary(workflow, description, designResult, matches);
|
|
1616
2526
|
if (options?.dryRun) {
|
|
@@ -1626,8 +2536,11 @@ var Kairos = class {
|
|
|
1626
2536
|
workflowName: workflow.name,
|
|
1627
2537
|
workflowId: null,
|
|
1628
2538
|
dryRun: true,
|
|
1629
|
-
credentialsNeeded: designResult.credentialsNeeded.length
|
|
1630
|
-
|
|
2539
|
+
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2540
|
+
warnedRules: designResult.warnedRules,
|
|
2541
|
+
workflowType
|
|
2542
|
+
}, runId);
|
|
2543
|
+
this.updatePatterns();
|
|
1631
2544
|
return {
|
|
1632
2545
|
workflowId: null,
|
|
1633
2546
|
name: workflow.name,
|
|
@@ -1656,8 +2569,11 @@ var Kairos = class {
|
|
|
1656
2569
|
workflowName: deployed.name,
|
|
1657
2570
|
workflowId: deployed.workflowId,
|
|
1658
2571
|
dryRun: false,
|
|
1659
|
-
credentialsNeeded: designResult.credentialsNeeded.length
|
|
1660
|
-
|
|
2572
|
+
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2573
|
+
warnedRules: designResult.warnedRules,
|
|
2574
|
+
workflowType
|
|
2575
|
+
}, runId);
|
|
2576
|
+
this.updatePatterns();
|
|
1661
2577
|
return {
|
|
1662
2578
|
workflowId: deployed.workflowId,
|
|
1663
2579
|
name: deployed.name,
|
|
@@ -1672,16 +2588,54 @@ var Kairos = class {
|
|
|
1672
2588
|
this.validateDescription(description);
|
|
1673
2589
|
this.logger.info("Kairos.update", { id, description });
|
|
1674
2590
|
const buildStart = Date.now();
|
|
2591
|
+
const runId = generateUUID();
|
|
2592
|
+
const workflowType = inferWorkflowType(description);
|
|
1675
2593
|
await this.telemetry?.emit("build_start", {
|
|
1676
2594
|
description,
|
|
1677
2595
|
model: this.model,
|
|
1678
2596
|
dryRun: false
|
|
1679
|
-
});
|
|
2597
|
+
}, runId);
|
|
1680
2598
|
await this.library.initialize();
|
|
1681
2599
|
const matches = await this.library.search(description);
|
|
1682
2600
|
const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
|
|
1683
|
-
|
|
1684
|
-
|
|
2601
|
+
let designResult;
|
|
2602
|
+
try {
|
|
2603
|
+
designResult = await this.designer.design({ description }, matches, globalFailureRates);
|
|
2604
|
+
} catch (err) {
|
|
2605
|
+
if (err instanceof ValidationError && err.attemptMetadata) {
|
|
2606
|
+
for (const meta of err.attemptMetadata) {
|
|
2607
|
+
await this.telemetry?.emit("generation_attempt", {
|
|
2608
|
+
description,
|
|
2609
|
+
attempt: meta.attempt,
|
|
2610
|
+
temperature: meta.temperature,
|
|
2611
|
+
durationMs: meta.durationMs,
|
|
2612
|
+
tokensInput: meta.tokensInput,
|
|
2613
|
+
tokensOutput: meta.tokensOutput,
|
|
2614
|
+
validationPassed: meta.validationPassed,
|
|
2615
|
+
issueCount: meta.issues.length,
|
|
2616
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
2617
|
+
workflowType
|
|
2618
|
+
}, runId);
|
|
2619
|
+
}
|
|
2620
|
+
await this.telemetry?.emit("build_complete", {
|
|
2621
|
+
description,
|
|
2622
|
+
success: false,
|
|
2623
|
+
totalAttempts: err.attemptMetadata.length,
|
|
2624
|
+
totalDurationMs: Date.now() - buildStart,
|
|
2625
|
+
totalTokensInput: err.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0),
|
|
2626
|
+
totalTokensOutput: err.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0),
|
|
2627
|
+
workflowName: null,
|
|
2628
|
+
workflowId: null,
|
|
2629
|
+
dryRun: false,
|
|
2630
|
+
credentialsNeeded: 0,
|
|
2631
|
+
warnedRules: err.warnedRules ?? [],
|
|
2632
|
+
workflowType
|
|
2633
|
+
}, runId);
|
|
2634
|
+
this.updatePatterns();
|
|
2635
|
+
}
|
|
2636
|
+
throw err;
|
|
2637
|
+
}
|
|
2638
|
+
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
1685
2639
|
const provider = this.requireProvider();
|
|
1686
2640
|
const deployed = await provider.update(id, designResult.workflow);
|
|
1687
2641
|
this.saveToLibrary(designResult.workflow, description, designResult, matches);
|
|
@@ -1698,8 +2652,11 @@ var Kairos = class {
|
|
|
1698
2652
|
workflowName: deployed.name,
|
|
1699
2653
|
workflowId: deployed.workflowId,
|
|
1700
2654
|
dryRun: false,
|
|
1701
|
-
credentialsNeeded: designResult.credentialsNeeded.length
|
|
1702
|
-
|
|
2655
|
+
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2656
|
+
warnedRules: designResult.warnedRules,
|
|
2657
|
+
workflowType
|
|
2658
|
+
}, runId);
|
|
2659
|
+
this.updatePatterns();
|
|
1703
2660
|
return {
|
|
1704
2661
|
workflowId: deployed.workflowId,
|
|
1705
2662
|
name: deployed.name,
|
|
@@ -1714,7 +2671,14 @@ var Kairos = class {
|
|
|
1714
2671
|
await this.saveQueue.catch(() => {
|
|
1715
2672
|
});
|
|
1716
2673
|
}
|
|
1717
|
-
|
|
2674
|
+
updatePatterns() {
|
|
2675
|
+
if (!this.patternAnalyzer) return;
|
|
2676
|
+
this.saveQueue = this.saveQueue.then(() => this.patternAnalyzer.analyzeAndSave()).then(() => null).catch((err) => {
|
|
2677
|
+
this.logger.warn("Pattern analysis failed (non-fatal)", { err: String(err) });
|
|
2678
|
+
return null;
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
async emitAttemptTelemetry(description, designResult, workflowType, runId) {
|
|
1718
2682
|
for (const meta of designResult.attemptMetadata) {
|
|
1719
2683
|
await this.telemetry?.emit("generation_attempt", {
|
|
1720
2684
|
description,
|
|
@@ -1725,8 +2689,9 @@ var Kairos = class {
|
|
|
1725
2689
|
tokensOutput: meta.tokensOutput,
|
|
1726
2690
|
validationPassed: meta.validationPassed,
|
|
1727
2691
|
issueCount: meta.issues.length,
|
|
1728
|
-
issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message }))
|
|
1729
|
-
|
|
2692
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
2693
|
+
workflowType
|
|
2694
|
+
}, runId);
|
|
1730
2695
|
}
|
|
1731
2696
|
}
|
|
1732
2697
|
recordDeploy() {
|
|
@@ -1824,9 +2789,9 @@ var Kairos = class {
|
|
|
1824
2789
|
};
|
|
1825
2790
|
|
|
1826
2791
|
// src/library/file-library.ts
|
|
1827
|
-
var
|
|
1828
|
-
var
|
|
1829
|
-
var
|
|
2792
|
+
var import_promises4 = require("fs/promises");
|
|
2793
|
+
var import_node_path7 = require("path");
|
|
2794
|
+
var import_node_os6 = require("os");
|
|
1830
2795
|
|
|
1831
2796
|
// src/library/scorer.ts
|
|
1832
2797
|
var WEIGHTS = {
|
|
@@ -2056,13 +3021,27 @@ function buildSearchCorpus(w) {
|
|
|
2056
3021
|
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
2057
3022
|
}
|
|
2058
3023
|
var MAX_LIBRARY_SIZE = 500;
|
|
3024
|
+
function isValidMeta(item) {
|
|
3025
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
3026
|
+
}
|
|
3027
|
+
function isValidOldEntry(item) {
|
|
3028
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
|
|
3029
|
+
item.workflow.nodes
|
|
3030
|
+
);
|
|
3031
|
+
}
|
|
2059
3032
|
var FileLibrary = class {
|
|
2060
3033
|
dir;
|
|
2061
|
-
|
|
3034
|
+
meta = [];
|
|
2062
3035
|
initPromise = null;
|
|
2063
3036
|
writeQueue = Promise.resolve();
|
|
2064
3037
|
constructor(dir) {
|
|
2065
|
-
this.dir = dir ?? (0,
|
|
3038
|
+
this.dir = dir ?? (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".kairos", "library");
|
|
3039
|
+
}
|
|
3040
|
+
get workflowsDir() {
|
|
3041
|
+
return (0, import_node_path7.join)(this.dir, "workflows");
|
|
3042
|
+
}
|
|
3043
|
+
workflowFilePath(id) {
|
|
3044
|
+
return (0, import_node_path7.join)(this.workflowsDir, `${id}.json`);
|
|
2066
3045
|
}
|
|
2067
3046
|
async initialize() {
|
|
2068
3047
|
if (!this.initPromise) {
|
|
@@ -2071,62 +3050,149 @@ var FileLibrary = class {
|
|
|
2071
3050
|
return this.initPromise;
|
|
2072
3051
|
}
|
|
2073
3052
|
async doInitialize() {
|
|
2074
|
-
await (0,
|
|
2075
|
-
const indexPath = (0,
|
|
3053
|
+
await (0, import_promises4.mkdir)(this.dir, { recursive: true });
|
|
3054
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3055
|
+
let workflowsDirExists = false;
|
|
2076
3056
|
try {
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
);
|
|
3057
|
+
await (0, import_promises4.stat)(this.workflowsDir);
|
|
3058
|
+
workflowsDirExists = true;
|
|
3059
|
+
} catch {
|
|
3060
|
+
}
|
|
3061
|
+
if (workflowsDirExists) {
|
|
3062
|
+
try {
|
|
3063
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3064
|
+
const parsed = JSON.parse(raw);
|
|
3065
|
+
if (Array.isArray(parsed)) {
|
|
3066
|
+
this.meta = parsed.filter(isValidMeta);
|
|
3067
|
+
}
|
|
3068
|
+
} catch {
|
|
3069
|
+
this.meta = [];
|
|
3070
|
+
}
|
|
3071
|
+
} else {
|
|
3072
|
+
try {
|
|
3073
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3074
|
+
const parsed = JSON.parse(raw);
|
|
3075
|
+
if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
|
|
3076
|
+
await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
} catch {
|
|
2085
3080
|
}
|
|
3081
|
+
this.meta = [];
|
|
3082
|
+
await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
3087
|
+
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
3088
|
+
* meta entry. Rewrites index.json in the new format.
|
|
3089
|
+
*/
|
|
3090
|
+
async migrateFromMonolithic(oldEntries) {
|
|
3091
|
+
await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
|
|
3092
|
+
const newMeta = [];
|
|
3093
|
+
for (const entry of oldEntries) {
|
|
3094
|
+
const wfPath = this.workflowFilePath(entry.id);
|
|
3095
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
3096
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
|
|
3097
|
+
await (0, import_promises4.rename)(tmpPath, wfPath);
|
|
3098
|
+
const { workflow, ...metaFields } = entry;
|
|
3099
|
+
newMeta.push({
|
|
3100
|
+
...metaFields,
|
|
3101
|
+
workflowName: workflow.name,
|
|
3102
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type)
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
this.meta = newMeta;
|
|
3106
|
+
await this.persistNow();
|
|
3107
|
+
}
|
|
3108
|
+
async loadWorkflowFile(id) {
|
|
3109
|
+
try {
|
|
3110
|
+
const raw = await (0, import_promises4.readFile)(this.workflowFilePath(id), "utf-8");
|
|
3111
|
+
return JSON.parse(raw);
|
|
2086
3112
|
} catch {
|
|
2087
|
-
|
|
3113
|
+
return null;
|
|
2088
3114
|
}
|
|
2089
3115
|
}
|
|
3116
|
+
async writeWorkflowFile(id, workflow) {
|
|
3117
|
+
const wfPath = this.workflowFilePath(id);
|
|
3118
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
3119
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
|
|
3120
|
+
await (0, import_promises4.rename)(tmpPath, wfPath);
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* Build a lightweight StoredWorkflow shell from a meta entry for use in
|
|
3124
|
+
* scoring / clustering. Only node.type is populated in each node — no other
|
|
3125
|
+
* node fields are used by hybridScore or clusterWorkflows.
|
|
3126
|
+
*/
|
|
3127
|
+
makeSearchShell(m) {
|
|
3128
|
+
return {
|
|
3129
|
+
...m,
|
|
3130
|
+
workflow: {
|
|
3131
|
+
name: m.workflowName,
|
|
3132
|
+
nodes: m.cachedNodeTypes.map((type) => ({
|
|
3133
|
+
id: "",
|
|
3134
|
+
name: "",
|
|
3135
|
+
type,
|
|
3136
|
+
typeVersion: 1,
|
|
3137
|
+
position: [0, 0],
|
|
3138
|
+
parameters: {}
|
|
3139
|
+
})),
|
|
3140
|
+
connections: {}
|
|
3141
|
+
}
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
2090
3144
|
async search(description, options) {
|
|
2091
|
-
const
|
|
2092
|
-
if (
|
|
3145
|
+
const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
|
|
3146
|
+
if (filteredMeta.length === 0) return [];
|
|
2093
3147
|
const limit = options?.limit ?? 3;
|
|
2094
3148
|
const queryTokens = tokenize(description);
|
|
2095
3149
|
if (queryTokens.length === 0) return [];
|
|
2096
|
-
const
|
|
3150
|
+
const shells = filteredMeta.map((m) => this.makeSearchShell(m));
|
|
3151
|
+
const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
|
|
2097
3152
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
2098
|
-
const docCount =
|
|
3153
|
+
const docCount = shells.length;
|
|
2099
3154
|
const idf = /* @__PURE__ */ new Map();
|
|
2100
3155
|
const allTokens = new Set(queryTokens);
|
|
2101
3156
|
for (const token of allTokens) {
|
|
2102
3157
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
2103
3158
|
idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
|
|
2104
3159
|
}
|
|
2105
|
-
const scored = hybridScore(queryTokens, description,
|
|
2106
|
-
const clusters = clusterWorkflows(
|
|
3160
|
+
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
3161
|
+
const clusters = clusterWorkflows(shells);
|
|
2107
3162
|
const reranked = rerank(scored, clusters).slice(0, limit);
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
3163
|
+
if (reranked.length === 0) return [];
|
|
3164
|
+
for (const r of reranked) {
|
|
3165
|
+
const m = this.meta.find((m2) => m2.id === r.workflow.id);
|
|
3166
|
+
if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
|
|
3167
|
+
}
|
|
3168
|
+
this.persist();
|
|
3169
|
+
const results = await Promise.all(
|
|
3170
|
+
reranked.map(async (r) => {
|
|
3171
|
+
const m = this.meta.find((meta) => meta.id === r.workflow.id);
|
|
3172
|
+
const workflow = await this.loadWorkflowFile(r.workflow.id);
|
|
3173
|
+
if (!workflow) return null;
|
|
3174
|
+
return {
|
|
3175
|
+
workflow: { ...m, workflow },
|
|
3176
|
+
score: r.score,
|
|
3177
|
+
mode: scoreToMode(r.score)
|
|
3178
|
+
};
|
|
3179
|
+
})
|
|
3180
|
+
);
|
|
3181
|
+
return results.filter((r) => r !== null);
|
|
2118
3182
|
}
|
|
2119
3183
|
async save(workflow, metadata) {
|
|
2120
3184
|
const id = generateUUID();
|
|
3185
|
+
await this.writeWorkflowFile(id, workflow);
|
|
2121
3186
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
2122
|
-
const
|
|
3187
|
+
const meta = {
|
|
2123
3188
|
id,
|
|
2124
|
-
workflow,
|
|
2125
3189
|
description: metadata.description,
|
|
2126
3190
|
tags: metadata.tags ?? [],
|
|
2127
3191
|
platform: metadata.platform ?? "n8n",
|
|
2128
3192
|
deployCount: 0,
|
|
2129
3193
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3194
|
+
workflowName: workflow.name,
|
|
3195
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type),
|
|
2130
3196
|
...failurePatterns?.length ? { failurePatterns } : {},
|
|
2131
3197
|
...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
|
|
2132
3198
|
...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
|
|
@@ -2138,31 +3204,35 @@ var FileLibrary = class {
|
|
|
2138
3204
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
2139
3205
|
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
2140
3206
|
};
|
|
2141
|
-
this.
|
|
2142
|
-
if (this.
|
|
2143
|
-
this.
|
|
2144
|
-
|
|
3207
|
+
this.meta.push(meta);
|
|
3208
|
+
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
3209
|
+
this.meta.sort((a, b) => {
|
|
3210
|
+
if (a.id === id) return -1;
|
|
3211
|
+
if (b.id === id) return 1;
|
|
3212
|
+
return (b.deployCount ?? 0) - (a.deployCount ?? 0);
|
|
3213
|
+
});
|
|
3214
|
+
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
2145
3215
|
}
|
|
2146
3216
|
await this.persist();
|
|
2147
3217
|
return id;
|
|
2148
3218
|
}
|
|
2149
3219
|
async recordDeployment(id) {
|
|
2150
|
-
const
|
|
2151
|
-
if (
|
|
2152
|
-
|
|
2153
|
-
|
|
3220
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3221
|
+
if (m) {
|
|
3222
|
+
m.deployCount++;
|
|
3223
|
+
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2154
3224
|
await this.persist();
|
|
2155
3225
|
}
|
|
2156
3226
|
}
|
|
2157
3227
|
async recordOutcome(id, outcome) {
|
|
2158
|
-
const
|
|
2159
|
-
if (!
|
|
3228
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3229
|
+
if (!m) return;
|
|
2160
3230
|
if (outcome.mode === "direct") {
|
|
2161
|
-
|
|
3231
|
+
m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
|
|
2162
3232
|
} else {
|
|
2163
|
-
|
|
3233
|
+
m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
|
|
2164
3234
|
}
|
|
2165
|
-
const stats =
|
|
3235
|
+
const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
|
|
2166
3236
|
stats.totalUses++;
|
|
2167
3237
|
stats.totalAttempts += outcome.attempts;
|
|
2168
3238
|
if (outcome.firstTryPass) stats.firstTryPasses++;
|
|
@@ -2170,24 +3240,35 @@ var FileLibrary = class {
|
|
|
2170
3240
|
const key = String(rule);
|
|
2171
3241
|
stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
|
|
2172
3242
|
}
|
|
2173
|
-
|
|
3243
|
+
m.outcomeStats = stats;
|
|
2174
3244
|
await this.persist();
|
|
2175
3245
|
}
|
|
2176
3246
|
async drain() {
|
|
2177
3247
|
await this.writeQueue;
|
|
2178
3248
|
}
|
|
2179
3249
|
async get(id) {
|
|
2180
|
-
|
|
3250
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3251
|
+
if (!m) return null;
|
|
3252
|
+
const workflow = await this.loadWorkflowFile(id);
|
|
3253
|
+
if (!workflow) return null;
|
|
3254
|
+
return { ...m, workflow };
|
|
2181
3255
|
}
|
|
2182
3256
|
async list(filters) {
|
|
2183
|
-
let
|
|
3257
|
+
let filtered = this.meta;
|
|
2184
3258
|
if (filters?.platform) {
|
|
2185
|
-
|
|
3259
|
+
filtered = filtered.filter((m) => m.platform === filters.platform);
|
|
2186
3260
|
}
|
|
2187
3261
|
if (filters?.tags && filters.tags.length > 0) {
|
|
2188
|
-
|
|
3262
|
+
filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
|
|
2189
3263
|
}
|
|
2190
|
-
|
|
3264
|
+
const results = await Promise.all(
|
|
3265
|
+
filtered.map(async (m) => {
|
|
3266
|
+
const workflow = await this.loadWorkflowFile(m.id);
|
|
3267
|
+
if (!workflow) return null;
|
|
3268
|
+
return { ...m, workflow };
|
|
3269
|
+
})
|
|
3270
|
+
);
|
|
3271
|
+
return results.filter((r) => r !== null);
|
|
2191
3272
|
}
|
|
2192
3273
|
deduplicateFailurePatterns(patterns) {
|
|
2193
3274
|
if (!patterns?.length) return void 0;
|
|
@@ -2202,12 +3283,37 @@ var FileLibrary = class {
|
|
|
2202
3283
|
}
|
|
2203
3284
|
return [...map.values()];
|
|
2204
3285
|
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Direct write used only during migration (before writeQueue is needed).
|
|
3288
|
+
*/
|
|
3289
|
+
async persistNow() {
|
|
3290
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3291
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
3292
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
3293
|
+
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3294
|
+
}
|
|
2205
3295
|
persist() {
|
|
2206
3296
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
2207
|
-
const indexPath = (0,
|
|
3297
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3298
|
+
let onDisk = [];
|
|
3299
|
+
try {
|
|
3300
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3301
|
+
const parsed = JSON.parse(raw);
|
|
3302
|
+
if (Array.isArray(parsed)) {
|
|
3303
|
+
onDisk = parsed.filter(isValidMeta);
|
|
3304
|
+
}
|
|
3305
|
+
} catch {
|
|
3306
|
+
}
|
|
3307
|
+
const ourIds = new Set(this.meta.map((m) => m.id));
|
|
3308
|
+
const external = onDisk.filter((m) => !ourIds.has(m.id));
|
|
3309
|
+
let merged = [...this.meta, ...external];
|
|
3310
|
+
if (merged.length > MAX_LIBRARY_SIZE) {
|
|
3311
|
+
merged.sort((a, b) => (b.deployCount ?? 0) - (a.deployCount ?? 0));
|
|
3312
|
+
merged = merged.slice(0, MAX_LIBRARY_SIZE);
|
|
3313
|
+
}
|
|
2208
3314
|
const tmpPath = `${indexPath}.tmp`;
|
|
2209
|
-
await (0,
|
|
2210
|
-
await (0,
|
|
3315
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
3316
|
+
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
2211
3317
|
});
|
|
2212
3318
|
return this.writeQueue;
|
|
2213
3319
|
}
|
|
@@ -2395,6 +3501,8 @@ Kairos SDK \u2014 LLM-powered n8n workflow generation
|
|
|
2395
3501
|
Usage:
|
|
2396
3502
|
kairos init First-time setup wizard
|
|
2397
3503
|
kairos build <description> [options]
|
|
3504
|
+
kairos patterns [options]
|
|
3505
|
+
kairos sessions [options]
|
|
2398
3506
|
kairos list
|
|
2399
3507
|
kairos get <id>
|
|
2400
3508
|
kairos activate <id>
|
|
@@ -2407,15 +3515,27 @@ Build options:
|
|
|
2407
3515
|
--name <name> Override the generated workflow name
|
|
2408
3516
|
--activate Activate the workflow after deployment
|
|
2409
3517
|
|
|
3518
|
+
Patterns options:
|
|
3519
|
+
--days <days> Analysis window (default: 30)
|
|
3520
|
+
--json Output raw JSON instead of summary
|
|
3521
|
+
|
|
3522
|
+
Sessions options:
|
|
3523
|
+
--limit <n> Number of recent sessions to show (default: 20)
|
|
3524
|
+
--json Output raw JSON instead of summary
|
|
3525
|
+
|
|
2410
3526
|
Sync options:
|
|
2411
3527
|
--max <count> Maximum templates to fetch (default: 500)
|
|
2412
3528
|
|
|
2413
3529
|
Environment variables:
|
|
2414
|
-
ANTHROPIC_API_KEY
|
|
2415
|
-
N8N_BASE_URL
|
|
2416
|
-
N8N_API_KEY
|
|
2417
|
-
KAIROS_MODEL
|
|
2418
|
-
KAIROS_TELEMETRY
|
|
3530
|
+
ANTHROPIC_API_KEY Anthropic API key (required)
|
|
3531
|
+
N8N_BASE_URL n8n instance URL (required for deploy, optional for --dry-run)
|
|
3532
|
+
N8N_API_KEY n8n API key (required for deploy, optional for --dry-run)
|
|
3533
|
+
KAIROS_MODEL Claude model override (default: claude-sonnet-4-6)
|
|
3534
|
+
KAIROS_TELEMETRY Set to "true" or a directory path to enable telemetry logging
|
|
3535
|
+
KAIROS_PROMPT_PROFILE minimal | standard | rich (default: standard)
|
|
3536
|
+
minimal: base prompt only, no library context, top 3 patterns
|
|
3537
|
+
standard: full library context, top 10 patterns (default)
|
|
3538
|
+
rich: full library context, top 15 patterns, proactive expression guidance
|
|
2419
3539
|
`;
|
|
2420
3540
|
function getEnvOrExit(name) {
|
|
2421
3541
|
const val = process.env[name];
|
|
@@ -2577,7 +3697,8 @@ async function handleDelete(positional, flags) {
|
|
|
2577
3697
|
console.log(`Deleted workflow ${id}`);
|
|
2578
3698
|
}
|
|
2579
3699
|
async function handleSyncTemplates(flags) {
|
|
2580
|
-
const
|
|
3700
|
+
const maxRaw = typeof flags["max"] === "string" ? parseInt(flags["max"], 10) : NaN;
|
|
3701
|
+
const max = Number.isNaN(maxRaw) ? 500 : maxRaw;
|
|
2581
3702
|
const library = new FileLibrary();
|
|
2582
3703
|
const logger = {
|
|
2583
3704
|
debug: () => {
|
|
@@ -2604,10 +3725,115 @@ async function handleSyncTemplates(flags) {
|
|
|
2604
3725
|
console.error(` Duplicates: ${result.skippedDuplicate} (already in library)`);
|
|
2605
3726
|
console.error(` Paid: ${result.skippedPaid} (skipped)`);
|
|
2606
3727
|
}
|
|
3728
|
+
async function handlePatterns(flags) {
|
|
3729
|
+
const daysRaw = typeof flags["days"] === "string" ? parseInt(flags["days"], 10) : NaN;
|
|
3730
|
+
const days = Number.isNaN(daysRaw) ? 30 : daysRaw;
|
|
3731
|
+
const analyzer = PatternAnalyzer.fromEnv();
|
|
3732
|
+
const analysis = await analyzer.analyzeAndSave(days);
|
|
3733
|
+
if (flags["json"] === true) {
|
|
3734
|
+
console.log(JSON.stringify(analysis, null, 2));
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3737
|
+
console.log(`
|
|
3738
|
+
Kairos Pattern Analysis (last ${days} days)`);
|
|
3739
|
+
console.log("\u2500".repeat(45));
|
|
3740
|
+
console.log(` Builds: ${analysis.summary.totalBuilds}`);
|
|
3741
|
+
console.log(` Attempts: ${analysis.summary.totalAttempts}`);
|
|
3742
|
+
console.log(` First-try pass: ${(analysis.summary.firstTryPassRate * 100).toFixed(1)}%`);
|
|
3743
|
+
console.log(` Correction rate: ${(analysis.summary.correctionRate * 100).toFixed(1)}%`);
|
|
3744
|
+
if (analysis.summary.singleAttemptFailRate !== void 0) {
|
|
3745
|
+
console.log(` Single-attempt failures: ${(analysis.summary.singleAttemptFailRate * 100).toFixed(1)}%`);
|
|
3746
|
+
}
|
|
3747
|
+
console.log(` Avg duration: ${(analysis.summary.avgDurationMs / 1e3).toFixed(1)}s`);
|
|
3748
|
+
const active = analysis.topFailureRules.filter((p) => p.state !== "resolved");
|
|
3749
|
+
const resolved = analysis.topFailureRules.filter((p) => p.state === "resolved");
|
|
3750
|
+
if (active.length > 0) {
|
|
3751
|
+
console.log(`
|
|
3752
|
+
Active Failure Patterns:`);
|
|
3753
|
+
for (const p of active) {
|
|
3754
|
+
const regressionTag = p.regressed ? "[REGRESSION] " : "";
|
|
3755
|
+
const stateTag = p.state === "confirmed" ? "[CONFIRMED]" : "[DRAFT]";
|
|
3756
|
+
const trendIcon = p.trend === "improving" ? " ^" : p.trend === "worsening" ? " v" : p.trend === "new" ? " *" : "";
|
|
3757
|
+
const stage = p.pipelineStage.replace(/_/g, " ");
|
|
3758
|
+
const scoreStr = p.compositeScore.toFixed(3);
|
|
3759
|
+
console.log(` Rule ${p.rule} ${regressionTag}${stateTag}${trendIcon} \u2014 score ${scoreStr} | ${p.failureCount} failures (${(p.confidence * 100).toFixed(1)}%) [${stage}]`);
|
|
3760
|
+
const f = p.scoringFactors;
|
|
3761
|
+
console.log(` Factors: confidence=${f.rawConfidence} \xD7 impact=${f.impact} \xD7 recency=${f.recency} + boost=${f.stickinessBoost}`);
|
|
3762
|
+
if (p.mitigation) console.log(` Fix: ${p.mitigation}`);
|
|
3763
|
+
if (p.exampleMessages.length > 0) console.log(` e.g. ${p.exampleMessages[0]}`);
|
|
3764
|
+
if (p.workflowTypeBreakdown) {
|
|
3765
|
+
const topType = Object.entries(p.workflowTypeBreakdown).sort((a, b) => b[1] - a[1])[0];
|
|
3766
|
+
if (topType) console.log(` Top workflow type: ${topType[0]} (${topType[1]} failures)`);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
} else {
|
|
3770
|
+
console.log(`
|
|
3771
|
+
No active failure patterns.`);
|
|
3772
|
+
}
|
|
3773
|
+
if (resolved.length > 0) {
|
|
3774
|
+
console.log(`
|
|
3775
|
+
Resolved Patterns:`);
|
|
3776
|
+
for (const p of resolved) {
|
|
3777
|
+
console.log(` Rule ${p.rule} \u2014 previously confirmed, 0 failures in current window`);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
if (analysis.failingCredentialTypes.length > 0) {
|
|
3781
|
+
console.log(`
|
|
3782
|
+
Failing Credential Types:`);
|
|
3783
|
+
for (const c of analysis.failingCredentialTypes) {
|
|
3784
|
+
console.log(` ${c.type}: ${c.count} failures`);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
if (analysis.warningEffectiveness && analysis.warningEffectiveness.length > 0) {
|
|
3788
|
+
console.log(`
|
|
3789
|
+
Warning Effectiveness:`);
|
|
3790
|
+
for (const w of analysis.warningEffectiveness) {
|
|
3791
|
+
console.log(` Rule ${w.rule}: warned ${w.timesWarned}x, prevented ${w.timesWarnedAndPassed}x (${Math.round(w.effectivenessRate * 100)}% effective)`);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
const drift = analysis.drift;
|
|
3795
|
+
if (drift) {
|
|
3796
|
+
console.log(`
|
|
3797
|
+
Drift Detection: ${drift.healthy ? "HEALTHY" : "ALERTS FOUND"}`);
|
|
3798
|
+
console.log(` Coverage: ${drift.coveredRules}/${drift.totalRules} rules have mitigations + stage mappings`);
|
|
3799
|
+
if (drift.alerts.length > 0) {
|
|
3800
|
+
for (const a of drift.alerts) {
|
|
3801
|
+
console.log(` [${a.type}] Rule ${a.rule}: ${a.message}`);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
console.log(`
|
|
3806
|
+
Patterns saved to ~/.kairos/patterns.json`);
|
|
3807
|
+
}
|
|
3808
|
+
async function handleSessions(flags) {
|
|
3809
|
+
const limitRaw = typeof flags["limit"] === "string" ? parseInt(flags["limit"], 10) : NaN;
|
|
3810
|
+
const limit = Number.isNaN(limitRaw) ? 20 : limitRaw;
|
|
3811
|
+
const analyzer = PatternAnalyzer.fromEnv();
|
|
3812
|
+
const sessions = await analyzer.getSessions(limit);
|
|
3813
|
+
if (flags["json"] === true) {
|
|
3814
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
if (sessions.length === 0) {
|
|
3818
|
+
console.log("No session history found. Run kairos patterns first to generate session data.");
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3821
|
+
console.log(`
|
|
3822
|
+
Recent Sessions (last ${sessions.length})`);
|
|
3823
|
+
console.log("\u2500".repeat(60));
|
|
3824
|
+
for (const s of [...sessions].reverse()) {
|
|
3825
|
+
const status = s.success ? "\u2713" : "\u2717";
|
|
3826
|
+
const typeTag = s.workflowType ? ` [${s.workflowType}]` : "";
|
|
3827
|
+
const attemptsStr = s.attempts > 1 ? ` (${s.attempts} attempts)` : "";
|
|
3828
|
+
const nameStr = s.workflowName ? ` ${s.workflowName}` : ` ${s.description.slice(0, 50)}`;
|
|
3829
|
+
const rulesStr = s.failedRules.length > 0 ? ` \u2014 rules ${s.failedRules.join(", ")} failed` : "";
|
|
3830
|
+
console.log(`${s.date} ${status}${nameStr}${attemptsStr}${typeTag}${rulesStr}`);
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
2607
3833
|
async function handleInit() {
|
|
2608
|
-
const { writeFile:
|
|
2609
|
-
const { join:
|
|
2610
|
-
const { homedir:
|
|
3834
|
+
const { writeFile: writeFile3, readFile: readFile2, mkdir: mkdir4 } = await import("fs/promises");
|
|
3835
|
+
const { join: join8 } = await import("path");
|
|
3836
|
+
const { homedir: homedir7 } = await import("os");
|
|
2611
3837
|
const readline = await import("readline");
|
|
2612
3838
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
2613
3839
|
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
@@ -2615,10 +3841,10 @@ async function handleInit() {
|
|
|
2615
3841
|
console.error(" Kairos SDK \u2014 Setup Wizard");
|
|
2616
3842
|
console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2617
3843
|
console.error("");
|
|
2618
|
-
const envPath =
|
|
3844
|
+
const envPath = join8(process.cwd(), ".env");
|
|
2619
3845
|
let existingEnv = "";
|
|
2620
3846
|
try {
|
|
2621
|
-
existingEnv = await
|
|
3847
|
+
existingEnv = await readFile2(envPath, "utf-8");
|
|
2622
3848
|
} catch {
|
|
2623
3849
|
}
|
|
2624
3850
|
const has = (key) => existingEnv.includes(key) || !!process.env[key];
|
|
@@ -2644,7 +3870,7 @@ async function handleInit() {
|
|
|
2644
3870
|
rl.close();
|
|
2645
3871
|
if (lines.length > 0) {
|
|
2646
3872
|
const newContent = existingEnv ? existingEnv.trimEnd() + "\n" + lines.join("\n") + "\n" : lines.join("\n") + "\n";
|
|
2647
|
-
await
|
|
3873
|
+
await writeFile3(envPath, newContent, "utf-8");
|
|
2648
3874
|
console.error(`
|
|
2649
3875
|
Saved to ${envPath}`);
|
|
2650
3876
|
} else {
|
|
@@ -2679,8 +3905,8 @@ async function handleInit() {
|
|
|
2679
3905
|
});
|
|
2680
3906
|
console.error(` Synced ${result.saved} templates (${result.blocked} blocked, ${result.skippedDuplicate} duplicates)`);
|
|
2681
3907
|
}
|
|
2682
|
-
const kairosDir =
|
|
2683
|
-
await
|
|
3908
|
+
const kairosDir = join8(homedir7(), ".kairos");
|
|
3909
|
+
await mkdir4(join8(kairosDir, "telemetry"), { recursive: true });
|
|
2684
3910
|
console.error("");
|
|
2685
3911
|
console.error(" Setup complete! Try:");
|
|
2686
3912
|
console.error("");
|
|
@@ -2700,6 +3926,12 @@ async function main() {
|
|
|
2700
3926
|
case "build":
|
|
2701
3927
|
await handleBuild(positional, flags);
|
|
2702
3928
|
break;
|
|
3929
|
+
case "patterns":
|
|
3930
|
+
await handlePatterns(flags);
|
|
3931
|
+
break;
|
|
3932
|
+
case "sessions":
|
|
3933
|
+
await handleSessions(flags);
|
|
3934
|
+
break;
|
|
2703
3935
|
case "list":
|
|
2704
3936
|
await handleList();
|
|
2705
3937
|
break;
|