@kairos-sdk/core 0.4.0 → 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 +12 -9
- package/dist/{chunk-N6LRD2FN.js → chunk-4TS6GW6O.js} +60 -372
- 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-NJ6QZBIC.js → chunk-6IXW3WCC.js} +477 -534
- 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 +632 -154
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +56 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +577 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -540
- package/dist/index.d.ts +3 -540
- package/dist/index.js +8 -4
- package/dist/mcp-server.cjs +651 -122
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/mcp-server.js +91 -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-N6LRD2FN.js.map +0 -1
- package/dist/chunk-NJ6QZBIC.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -559,6 +559,9 @@ var N8nValidator = class {
|
|
|
559
559
|
this.checkRule21(workflow, issues);
|
|
560
560
|
this.checkRule22(workflow, issues);
|
|
561
561
|
this.checkRule23(workflow, issues);
|
|
562
|
+
this.checkRule24(workflow, issues);
|
|
563
|
+
this.checkRule25(workflow, issues);
|
|
564
|
+
this.checkRule26(workflow, issues);
|
|
562
565
|
if (Array.isArray(workflow.nodes)) {
|
|
563
566
|
const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
|
|
564
567
|
for (const issue of issues) {
|
|
@@ -691,10 +694,14 @@ var N8nValidator = class {
|
|
|
691
694
|
checkRule11(w, issues) {
|
|
692
695
|
if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
|
|
693
696
|
const reachable = /* @__PURE__ */ new Set();
|
|
694
|
-
|
|
697
|
+
const aiSubNodeSources = /* @__PURE__ */ new Set();
|
|
698
|
+
for (const [sourceName, outputs] of Object.entries(w.connections)) {
|
|
695
699
|
if (typeof outputs !== "object" || outputs === null) continue;
|
|
696
|
-
|
|
700
|
+
let hasAiPort = false;
|
|
701
|
+
for (const [portName, portGroup] of Object.entries(outputs)) {
|
|
697
702
|
if (!Array.isArray(portGroup)) continue;
|
|
703
|
+
const isAiPort = portName.startsWith("ai_");
|
|
704
|
+
if (isAiPort) hasAiPort = true;
|
|
698
705
|
for (const targets of portGroup) {
|
|
699
706
|
if (!Array.isArray(targets)) continue;
|
|
700
707
|
for (const target of targets) {
|
|
@@ -703,10 +710,13 @@ var N8nValidator = class {
|
|
|
703
710
|
}
|
|
704
711
|
}
|
|
705
712
|
}
|
|
713
|
+
if (hasAiPort) aiSubNodeSources.add(sourceName);
|
|
706
714
|
}
|
|
707
715
|
for (const node of w.nodes) {
|
|
708
716
|
if (node.type.includes("stickyNote")) continue;
|
|
709
|
-
if (
|
|
717
|
+
if (this.isTriggerNode(node)) continue;
|
|
718
|
+
if (aiSubNodeSources.has(node.name)) continue;
|
|
719
|
+
if (!reachable.has(node.name)) {
|
|
710
720
|
this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
|
|
711
721
|
}
|
|
712
722
|
}
|
|
@@ -902,6 +912,76 @@ var N8nValidator = class {
|
|
|
902
912
|
}
|
|
903
913
|
}
|
|
904
914
|
}
|
|
915
|
+
// Rule 24 (WARN): deprecated accessor syntax in expressions
|
|
916
|
+
checkRule24(w, issues) {
|
|
917
|
+
if (!Array.isArray(w.nodes)) return;
|
|
918
|
+
const deprecated = /\$node\s*\[/;
|
|
919
|
+
for (const node of w.nodes) {
|
|
920
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
921
|
+
if (deprecated.test(expr)) {
|
|
922
|
+
this.warn(
|
|
923
|
+
issues,
|
|
924
|
+
24,
|
|
925
|
+
`Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
|
|
926
|
+
node.id
|
|
927
|
+
);
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// Rule 25 (WARN): wrong item index assumptions in expressions
|
|
934
|
+
checkRule25(w, issues) {
|
|
935
|
+
if (!Array.isArray(w.nodes)) return;
|
|
936
|
+
const itemIndex = /\$json\s*\.\s*items\s*\[/;
|
|
937
|
+
for (const node of w.nodes) {
|
|
938
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
939
|
+
if (itemIndex.test(expr)) {
|
|
940
|
+
this.warn(
|
|
941
|
+
issues,
|
|
942
|
+
25,
|
|
943
|
+
`Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
|
|
944
|
+
node.id
|
|
945
|
+
);
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
// Rule 26 (WARN): missing .first() or .all() on node references
|
|
952
|
+
checkRule26(w, issues) {
|
|
953
|
+
if (!Array.isArray(w.nodes)) return;
|
|
954
|
+
const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
|
|
955
|
+
for (const node of w.nodes) {
|
|
956
|
+
for (const expr of this.extractExpressions(node.parameters)) {
|
|
957
|
+
if (bareRef.test(expr)) {
|
|
958
|
+
this.warn(
|
|
959
|
+
issues,
|
|
960
|
+
26,
|
|
961
|
+
`Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
|
|
962
|
+
node.id
|
|
963
|
+
);
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
extractExpressions(params) {
|
|
970
|
+
const expressions = [];
|
|
971
|
+
const walk = (val) => {
|
|
972
|
+
if (typeof val === "string") {
|
|
973
|
+
if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
|
|
974
|
+
expressions.push(val);
|
|
975
|
+
}
|
|
976
|
+
} else if (Array.isArray(val)) {
|
|
977
|
+
for (const item of val) walk(item);
|
|
978
|
+
} else if (val !== null && typeof val === "object") {
|
|
979
|
+
for (const v of Object.values(val)) walk(v);
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
walk(params);
|
|
983
|
+
return expressions;
|
|
984
|
+
}
|
|
905
985
|
// Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
|
|
906
986
|
checkRule21(w, issues) {
|
|
907
987
|
if (!Array.isArray(w.nodes)) return;
|
|
@@ -989,9 +1069,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
|
|
|
989
1069
|
- Never reuse IDs, never use sequential fake IDs like "node-1"
|
|
990
1070
|
|
|
991
1071
|
### Credentials:
|
|
992
|
-
-
|
|
993
|
-
|
|
994
|
-
-
|
|
1072
|
+
- Each credential is keyed by its type string, with an object value containing id and name:
|
|
1073
|
+
"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack Credential" } }
|
|
1074
|
+
- Use "placeholder-id" as the id \u2014 users replace this with their real credential ID from n8n after deployment
|
|
1075
|
+
- The credentialsNeeded field in your response declares what credentials the user must configure
|
|
1076
|
+
- Never put API keys or tokens directly in node parameters when a credential type exists
|
|
995
1077
|
|
|
996
1078
|
### Node names:
|
|
997
1079
|
- All node names must be unique within the workflow
|
|
@@ -1038,6 +1120,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
|
|
|
1038
1120
|
|
|
1039
1121
|
---
|
|
1040
1122
|
|
|
1123
|
+
## EXPRESSION SYNTAX \u2014 how to reference upstream node data
|
|
1124
|
+
|
|
1125
|
+
### Accessing a field from an upstream node:
|
|
1126
|
+
- CORRECT: $('NodeName').item.json.field
|
|
1127
|
+
- WRONG: $node["NodeName"].json.field \u2190 deprecated accessor, fails at runtime (Rule 24)
|
|
1128
|
+
|
|
1129
|
+
### Accessing array items from $json:
|
|
1130
|
+
- CORRECT: $json.field \u2190 n8n auto-flattens items; each item is already a flat object
|
|
1131
|
+
- WRONG: $json.items[0].field \u2190 do not index into items[] (Rule 25)
|
|
1132
|
+
|
|
1133
|
+
### Calling node data \u2014 always qualify with .first() or .all():
|
|
1134
|
+
- CORRECT: $('NodeName').first().json.field \u2190 single item
|
|
1135
|
+
- CORRECT: $('NodeName').all() \u2190 array of all items
|
|
1136
|
+
- WRONG: $('NodeName').json \u2190 throws at runtime without .first() or .all() (Rule 26)
|
|
1137
|
+
|
|
1138
|
+
---
|
|
1139
|
+
|
|
1041
1140
|
## NODE CATALOG \u2014 exact type strings and safe typeVersions
|
|
1042
1141
|
|
|
1043
1142
|
### Triggers (always at least one required):
|
|
@@ -1137,6 +1236,9 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
|
|
|
1137
1236
|
5. At least one trigger node present
|
|
1138
1237
|
6. Every AI Agent has an ai_languageModel sub-node
|
|
1139
1238
|
7. settings block is complete with executionOrder: "v1"
|
|
1239
|
+
8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
|
|
1240
|
+
9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
|
|
1241
|
+
10. No bare $('NodeName').json \u2014 always use .first().json.field or .all()
|
|
1140
1242
|
|
|
1141
1243
|
---
|
|
1142
1244
|
|
|
@@ -1153,7 +1255,7 @@ function scoreToMode(score) {
|
|
|
1153
1255
|
}
|
|
1154
1256
|
|
|
1155
1257
|
// src/validation/rule-metadata.ts
|
|
1156
|
-
var VALIDATOR_RULE_IDS = Array.from({ length:
|
|
1258
|
+
var VALIDATOR_RULE_IDS = Array.from({ length: 26 }, (_, i) => i + 1);
|
|
1157
1259
|
var RULE_PIPELINE_STAGES = {
|
|
1158
1260
|
1: "node_generation",
|
|
1159
1261
|
2: "node_generation",
|
|
@@ -1177,7 +1279,28 @@ var RULE_PIPELINE_STAGES = {
|
|
|
1177
1279
|
20: "connection_wiring",
|
|
1178
1280
|
21: "workflow_structure",
|
|
1179
1281
|
22: "workflow_structure",
|
|
1180
|
-
23: "node_generation"
|
|
1282
|
+
23: "node_generation",
|
|
1283
|
+
24: "expression_syntax",
|
|
1284
|
+
25: "expression_syntax",
|
|
1285
|
+
26: "expression_syntax"
|
|
1286
|
+
};
|
|
1287
|
+
var RULE_EXAMPLES = {
|
|
1288
|
+
17: {
|
|
1289
|
+
bad: '"credentials": { "slackOAuth2Api": "my-token" }',
|
|
1290
|
+
good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
|
|
1291
|
+
},
|
|
1292
|
+
24: {
|
|
1293
|
+
bad: '$node["Fetch Data"].json.email',
|
|
1294
|
+
good: "$('Fetch Data').item.json.email"
|
|
1295
|
+
},
|
|
1296
|
+
25: {
|
|
1297
|
+
bad: "$json.items[0].email",
|
|
1298
|
+
good: "$json.email"
|
|
1299
|
+
},
|
|
1300
|
+
26: {
|
|
1301
|
+
bad: "$('Fetch Data').json.email",
|
|
1302
|
+
good: "$('Fetch Data').first().json.email"
|
|
1303
|
+
}
|
|
1181
1304
|
};
|
|
1182
1305
|
var RULE_MITIGATIONS = {
|
|
1183
1306
|
1: "Provide a non-empty workflow name string",
|
|
@@ -1196,21 +1319,44 @@ var RULE_MITIGATIONS = {
|
|
|
1196
1319
|
14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
|
|
1197
1320
|
15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
|
|
1198
1321
|
16: "All node names must be unique within the workflow",
|
|
1199
|
-
17: '
|
|
1322
|
+
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',
|
|
1200
1323
|
18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
|
|
1201
1324
|
19: "Use known safe typeVersion values for each node type",
|
|
1202
1325
|
20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
|
|
1203
1326
|
21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
|
|
1204
1327
|
22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
|
|
1205
|
-
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync"
|
|
1328
|
+
23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
|
|
1329
|
+
24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
|
|
1330
|
+
25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
|
|
1331
|
+
26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime'
|
|
1206
1332
|
};
|
|
1207
1333
|
|
|
1208
1334
|
// src/generation/prompt-builder.ts
|
|
1209
1335
|
var CRITICAL_SCORE_THRESHOLD = 0.15;
|
|
1336
|
+
function resolveProfile() {
|
|
1337
|
+
const env = process.env["KAIROS_PROMPT_PROFILE"];
|
|
1338
|
+
if (env === "minimal" || env === "standard" || env === "rich") return env;
|
|
1339
|
+
return "standard";
|
|
1340
|
+
}
|
|
1341
|
+
var PROACTIVE_EXPRESSION_GUIDANCE = `## Expression Syntax Quick Reference
|
|
1342
|
+
|
|
1343
|
+
Always use these patterns in expressions:
|
|
1344
|
+
- Access node data: $('NodeName').item.json.field (not $node["NodeName"].json)
|
|
1345
|
+
- Access JSON field: $json.field (not $json.items[0].field)
|
|
1346
|
+
- Single item: $('NodeName').first().json.field
|
|
1347
|
+
- All items: $('NodeName').all()`;
|
|
1210
1348
|
var PromptBuilder = class {
|
|
1211
1349
|
patternsPath;
|
|
1212
|
-
|
|
1350
|
+
profile;
|
|
1351
|
+
_lastActivePatterns = null;
|
|
1352
|
+
constructor(patternsPath, profile) {
|
|
1213
1353
|
this.patternsPath = patternsPath ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "patterns.json");
|
|
1354
|
+
this.profile = profile ?? resolveProfile();
|
|
1355
|
+
}
|
|
1356
|
+
resolveMaxPatterns() {
|
|
1357
|
+
if (this.profile === "minimal") return 3;
|
|
1358
|
+
if (this.profile === "rich") return 15;
|
|
1359
|
+
return 10;
|
|
1214
1360
|
}
|
|
1215
1361
|
build(request, matches, globalFailureRates = [], dynamicCatalog) {
|
|
1216
1362
|
const mode = this.resolveMode(matches);
|
|
@@ -1248,53 +1394,62 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
|
|
|
1248
1394
|
cache_control: { type: "ephemeral" }
|
|
1249
1395
|
}
|
|
1250
1396
|
];
|
|
1251
|
-
if (
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1397
|
+
if (this.profile !== "minimal") {
|
|
1398
|
+
if (mode === "reference" && matches.length > 0) {
|
|
1399
|
+
const refText = matches.slice(0, 3).map((m) => {
|
|
1400
|
+
const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1401
|
+
return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
|
|
1255
1402
|
Nodes:
|
|
1256
1403
|
${nodes}`;
|
|
1257
|
-
|
|
1258
|
-
blocks.push({
|
|
1259
|
-
type: "text",
|
|
1260
|
-
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1261
|
-
|
|
1262
|
-
${refText}`
|
|
1263
|
-
});
|
|
1264
|
-
}
|
|
1265
|
-
if (mode === "direct" && matches[0]) {
|
|
1266
|
-
const match = matches[0];
|
|
1267
|
-
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1268
|
-
if (json.length > 3e4) {
|
|
1269
|
-
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1404
|
+
}).join("\n\n");
|
|
1270
1405
|
blocks.push({
|
|
1271
1406
|
type: "text",
|
|
1272
|
-
text: `##
|
|
1407
|
+
text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
|
|
1408
|
+
|
|
1409
|
+
${refText}`
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
if (mode === "direct" && matches[0]) {
|
|
1413
|
+
const match = matches[0];
|
|
1414
|
+
const json = JSON.stringify(match.workflow.workflow, null, 2);
|
|
1415
|
+
if (json.length > 3e4) {
|
|
1416
|
+
const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
|
|
1417
|
+
blocks.push({
|
|
1418
|
+
type: "text",
|
|
1419
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
|
|
1273
1420
|
Nodes:
|
|
1274
1421
|
${nodes}`
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1422
|
+
});
|
|
1423
|
+
} else {
|
|
1424
|
+
blocks.push({
|
|
1425
|
+
type: "text",
|
|
1426
|
+
text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
|
|
1280
1427
|
|
|
1281
1428
|
${json}`
|
|
1282
|
-
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1283
1431
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
text: `## Weak Structural Hint
|
|
1432
|
+
if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
|
|
1433
|
+
const hint = matches[0];
|
|
1434
|
+
const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
|
|
1435
|
+
blocks.push({
|
|
1436
|
+
type: "text",
|
|
1437
|
+
text: `## Weak Structural Hint
|
|
1291
1438
|
A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
|
|
1292
|
-
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1293
1441
|
}
|
|
1294
1442
|
const warnings = this.buildFailureWarnings(matches, globalFailureRates);
|
|
1295
1443
|
if (warnings) {
|
|
1296
1444
|
blocks.push({ type: "text", text: warnings });
|
|
1297
1445
|
}
|
|
1446
|
+
if (this.profile === "rich") {
|
|
1447
|
+
const expressionRules = /* @__PURE__ */ new Set([24, 25, 26]);
|
|
1448
|
+
const expressionAlreadyCovered = (this._lastActivePatterns ?? []).some((p) => expressionRules.has(p.rule));
|
|
1449
|
+
if (!expressionAlreadyCovered) {
|
|
1450
|
+
blocks.push({ type: "text", text: PROACTIVE_EXPRESSION_GUIDANCE });
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1298
1453
|
return blocks;
|
|
1299
1454
|
}
|
|
1300
1455
|
loadPatterns() {
|
|
@@ -1308,18 +1463,19 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1308
1463
|
}
|
|
1309
1464
|
}
|
|
1310
1465
|
getWarnedRules() {
|
|
1311
|
-
|
|
1466
|
+
const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
|
|
1467
|
+
return patterns.map((p) => p.rule);
|
|
1312
1468
|
}
|
|
1313
|
-
getActivePatterns() {
|
|
1314
|
-
const MAX_WARNED = 10;
|
|
1469
|
+
getActivePatterns(maxCount = 10) {
|
|
1315
1470
|
const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
|
|
1316
1471
|
const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1317
1472
|
const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1318
1473
|
const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1319
|
-
return [...regressed, ...confirmed, ...drafts].slice(0,
|
|
1474
|
+
return [...regressed, ...confirmed, ...drafts].slice(0, maxCount);
|
|
1320
1475
|
}
|
|
1321
1476
|
buildFailureWarnings(matches, globalFailureRates) {
|
|
1322
|
-
const richPatterns = this.getActivePatterns();
|
|
1477
|
+
const richPatterns = this.getActivePatterns(this.resolveMaxPatterns());
|
|
1478
|
+
this._lastActivePatterns = richPatterns;
|
|
1323
1479
|
if (richPatterns.length > 0) {
|
|
1324
1480
|
return this.buildStageGroupedWarnings(richPatterns, matches);
|
|
1325
1481
|
}
|
|
@@ -1330,7 +1486,8 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1330
1486
|
credential_injection: "CREDENTIAL FORMATTING",
|
|
1331
1487
|
connection_wiring: "CONNECTION WIRING",
|
|
1332
1488
|
node_generation: "NODE GENERATION",
|
|
1333
|
-
workflow_structure: "WORKFLOW STRUCTURE"
|
|
1489
|
+
workflow_structure: "WORKFLOW STRUCTURE",
|
|
1490
|
+
expression_syntax: "EXPRESSION SYNTAX"
|
|
1334
1491
|
};
|
|
1335
1492
|
const byStage = /* @__PURE__ */ new Map();
|
|
1336
1493
|
for (const p of patterns) {
|
|
@@ -1358,7 +1515,11 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
|
|
|
1358
1515
|
const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
|
|
1359
1516
|
const remedyStr = remedy ? `
|
|
1360
1517
|
Fix: ${remedy}` : "";
|
|
1361
|
-
|
|
1518
|
+
const ex = RULE_EXAMPLES[p.rule];
|
|
1519
|
+
const exampleStr = ex ? `
|
|
1520
|
+
Bad: ${ex.bad}
|
|
1521
|
+
Good: ${ex.good}` : "";
|
|
1522
|
+
lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}${exampleStr}`);
|
|
1362
1523
|
} else {
|
|
1363
1524
|
const ruleNums = group.map((p) => p.rule).join(", ");
|
|
1364
1525
|
const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
|
|
@@ -1465,12 +1626,12 @@ var GENERATE_WORKFLOW_TOOL = {
|
|
|
1465
1626
|
}
|
|
1466
1627
|
};
|
|
1467
1628
|
var WorkflowDesigner = class {
|
|
1468
|
-
constructor(anthropic, model, logger) {
|
|
1629
|
+
constructor(anthropic, model, logger, patternsPath) {
|
|
1469
1630
|
this.anthropic = anthropic;
|
|
1470
1631
|
this.model = model;
|
|
1471
1632
|
this.logger = logger;
|
|
1472
1633
|
this.validator = new N8nValidator();
|
|
1473
|
-
this.promptBuilder = new PromptBuilder();
|
|
1634
|
+
this.promptBuilder = new PromptBuilder(patternsPath);
|
|
1474
1635
|
}
|
|
1475
1636
|
anthropic;
|
|
1476
1637
|
model;
|
|
@@ -1554,6 +1715,11 @@ var WorkflowDesigner = class {
|
|
|
1554
1715
|
}
|
|
1555
1716
|
}
|
|
1556
1717
|
extractToolUse(message) {
|
|
1718
|
+
if (message.stop_reason === "max_tokens") {
|
|
1719
|
+
throw new GenerationError(
|
|
1720
|
+
"Claude response was truncated (max_tokens reached) \u2014 the workflow may be too large. Try a simpler description or break it into smaller workflows."
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1557
1723
|
const toolUseBlock = message.content.find(
|
|
1558
1724
|
(block) => block.type === "tool_use"
|
|
1559
1725
|
);
|
|
@@ -1596,11 +1762,12 @@ var TelemetryCollector = class {
|
|
|
1596
1762
|
this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
|
|
1597
1763
|
this.sessionId = generateUUID();
|
|
1598
1764
|
}
|
|
1599
|
-
async emit(eventType, data) {
|
|
1765
|
+
async emit(eventType, data, runId) {
|
|
1600
1766
|
const event = {
|
|
1601
1767
|
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
1602
1768
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1603
1769
|
sessionId: this.sessionId,
|
|
1770
|
+
...runId ? { runId } : {},
|
|
1604
1771
|
eventType,
|
|
1605
1772
|
data
|
|
1606
1773
|
};
|
|
@@ -1726,6 +1893,7 @@ var PATTERN_SCHEMA_VERSION = 2;
|
|
|
1726
1893
|
var PatternAnalyzer = class _PatternAnalyzer {
|
|
1727
1894
|
telemetryDir;
|
|
1728
1895
|
outputDir;
|
|
1896
|
+
_cachedEvents = null;
|
|
1729
1897
|
constructor(telemetryDir) {
|
|
1730
1898
|
const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
|
|
1731
1899
|
this.telemetryDir = telemetryDir ?? defaultDir;
|
|
@@ -1754,19 +1922,23 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1754
1922
|
}));
|
|
1755
1923
|
}
|
|
1756
1924
|
if (fromVersion < 2) {
|
|
1757
|
-
migrated = migrated.map((p) =>
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
...p
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1925
|
+
migrated = migrated.map((p) => {
|
|
1926
|
+
const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
|
|
1927
|
+
return {
|
|
1928
|
+
...p,
|
|
1929
|
+
scoringFactors: {
|
|
1930
|
+
...sf,
|
|
1931
|
+
stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
});
|
|
1764
1935
|
}
|
|
1765
1936
|
return migrated;
|
|
1766
1937
|
}
|
|
1767
1938
|
async analyze(days = 30) {
|
|
1768
1939
|
const previousPatterns = await this.loadPreviousPatterns();
|
|
1769
1940
|
const events = await this.readAllEvents(days);
|
|
1941
|
+
this._cachedEvents = events;
|
|
1770
1942
|
const starts = events.filter((e) => e.eventType === "build_start");
|
|
1771
1943
|
const attempts = events.filter((e) => e.eventType === "generation_attempt");
|
|
1772
1944
|
const passed = attempts.filter(
|
|
@@ -1779,13 +1951,18 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1779
1951
|
const credentialFailures = /* @__PURE__ */ new Map();
|
|
1780
1952
|
for (const a of failed) {
|
|
1781
1953
|
const weight = this.recencyWeight(a.fileDate);
|
|
1954
|
+
const buildId = a.runId ?? a.sessionId;
|
|
1782
1955
|
const data = a.data;
|
|
1783
1956
|
for (const issue of data.issues ?? []) {
|
|
1784
|
-
|
|
1957
|
+
if (issue.severity === "warn") continue;
|
|
1958
|
+
const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
|
|
1785
1959
|
entry.count++;
|
|
1786
|
-
entry.sessions.add(
|
|
1960
|
+
entry.sessions.add(buildId);
|
|
1787
1961
|
entry.recencyWeights.push(weight);
|
|
1788
1962
|
entry.allMessages.push(issue.message);
|
|
1963
|
+
if (data.workflowType) {
|
|
1964
|
+
entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
|
|
1965
|
+
}
|
|
1789
1966
|
ruleFailures.set(issue.rule, entry);
|
|
1790
1967
|
if (issue.rule === 17) {
|
|
1791
1968
|
const credPatterns = [
|
|
@@ -1838,9 +2015,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1838
2015
|
}
|
|
1839
2016
|
const sessions = /* @__PURE__ */ new Map();
|
|
1840
2017
|
for (const a of attempts) {
|
|
1841
|
-
const
|
|
2018
|
+
const buildId = a.runId ?? a.sessionId;
|
|
2019
|
+
const list = sessions.get(buildId) ?? [];
|
|
1842
2020
|
list.push(a);
|
|
1843
|
-
sessions.set(
|
|
2021
|
+
sessions.set(buildId, list);
|
|
1844
2022
|
}
|
|
1845
2023
|
let firstTryPass = 0;
|
|
1846
2024
|
let correctionNeeded = 0;
|
|
@@ -1887,7 +2065,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1887
2065
|
const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
|
|
1888
2066
|
const stickiness = stickinessCount.get(rule) ?? 0;
|
|
1889
2067
|
const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
|
|
1890
|
-
|
|
2068
|
+
const pattern = {
|
|
1891
2069
|
rule,
|
|
1892
2070
|
failureCount: entry.count,
|
|
1893
2071
|
confidence: Math.round(rawConfidence * 1e3) / 1e3,
|
|
@@ -1899,6 +2077,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1899
2077
|
exampleMessages: this.deduplicateMessages(entry.allMessages),
|
|
1900
2078
|
mitigation: RULE_MITIGATIONS[rule] ?? null
|
|
1901
2079
|
};
|
|
2080
|
+
if (entry.workflowTypes.size > 0) {
|
|
2081
|
+
pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
|
|
2082
|
+
}
|
|
2083
|
+
return pattern;
|
|
1902
2084
|
}).sort((a, b) => b.compositeScore - a.compositeScore);
|
|
1903
2085
|
const activeRules = new Set(activePatterns.map((p) => p.rule));
|
|
1904
2086
|
for (const p of activePatterns) {
|
|
@@ -1955,7 +2137,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
1955
2137
|
const warned = bcData.warnedRules ?? [];
|
|
1956
2138
|
if (warned.length === 0) continue;
|
|
1957
2139
|
const sessionFailedRules = /* @__PURE__ */ new Set();
|
|
1958
|
-
const sessionAttempts = sessions.get(bc.sessionId) ?? [];
|
|
2140
|
+
const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
|
|
1959
2141
|
for (const a of sessionAttempts) {
|
|
1960
2142
|
const ad = a.data;
|
|
1961
2143
|
if (ad.validationPassed === false) {
|
|
@@ -2038,8 +2220,55 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2038
2220
|
};
|
|
2039
2221
|
const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
|
|
2040
2222
|
await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
|
|
2223
|
+
const sessions = await this.buildSessionSummaries(days);
|
|
2224
|
+
const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
|
|
2225
|
+
const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
|
|
2226
|
+
await (0, import_promises3.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
|
|
2227
|
+
await (0, import_promises3.rename)(sessionHistoryTmp, sessionHistoryPath);
|
|
2041
2228
|
return analysis;
|
|
2042
2229
|
}
|
|
2230
|
+
async getSessions(limit = 20) {
|
|
2231
|
+
try {
|
|
2232
|
+
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
|
|
2233
|
+
const all = JSON.parse(raw);
|
|
2234
|
+
return all.slice(-limit);
|
|
2235
|
+
} catch {
|
|
2236
|
+
return [];
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
async buildSessionSummaries(days = 30) {
|
|
2240
|
+
const events = this._cachedEvents ?? await this.readAllEvents(days);
|
|
2241
|
+
const buildCompletes = events.filter((e) => e.eventType === "build_complete");
|
|
2242
|
+
const attemptsByBuild = /* @__PURE__ */ new Map();
|
|
2243
|
+
for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
|
|
2244
|
+
const buildId = e.runId ?? e.sessionId;
|
|
2245
|
+
const list = attemptsByBuild.get(buildId) ?? [];
|
|
2246
|
+
list.push(e);
|
|
2247
|
+
attemptsByBuild.set(buildId, list);
|
|
2248
|
+
}
|
|
2249
|
+
const summaries = buildCompletes.map((bc) => {
|
|
2250
|
+
const data = bc.data;
|
|
2251
|
+
const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
|
|
2252
|
+
const failedRules = Array.from(new Set(
|
|
2253
|
+
sessionAttempts.flatMap((a) => {
|
|
2254
|
+
const ad = a.data;
|
|
2255
|
+
if (ad.validationPassed !== false) return [];
|
|
2256
|
+
return (ad.issues ?? []).map((i) => i.rule);
|
|
2257
|
+
})
|
|
2258
|
+
));
|
|
2259
|
+
return {
|
|
2260
|
+
sessionId: bc.sessionId,
|
|
2261
|
+
date: bc.fileDate,
|
|
2262
|
+
description: data.description ?? "",
|
|
2263
|
+
workflowType: data.workflowType ?? null,
|
|
2264
|
+
attempts: data.totalAttempts ?? 1,
|
|
2265
|
+
success: data.success ?? false,
|
|
2266
|
+
failedRules,
|
|
2267
|
+
workflowName: data.workflowName ?? null
|
|
2268
|
+
};
|
|
2269
|
+
});
|
|
2270
|
+
return summaries.sort((a, b) => a.date.localeCompare(b.date));
|
|
2271
|
+
}
|
|
2043
2272
|
async getHistory(limit = 20) {
|
|
2044
2273
|
try {
|
|
2045
2274
|
const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
|
|
@@ -2061,7 +2290,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
|
|
|
2061
2290
|
alerts.push({
|
|
2062
2291
|
type: "stale_pattern",
|
|
2063
2292
|
rule: p.rule,
|
|
2064
|
-
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-
|
|
2293
|
+
message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-26)`
|
|
2065
2294
|
});
|
|
2066
2295
|
}
|
|
2067
2296
|
}
|
|
@@ -2148,7 +2377,59 @@ var nullLogger = {
|
|
|
2148
2377
|
}
|
|
2149
2378
|
};
|
|
2150
2379
|
|
|
2380
|
+
// src/utils/workflow-type.ts
|
|
2381
|
+
var TYPE_KEYWORDS = [
|
|
2382
|
+
["gmail", "email"],
|
|
2383
|
+
["imap", "email"],
|
|
2384
|
+
["smtp", "email"],
|
|
2385
|
+
[" email", "email"],
|
|
2386
|
+
["slack", "slack"],
|
|
2387
|
+
["telegram", "messaging"],
|
|
2388
|
+
["discord", "messaging"],
|
|
2389
|
+
[" sms", "messaging"],
|
|
2390
|
+
["twilio", "messaging"],
|
|
2391
|
+
["webhook", "webhook"],
|
|
2392
|
+
["google sheets", "data"],
|
|
2393
|
+
["spreadsheet", "data"],
|
|
2394
|
+
["airtable", "data"],
|
|
2395
|
+
["notion", "data"],
|
|
2396
|
+
["github", "devops"],
|
|
2397
|
+
["gitlab", "devops"],
|
|
2398
|
+
["schedule", "schedule"],
|
|
2399
|
+
[" cron", "schedule"],
|
|
2400
|
+
["daily", "schedule"],
|
|
2401
|
+
["weekly", "schedule"],
|
|
2402
|
+
["hourly", "schedule"],
|
|
2403
|
+
["every day", "schedule"],
|
|
2404
|
+
["every hour", "schedule"],
|
|
2405
|
+
["every morning", "schedule"],
|
|
2406
|
+
["postgres", "database"],
|
|
2407
|
+
["mysql", "database"],
|
|
2408
|
+
["supabase", "database"],
|
|
2409
|
+
["redis", "database"],
|
|
2410
|
+
[" database", "database"],
|
|
2411
|
+
[" llm", "ai"],
|
|
2412
|
+
[" gpt", "ai"],
|
|
2413
|
+
["claude", "ai"],
|
|
2414
|
+
[" agent", "ai"],
|
|
2415
|
+
["langchain", "ai"],
|
|
2416
|
+
[" ai ", "ai"],
|
|
2417
|
+
[" ai", "ai"],
|
|
2418
|
+
["http request", "api"],
|
|
2419
|
+
["rest api", "api"],
|
|
2420
|
+
[" api", "api"]
|
|
2421
|
+
];
|
|
2422
|
+
function inferWorkflowType(description) {
|
|
2423
|
+
const lower = " " + description.toLowerCase();
|
|
2424
|
+
for (const [keyword, type] of TYPE_KEYWORDS) {
|
|
2425
|
+
if (lower.includes(keyword)) return type;
|
|
2426
|
+
}
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2151
2430
|
// src/client.ts
|
|
2431
|
+
var import_node_os5 = require("os");
|
|
2432
|
+
var import_node_path6 = require("path");
|
|
2152
2433
|
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
2153
2434
|
var Kairos = class {
|
|
2154
2435
|
provider;
|
|
@@ -2177,7 +2458,8 @@ var Kairos = class {
|
|
|
2177
2458
|
this.provider = null;
|
|
2178
2459
|
}
|
|
2179
2460
|
const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
|
|
2180
|
-
|
|
2461
|
+
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");
|
|
2462
|
+
this.designer = new WorkflowDesigner(anthropic, this.model, logger, patternsPath);
|
|
2181
2463
|
this.validator = new N8nValidator();
|
|
2182
2464
|
this.library = options.library ?? new NullLibrary();
|
|
2183
2465
|
this.logger = logger;
|
|
@@ -2210,11 +2492,13 @@ var Kairos = class {
|
|
|
2210
2492
|
this.validateDescription(description);
|
|
2211
2493
|
this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
|
|
2212
2494
|
const buildStart = Date.now();
|
|
2495
|
+
const runId = generateUUID();
|
|
2496
|
+
const workflowType = inferWorkflowType(description);
|
|
2213
2497
|
await this.telemetry?.emit("build_start", {
|
|
2214
2498
|
description,
|
|
2215
2499
|
model: this.model,
|
|
2216
2500
|
dryRun: options?.dryRun ?? false
|
|
2217
|
-
});
|
|
2501
|
+
}, runId);
|
|
2218
2502
|
await this.library.initialize();
|
|
2219
2503
|
const matches = await this.library.search(description);
|
|
2220
2504
|
if (matches.length > 0) {
|
|
@@ -2249,8 +2533,9 @@ var Kairos = class {
|
|
|
2249
2533
|
tokensOutput: meta.tokensOutput,
|
|
2250
2534
|
validationPassed: meta.validationPassed,
|
|
2251
2535
|
issueCount: meta.issues.length,
|
|
2252
|
-
issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
|
|
2253
|
-
|
|
2536
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
2537
|
+
workflowType
|
|
2538
|
+
}, runId);
|
|
2254
2539
|
}
|
|
2255
2540
|
await this.telemetry?.emit("build_complete", {
|
|
2256
2541
|
description,
|
|
@@ -2263,13 +2548,14 @@ var Kairos = class {
|
|
|
2263
2548
|
workflowId: null,
|
|
2264
2549
|
dryRun: options?.dryRun ?? false,
|
|
2265
2550
|
credentialsNeeded: 0,
|
|
2266
|
-
warnedRules: err.warnedRules ?? []
|
|
2267
|
-
|
|
2551
|
+
warnedRules: err.warnedRules ?? [],
|
|
2552
|
+
workflowType
|
|
2553
|
+
}, runId);
|
|
2268
2554
|
this.updatePatterns();
|
|
2269
2555
|
}
|
|
2270
2556
|
throw err;
|
|
2271
2557
|
}
|
|
2272
|
-
await this.emitAttemptTelemetry(description, designResult);
|
|
2558
|
+
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
2273
2559
|
const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
|
|
2274
2560
|
this.saveToLibrary(workflow, description, designResult, matches);
|
|
2275
2561
|
if (options?.dryRun) {
|
|
@@ -2286,8 +2572,9 @@ var Kairos = class {
|
|
|
2286
2572
|
workflowId: null,
|
|
2287
2573
|
dryRun: true,
|
|
2288
2574
|
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2289
|
-
warnedRules: designResult.warnedRules
|
|
2290
|
-
|
|
2575
|
+
warnedRules: designResult.warnedRules,
|
|
2576
|
+
workflowType
|
|
2577
|
+
}, runId);
|
|
2291
2578
|
this.updatePatterns();
|
|
2292
2579
|
return {
|
|
2293
2580
|
workflowId: null,
|
|
@@ -2318,8 +2605,9 @@ var Kairos = class {
|
|
|
2318
2605
|
workflowId: deployed.workflowId,
|
|
2319
2606
|
dryRun: false,
|
|
2320
2607
|
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2321
|
-
warnedRules: designResult.warnedRules
|
|
2322
|
-
|
|
2608
|
+
warnedRules: designResult.warnedRules,
|
|
2609
|
+
workflowType
|
|
2610
|
+
}, runId);
|
|
2323
2611
|
this.updatePatterns();
|
|
2324
2612
|
return {
|
|
2325
2613
|
workflowId: deployed.workflowId,
|
|
@@ -2335,11 +2623,13 @@ var Kairos = class {
|
|
|
2335
2623
|
this.validateDescription(description);
|
|
2336
2624
|
this.logger.info("Kairos.update", { id, description });
|
|
2337
2625
|
const buildStart = Date.now();
|
|
2626
|
+
const runId = generateUUID();
|
|
2627
|
+
const workflowType = inferWorkflowType(description);
|
|
2338
2628
|
await this.telemetry?.emit("build_start", {
|
|
2339
2629
|
description,
|
|
2340
2630
|
model: this.model,
|
|
2341
2631
|
dryRun: false
|
|
2342
|
-
});
|
|
2632
|
+
}, runId);
|
|
2343
2633
|
await this.library.initialize();
|
|
2344
2634
|
const matches = await this.library.search(description);
|
|
2345
2635
|
const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
|
|
@@ -2358,8 +2648,9 @@ var Kairos = class {
|
|
|
2358
2648
|
tokensOutput: meta.tokensOutput,
|
|
2359
2649
|
validationPassed: meta.validationPassed,
|
|
2360
2650
|
issueCount: meta.issues.length,
|
|
2361
|
-
issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
|
|
2362
|
-
|
|
2651
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
2652
|
+
workflowType
|
|
2653
|
+
}, runId);
|
|
2363
2654
|
}
|
|
2364
2655
|
await this.telemetry?.emit("build_complete", {
|
|
2365
2656
|
description,
|
|
@@ -2372,13 +2663,14 @@ var Kairos = class {
|
|
|
2372
2663
|
workflowId: null,
|
|
2373
2664
|
dryRun: false,
|
|
2374
2665
|
credentialsNeeded: 0,
|
|
2375
|
-
warnedRules: err.warnedRules ?? []
|
|
2376
|
-
|
|
2666
|
+
warnedRules: err.warnedRules ?? [],
|
|
2667
|
+
workflowType
|
|
2668
|
+
}, runId);
|
|
2377
2669
|
this.updatePatterns();
|
|
2378
2670
|
}
|
|
2379
2671
|
throw err;
|
|
2380
2672
|
}
|
|
2381
|
-
await this.emitAttemptTelemetry(description, designResult);
|
|
2673
|
+
await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
|
|
2382
2674
|
const provider = this.requireProvider();
|
|
2383
2675
|
const deployed = await provider.update(id, designResult.workflow);
|
|
2384
2676
|
this.saveToLibrary(designResult.workflow, description, designResult, matches);
|
|
@@ -2396,8 +2688,9 @@ var Kairos = class {
|
|
|
2396
2688
|
workflowId: deployed.workflowId,
|
|
2397
2689
|
dryRun: false,
|
|
2398
2690
|
credentialsNeeded: designResult.credentialsNeeded.length,
|
|
2399
|
-
warnedRules: designResult.warnedRules
|
|
2400
|
-
|
|
2691
|
+
warnedRules: designResult.warnedRules,
|
|
2692
|
+
workflowType
|
|
2693
|
+
}, runId);
|
|
2401
2694
|
this.updatePatterns();
|
|
2402
2695
|
return {
|
|
2403
2696
|
workflowId: deployed.workflowId,
|
|
@@ -2420,7 +2713,7 @@ var Kairos = class {
|
|
|
2420
2713
|
return null;
|
|
2421
2714
|
});
|
|
2422
2715
|
}
|
|
2423
|
-
async emitAttemptTelemetry(description, designResult) {
|
|
2716
|
+
async emitAttemptTelemetry(description, designResult, workflowType, runId) {
|
|
2424
2717
|
for (const meta of designResult.attemptMetadata) {
|
|
2425
2718
|
await this.telemetry?.emit("generation_attempt", {
|
|
2426
2719
|
description,
|
|
@@ -2431,8 +2724,9 @@ var Kairos = class {
|
|
|
2431
2724
|
tokensOutput: meta.tokensOutput,
|
|
2432
2725
|
validationPassed: meta.validationPassed,
|
|
2433
2726
|
issueCount: meta.issues.length,
|
|
2434
|
-
issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
|
|
2435
|
-
|
|
2727
|
+
issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
|
|
2728
|
+
workflowType
|
|
2729
|
+
}, runId);
|
|
2436
2730
|
}
|
|
2437
2731
|
}
|
|
2438
2732
|
recordDeploy() {
|
|
@@ -2531,8 +2825,8 @@ var Kairos = class {
|
|
|
2531
2825
|
|
|
2532
2826
|
// src/library/file-library.ts
|
|
2533
2827
|
var import_promises4 = require("fs/promises");
|
|
2534
|
-
var
|
|
2535
|
-
var
|
|
2828
|
+
var import_node_path7 = require("path");
|
|
2829
|
+
var import_node_os6 = require("os");
|
|
2536
2830
|
|
|
2537
2831
|
// src/library/scorer.ts
|
|
2538
2832
|
var WEIGHTS = {
|
|
@@ -2762,13 +3056,27 @@ function buildSearchCorpus(w) {
|
|
|
2762
3056
|
return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
|
|
2763
3057
|
}
|
|
2764
3058
|
var MAX_LIBRARY_SIZE = 500;
|
|
3059
|
+
function isValidMeta(item) {
|
|
3060
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
|
|
3061
|
+
}
|
|
3062
|
+
function isValidOldEntry(item) {
|
|
3063
|
+
return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
|
|
3064
|
+
item.workflow.nodes
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
2765
3067
|
var FileLibrary = class {
|
|
2766
3068
|
dir;
|
|
2767
|
-
|
|
3069
|
+
meta = [];
|
|
2768
3070
|
initPromise = null;
|
|
2769
3071
|
writeQueue = Promise.resolve();
|
|
2770
3072
|
constructor(dir) {
|
|
2771
|
-
this.dir = dir ?? (0,
|
|
3073
|
+
this.dir = dir ?? (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".kairos", "library");
|
|
3074
|
+
}
|
|
3075
|
+
get workflowsDir() {
|
|
3076
|
+
return (0, import_node_path7.join)(this.dir, "workflows");
|
|
3077
|
+
}
|
|
3078
|
+
workflowFilePath(id) {
|
|
3079
|
+
return (0, import_node_path7.join)(this.workflowsDir, `${id}.json`);
|
|
2772
3080
|
}
|
|
2773
3081
|
async initialize() {
|
|
2774
3082
|
if (!this.initPromise) {
|
|
@@ -2778,61 +3086,148 @@ var FileLibrary = class {
|
|
|
2778
3086
|
}
|
|
2779
3087
|
async doInitialize() {
|
|
2780
3088
|
await (0, import_promises4.mkdir)(this.dir, { recursive: true });
|
|
2781
|
-
const indexPath = (0,
|
|
3089
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3090
|
+
let workflowsDirExists = false;
|
|
2782
3091
|
try {
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
);
|
|
3092
|
+
await (0, import_promises4.stat)(this.workflowsDir);
|
|
3093
|
+
workflowsDirExists = true;
|
|
3094
|
+
} catch {
|
|
3095
|
+
}
|
|
3096
|
+
if (workflowsDirExists) {
|
|
3097
|
+
try {
|
|
3098
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3099
|
+
const parsed = JSON.parse(raw);
|
|
3100
|
+
if (Array.isArray(parsed)) {
|
|
3101
|
+
this.meta = parsed.filter(isValidMeta);
|
|
3102
|
+
}
|
|
3103
|
+
} catch {
|
|
3104
|
+
this.meta = [];
|
|
3105
|
+
}
|
|
3106
|
+
} else {
|
|
3107
|
+
try {
|
|
3108
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3109
|
+
const parsed = JSON.parse(raw);
|
|
3110
|
+
if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
|
|
3111
|
+
await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
} catch {
|
|
2791
3115
|
}
|
|
3116
|
+
this.meta = [];
|
|
3117
|
+
await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* One-time transparent migration from v0.4.x monolithic index.json.
|
|
3122
|
+
* Splits each stored workflow into a per-file workflow JSON and a lightweight
|
|
3123
|
+
* meta entry. Rewrites index.json in the new format.
|
|
3124
|
+
*/
|
|
3125
|
+
async migrateFromMonolithic(oldEntries) {
|
|
3126
|
+
await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
|
|
3127
|
+
const newMeta = [];
|
|
3128
|
+
for (const entry of oldEntries) {
|
|
3129
|
+
const wfPath = this.workflowFilePath(entry.id);
|
|
3130
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
3131
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
|
|
3132
|
+
await (0, import_promises4.rename)(tmpPath, wfPath);
|
|
3133
|
+
const { workflow, ...metaFields } = entry;
|
|
3134
|
+
newMeta.push({
|
|
3135
|
+
...metaFields,
|
|
3136
|
+
workflowName: workflow.name,
|
|
3137
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type)
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
this.meta = newMeta;
|
|
3141
|
+
await this.persistNow();
|
|
3142
|
+
}
|
|
3143
|
+
async loadWorkflowFile(id) {
|
|
3144
|
+
try {
|
|
3145
|
+
const raw = await (0, import_promises4.readFile)(this.workflowFilePath(id), "utf-8");
|
|
3146
|
+
return JSON.parse(raw);
|
|
2792
3147
|
} catch {
|
|
2793
|
-
|
|
3148
|
+
return null;
|
|
2794
3149
|
}
|
|
2795
3150
|
}
|
|
3151
|
+
async writeWorkflowFile(id, workflow) {
|
|
3152
|
+
const wfPath = this.workflowFilePath(id);
|
|
3153
|
+
const tmpPath = `${wfPath}.tmp`;
|
|
3154
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
|
|
3155
|
+
await (0, import_promises4.rename)(tmpPath, wfPath);
|
|
3156
|
+
}
|
|
3157
|
+
/**
|
|
3158
|
+
* Build a lightweight StoredWorkflow shell from a meta entry for use in
|
|
3159
|
+
* scoring / clustering. Only node.type is populated in each node — no other
|
|
3160
|
+
* node fields are used by hybridScore or clusterWorkflows.
|
|
3161
|
+
*/
|
|
3162
|
+
makeSearchShell(m) {
|
|
3163
|
+
return {
|
|
3164
|
+
...m,
|
|
3165
|
+
workflow: {
|
|
3166
|
+
name: m.workflowName,
|
|
3167
|
+
nodes: m.cachedNodeTypes.map((type) => ({
|
|
3168
|
+
id: "",
|
|
3169
|
+
name: "",
|
|
3170
|
+
type,
|
|
3171
|
+
typeVersion: 1,
|
|
3172
|
+
position: [0, 0],
|
|
3173
|
+
parameters: {}
|
|
3174
|
+
})),
|
|
3175
|
+
connections: {}
|
|
3176
|
+
}
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
2796
3179
|
async search(description, options) {
|
|
2797
|
-
const
|
|
2798
|
-
if (
|
|
3180
|
+
const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
|
|
3181
|
+
if (filteredMeta.length === 0) return [];
|
|
2799
3182
|
const limit = options?.limit ?? 3;
|
|
2800
3183
|
const queryTokens = tokenize(description);
|
|
2801
3184
|
if (queryTokens.length === 0) return [];
|
|
2802
|
-
const
|
|
3185
|
+
const shells = filteredMeta.map((m) => this.makeSearchShell(m));
|
|
3186
|
+
const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
|
|
2803
3187
|
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
|
2804
|
-
const docCount =
|
|
3188
|
+
const docCount = shells.length;
|
|
2805
3189
|
const idf = /* @__PURE__ */ new Map();
|
|
2806
3190
|
const allTokens = new Set(queryTokens);
|
|
2807
3191
|
for (const token of allTokens) {
|
|
2808
3192
|
const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
|
|
2809
3193
|
idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
|
|
2810
3194
|
}
|
|
2811
|
-
const scored = hybridScore(queryTokens, description,
|
|
2812
|
-
const clusters = clusterWorkflows(
|
|
3195
|
+
const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
3196
|
+
const clusters = clusterWorkflows(shells);
|
|
2813
3197
|
const reranked = rerank(scored, clusters).slice(0, limit);
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
3198
|
+
if (reranked.length === 0) return [];
|
|
3199
|
+
for (const r of reranked) {
|
|
3200
|
+
const m = this.meta.find((m2) => m2.id === r.workflow.id);
|
|
3201
|
+
if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
|
|
3202
|
+
}
|
|
3203
|
+
this.persist();
|
|
3204
|
+
const results = await Promise.all(
|
|
3205
|
+
reranked.map(async (r) => {
|
|
3206
|
+
const m = this.meta.find((meta) => meta.id === r.workflow.id);
|
|
3207
|
+
const workflow = await this.loadWorkflowFile(r.workflow.id);
|
|
3208
|
+
if (!workflow) return null;
|
|
3209
|
+
return {
|
|
3210
|
+
workflow: { ...m, workflow },
|
|
3211
|
+
score: r.score,
|
|
3212
|
+
mode: scoreToMode(r.score)
|
|
3213
|
+
};
|
|
3214
|
+
})
|
|
3215
|
+
);
|
|
3216
|
+
return results.filter((r) => r !== null);
|
|
2824
3217
|
}
|
|
2825
3218
|
async save(workflow, metadata) {
|
|
2826
3219
|
const id = generateUUID();
|
|
3220
|
+
await this.writeWorkflowFile(id, workflow);
|
|
2827
3221
|
const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
|
|
2828
|
-
const
|
|
3222
|
+
const meta = {
|
|
2829
3223
|
id,
|
|
2830
|
-
workflow,
|
|
2831
3224
|
description: metadata.description,
|
|
2832
3225
|
tags: metadata.tags ?? [],
|
|
2833
3226
|
platform: metadata.platform ?? "n8n",
|
|
2834
3227
|
deployCount: 0,
|
|
2835
3228
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3229
|
+
workflowName: workflow.name,
|
|
3230
|
+
cachedNodeTypes: workflow.nodes.map((n) => n.type),
|
|
2836
3231
|
...failurePatterns?.length ? { failurePatterns } : {},
|
|
2837
3232
|
...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
|
|
2838
3233
|
...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
|
|
@@ -2844,31 +3239,35 @@ var FileLibrary = class {
|
|
|
2844
3239
|
...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
|
|
2845
3240
|
...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
|
|
2846
3241
|
};
|
|
2847
|
-
this.
|
|
2848
|
-
if (this.
|
|
2849
|
-
this.
|
|
2850
|
-
|
|
3242
|
+
this.meta.push(meta);
|
|
3243
|
+
if (this.meta.length > MAX_LIBRARY_SIZE) {
|
|
3244
|
+
this.meta.sort((a, b) => {
|
|
3245
|
+
if (a.id === id) return -1;
|
|
3246
|
+
if (b.id === id) return 1;
|
|
3247
|
+
return (b.deployCount ?? 0) - (a.deployCount ?? 0);
|
|
3248
|
+
});
|
|
3249
|
+
this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
|
|
2851
3250
|
}
|
|
2852
3251
|
await this.persist();
|
|
2853
3252
|
return id;
|
|
2854
3253
|
}
|
|
2855
3254
|
async recordDeployment(id) {
|
|
2856
|
-
const
|
|
2857
|
-
if (
|
|
2858
|
-
|
|
2859
|
-
|
|
3255
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3256
|
+
if (m) {
|
|
3257
|
+
m.deployCount++;
|
|
3258
|
+
m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2860
3259
|
await this.persist();
|
|
2861
3260
|
}
|
|
2862
3261
|
}
|
|
2863
3262
|
async recordOutcome(id, outcome) {
|
|
2864
|
-
const
|
|
2865
|
-
if (!
|
|
3263
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3264
|
+
if (!m) return;
|
|
2866
3265
|
if (outcome.mode === "direct") {
|
|
2867
|
-
|
|
3266
|
+
m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
|
|
2868
3267
|
} else {
|
|
2869
|
-
|
|
3268
|
+
m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
|
|
2870
3269
|
}
|
|
2871
|
-
const stats =
|
|
3270
|
+
const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
|
|
2872
3271
|
stats.totalUses++;
|
|
2873
3272
|
stats.totalAttempts += outcome.attempts;
|
|
2874
3273
|
if (outcome.firstTryPass) stats.firstTryPasses++;
|
|
@@ -2876,24 +3275,35 @@ var FileLibrary = class {
|
|
|
2876
3275
|
const key = String(rule);
|
|
2877
3276
|
stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
|
|
2878
3277
|
}
|
|
2879
|
-
|
|
3278
|
+
m.outcomeStats = stats;
|
|
2880
3279
|
await this.persist();
|
|
2881
3280
|
}
|
|
2882
3281
|
async drain() {
|
|
2883
3282
|
await this.writeQueue;
|
|
2884
3283
|
}
|
|
2885
3284
|
async get(id) {
|
|
2886
|
-
|
|
3285
|
+
const m = this.meta.find((m2) => m2.id === id);
|
|
3286
|
+
if (!m) return null;
|
|
3287
|
+
const workflow = await this.loadWorkflowFile(id);
|
|
3288
|
+
if (!workflow) return null;
|
|
3289
|
+
return { ...m, workflow };
|
|
2887
3290
|
}
|
|
2888
3291
|
async list(filters) {
|
|
2889
|
-
let
|
|
3292
|
+
let filtered = this.meta;
|
|
2890
3293
|
if (filters?.platform) {
|
|
2891
|
-
|
|
3294
|
+
filtered = filtered.filter((m) => m.platform === filters.platform);
|
|
2892
3295
|
}
|
|
2893
3296
|
if (filters?.tags && filters.tags.length > 0) {
|
|
2894
|
-
|
|
3297
|
+
filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
|
|
2895
3298
|
}
|
|
2896
|
-
|
|
3299
|
+
const results = await Promise.all(
|
|
3300
|
+
filtered.map(async (m) => {
|
|
3301
|
+
const workflow = await this.loadWorkflowFile(m.id);
|
|
3302
|
+
if (!workflow) return null;
|
|
3303
|
+
return { ...m, workflow };
|
|
3304
|
+
})
|
|
3305
|
+
);
|
|
3306
|
+
return results.filter((r) => r !== null);
|
|
2897
3307
|
}
|
|
2898
3308
|
deduplicateFailurePatterns(patterns) {
|
|
2899
3309
|
if (!patterns?.length) return void 0;
|
|
@@ -2908,11 +3318,36 @@ var FileLibrary = class {
|
|
|
2908
3318
|
}
|
|
2909
3319
|
return [...map.values()];
|
|
2910
3320
|
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Direct write used only during migration (before writeQueue is needed).
|
|
3323
|
+
*/
|
|
3324
|
+
async persistNow() {
|
|
3325
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3326
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
3327
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
|
|
3328
|
+
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
3329
|
+
}
|
|
2911
3330
|
persist() {
|
|
2912
3331
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
2913
|
-
const indexPath = (0,
|
|
3332
|
+
const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
|
|
3333
|
+
let onDisk = [];
|
|
3334
|
+
try {
|
|
3335
|
+
const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
|
|
3336
|
+
const parsed = JSON.parse(raw);
|
|
3337
|
+
if (Array.isArray(parsed)) {
|
|
3338
|
+
onDisk = parsed.filter(isValidMeta);
|
|
3339
|
+
}
|
|
3340
|
+
} catch {
|
|
3341
|
+
}
|
|
3342
|
+
const ourIds = new Set(this.meta.map((m) => m.id));
|
|
3343
|
+
const external = onDisk.filter((m) => !ourIds.has(m.id));
|
|
3344
|
+
let merged = [...this.meta, ...external];
|
|
3345
|
+
if (merged.length > MAX_LIBRARY_SIZE) {
|
|
3346
|
+
merged.sort((a, b) => (b.deployCount ?? 0) - (a.deployCount ?? 0));
|
|
3347
|
+
merged = merged.slice(0, MAX_LIBRARY_SIZE);
|
|
3348
|
+
}
|
|
2914
3349
|
const tmpPath = `${indexPath}.tmp`;
|
|
2915
|
-
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(
|
|
3350
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
2916
3351
|
await (0, import_promises4.rename)(tmpPath, indexPath);
|
|
2917
3352
|
});
|
|
2918
3353
|
return this.writeQueue;
|