@kairos-sdk/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -28,7 +28,13 @@ var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
28
28
 
29
29
  // src/utils/uuid.ts
30
30
  function generateUUID() {
31
- return crypto.randomUUID();
31
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
32
+ return crypto.randomUUID();
33
+ }
34
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
35
+ const r = Math.random() * 16 | 0;
36
+ return (c === "x" ? r : r & 3 | 8).toString(16);
37
+ });
32
38
  }
33
39
 
34
40
  // src/library/null-library.ts
@@ -84,7 +90,26 @@ var ProviderError = class extends KairosError {
84
90
  }
85
91
  };
86
92
 
93
+ // src/errors/guard-error.ts
94
+ var GuardError = class extends KairosError {
95
+ constructor(message) {
96
+ super(message);
97
+ this.name = "GuardError";
98
+ }
99
+ };
100
+
87
101
  // src/utils/retry.ts
102
+ function isTransientNetworkError(err) {
103
+ const TRANSIENT_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "ENOTFOUND", "ECONNABORTED"]);
104
+ let current = err;
105
+ for (let i = 0; i < 4; i++) {
106
+ if (current === null || typeof current !== "object") break;
107
+ const code = current.code;
108
+ if (typeof code === "string" && TRANSIENT_CODES.has(code)) return true;
109
+ current = current.cause;
110
+ }
111
+ return false;
112
+ }
88
113
  async function withRetry(fn, maxAttempts, delayMs, shouldRetry) {
89
114
  let lastError;
90
115
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
@@ -109,6 +134,7 @@ function fetchWithTimeout(url, init, timeoutMs) {
109
134
 
110
135
  // src/providers/n8n/api-client.ts
111
136
  var EXECUTION_LIMIT_CAP = 100;
137
+ var N8N_API_PAGE_SIZE = 250;
112
138
  var REQUEST_TIMEOUT_MS = 3e4;
113
139
  var RETRY_ATTEMPTS = 3;
114
140
  var RETRY_DELAY_MS = 1e3;
@@ -117,6 +143,17 @@ var N8nApiClient = class {
117
143
  this.baseUrl = baseUrl;
118
144
  this.apiKey = apiKey;
119
145
  this.logger = logger;
146
+ if (!baseUrl || typeof baseUrl !== "string") {
147
+ throw new GuardError("N8nApiClient: baseUrl must be a non-empty string");
148
+ }
149
+ try {
150
+ new URL(baseUrl);
151
+ } catch {
152
+ throw new GuardError(`N8nApiClient: baseUrl is not a valid URL: "${baseUrl}"`);
153
+ }
154
+ if (!apiKey || typeof apiKey !== "string") {
155
+ throw new GuardError("N8nApiClient: apiKey must be a non-empty string");
156
+ }
120
157
  }
121
158
  baseUrl;
122
159
  apiKey;
@@ -126,7 +163,12 @@ var N8nApiClient = class {
126
163
  this.logger.debug(`n8n ${method} ${path}`);
127
164
  const isSafe = method === "GET";
128
165
  if (!isSafe) {
129
- return this.singleRequest(url, method, path, body);
166
+ return withRetry(
167
+ () => this.singleRequest(url, method, path, body),
168
+ 2,
169
+ RETRY_DELAY_MS,
170
+ isTransientNetworkError
171
+ );
130
172
  }
131
173
  return withRetry(
132
174
  () => this.singleRequest(url, method, path, body),
@@ -181,7 +223,7 @@ var N8nApiClient = class {
181
223
  }
182
224
  async listWorkflows() {
183
225
  const all = [];
184
- let path = "/workflows?limit=250";
226
+ let path = `/workflows?limit=${N8N_API_PAGE_SIZE}`;
185
227
  for (; ; ) {
186
228
  const response = await this.request("GET", path);
187
229
  for (const w of response.data) {
@@ -195,7 +237,7 @@ var N8nApiClient = class {
195
237
  });
196
238
  }
197
239
  if (!response.nextCursor) break;
198
- path = `/workflows?limit=250&cursor=${response.nextCursor}`;
240
+ path = `/workflows?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
199
241
  }
200
242
  return all;
201
243
  }
@@ -225,14 +267,14 @@ var N8nApiClient = class {
225
267
  }
226
268
  async listTags() {
227
269
  const all = [];
228
- let path = "/tags?limit=250";
270
+ let path = `/tags?limit=${N8N_API_PAGE_SIZE}`;
229
271
  for (; ; ) {
230
272
  const response = await this.request("GET", path);
231
273
  for (const t of response.data) {
232
274
  all.push({ id: t.id, name: t.name });
233
275
  }
234
276
  if (!response.nextCursor) break;
235
- path = `/tags?limit=250&cursor=${response.nextCursor}`;
277
+ path = `/tags?limit=${N8N_API_PAGE_SIZE}&cursor=${response.nextCursor}`;
236
278
  }
237
279
  return all;
238
280
  }
@@ -256,6 +298,32 @@ var N8nApiClient = class {
256
298
  return [];
257
299
  }
258
300
  }
301
+ async triggerManual(workflowId) {
302
+ const raw = await this.request("POST", `/workflows/${workflowId}/run`);
303
+ const inner = raw["data"];
304
+ const execId = inner?.["executionId"] ?? raw["executionId"];
305
+ if (execId === void 0 || execId === null) {
306
+ throw new ProviderError(
307
+ `n8n trigger response missing executionId \u2014 got: ${JSON.stringify(raw)}`
308
+ );
309
+ }
310
+ return String(execId);
311
+ }
312
+ async triggerWebhookTest(path) {
313
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
314
+ const url = `${this.baseUrl.replace(/\/$/, "")}/webhook-test${cleanPath}`;
315
+ this.logger.debug(`n8n POST webhook-test ${cleanPath}`);
316
+ try {
317
+ const response = await fetchWithTimeout(
318
+ url,
319
+ { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) },
320
+ REQUEST_TIMEOUT_MS
321
+ );
322
+ return response.status;
323
+ } catch (err) {
324
+ throw new ProviderError(`Webhook test request failed for path "${path}"`, err);
325
+ }
326
+ }
259
327
  mapExecution(e) {
260
328
  return {
261
329
  id: e.id,
@@ -303,15 +371,9 @@ var N8nFieldStripper = class {
303
371
  }
304
372
  };
305
373
 
306
- // src/errors/guard-error.ts
307
- var GuardError = class extends KairosError {
308
- constructor(message) {
309
- super(message);
310
- this.name = "GuardError";
311
- }
312
- };
313
-
314
374
  // src/providers/n8n/provider.ts
375
+ var SMOKE_TEST_TIMEOUT_MS = 3e4;
376
+ var SMOKE_TEST_POLL_INTERVAL_MS = 1e3;
315
377
  var N8nProvider = class {
316
378
  constructor(client, stripper) {
317
379
  this.client = client;
@@ -373,6 +435,71 @@ var N8nProvider = class {
373
435
  async untag(workflowId, tagIds) {
374
436
  await this.client.untagWorkflow(workflowId, tagIds);
375
437
  }
438
+ async smokeTest(workflowId, workflow) {
439
+ const start = Date.now();
440
+ const trigger = this.detectTrigger(workflow);
441
+ if (trigger.type === "unsupported") {
442
+ return { status: "not-applicable", triggerType: "not-applicable" };
443
+ }
444
+ if (trigger.type === "manual") {
445
+ let executionId;
446
+ try {
447
+ executionId = await this.client.triggerManual(workflowId);
448
+ } catch (err) {
449
+ return { status: "error", triggerType: "manual", durationMs: Date.now() - start, error: String(err) };
450
+ }
451
+ try {
452
+ const execution = await this.pollExecution(executionId);
453
+ const durationMs = Date.now() - start;
454
+ if (execution.status === "success") {
455
+ return { status: "passed", triggerType: "manual", executionId, durationMs };
456
+ }
457
+ return {
458
+ status: "failed",
459
+ triggerType: "manual",
460
+ executionId,
461
+ durationMs,
462
+ error: `Execution ended with status: ${execution.status}`
463
+ };
464
+ } catch (err) {
465
+ return { status: "error", triggerType: "manual", executionId, durationMs: Date.now() - start, error: String(err) };
466
+ }
467
+ }
468
+ try {
469
+ const statusCode = await this.client.triggerWebhookTest(trigger.path);
470
+ const durationMs = Date.now() - start;
471
+ if (statusCode >= 200 && statusCode < 300) {
472
+ return { status: "passed", triggerType: "webhook", durationMs };
473
+ }
474
+ return { status: "failed", triggerType: "webhook", durationMs, error: `Webhook returned HTTP ${statusCode}` };
475
+ } catch (err) {
476
+ return { status: "error", triggerType: "webhook", durationMs: Date.now() - start, error: String(err) };
477
+ }
478
+ }
479
+ detectTrigger(workflow) {
480
+ for (const node of workflow.nodes) {
481
+ if (node.type === "n8n-nodes-base.manualTrigger") return { type: "manual" };
482
+ if (node.type === "n8n-nodes-base.webhook") {
483
+ const params = node.parameters;
484
+ const path = typeof params?.["path"] === "string" ? params["path"] : "webhook";
485
+ return { type: "webhook", path };
486
+ }
487
+ }
488
+ return { type: "unsupported" };
489
+ }
490
+ async pollExecution(executionId) {
491
+ const deadline = Date.now() + SMOKE_TEST_TIMEOUT_MS;
492
+ for (; ; ) {
493
+ const execution = await this.client.getExecution(executionId);
494
+ if (execution.status !== "running" && execution.status !== "waiting") {
495
+ return execution;
496
+ }
497
+ const remaining = deadline - Date.now();
498
+ if (remaining <= 0) break;
499
+ await new Promise((resolve) => setTimeout(resolve, Math.min(SMOKE_TEST_POLL_INTERVAL_MS, remaining)));
500
+ }
501
+ throw new ProviderError(`Smoke test: execution ${executionId} did not complete within ${SMOKE_TEST_TIMEOUT_MS}ms`);
502
+ }
376
503
  };
377
504
 
378
505
  // src/validation/registry.ts
@@ -475,6 +602,14 @@ var NodeRegistry = class {
475
602
  if (!def) return true;
476
603
  return def.safeTypeVersions.includes(version);
477
604
  }
605
+ // Returns true when the version is a positive integer greater than the highest
606
+ // known safe version — indicates a newer release rather than a bad value.
607
+ isVersionNewer(type, version) {
608
+ const def = this.byType.get(type);
609
+ if (!def || def.safeTypeVersions.length === 0) return false;
610
+ const max = Math.max(...def.safeTypeVersions);
611
+ return Number.isInteger(version) && version > max;
612
+ }
478
613
  getRequiredParams(type) {
479
614
  return this.byType.get(type)?.requiredParams ?? [];
480
615
  }
@@ -524,6 +659,17 @@ var N8nValidator = class {
524
659
  this.checkRule21(workflow, issues);
525
660
  this.checkRule22(workflow, issues);
526
661
  this.checkRule23(workflow, issues);
662
+ this.checkRule24(workflow, issues);
663
+ this.checkRule25(workflow, issues);
664
+ this.checkRule26(workflow, issues);
665
+ this.checkRule27(workflow, issues);
666
+ this.checkRule28(workflow, issues);
667
+ this.checkRule29(workflow, issues);
668
+ this.checkRule30(workflow, issues);
669
+ this.checkRule31(workflow, issues);
670
+ this.checkRule32(workflow, issues);
671
+ this.checkRule33(workflow, issues);
672
+ this.checkRule34(workflow, issues);
527
673
  if (Array.isArray(workflow.nodes)) {
528
674
  const nodeById = new Map(workflow.nodes.map((n) => [n.id, n.type]));
529
675
  for (const issue of issues) {
@@ -656,10 +802,14 @@ var N8nValidator = class {
656
802
  checkRule11(w, issues) {
657
803
  if (!Array.isArray(w.nodes) || typeof w.connections !== "object" || w.connections === null) return;
658
804
  const reachable = /* @__PURE__ */ new Set();
659
- for (const [, outputs] of Object.entries(w.connections)) {
805
+ const aiSubNodeSources = /* @__PURE__ */ new Set();
806
+ for (const [sourceName, outputs] of Object.entries(w.connections)) {
660
807
  if (typeof outputs !== "object" || outputs === null) continue;
661
- for (const portGroup of Object.values(outputs)) {
808
+ let hasAiPort = false;
809
+ for (const [portName, portGroup] of Object.entries(outputs)) {
662
810
  if (!Array.isArray(portGroup)) continue;
811
+ const isAiPort = portName.startsWith("ai_");
812
+ if (isAiPort) hasAiPort = true;
663
813
  for (const targets of portGroup) {
664
814
  if (!Array.isArray(targets)) continue;
665
815
  for (const target of targets) {
@@ -668,10 +818,13 @@ var N8nValidator = class {
668
818
  }
669
819
  }
670
820
  }
821
+ if (hasAiPort) aiSubNodeSources.add(sourceName);
671
822
  }
672
823
  for (const node of w.nodes) {
673
824
  if (node.type.includes("stickyNote")) continue;
674
- if (!this.isTriggerNode(node) && !reachable.has(node.name)) {
825
+ if (this.isTriggerNode(node)) continue;
826
+ if (aiSubNodeSources.has(node.name)) continue;
827
+ if (!reachable.has(node.name)) {
675
828
  this.warn(issues, 11, `Node "${node.name}" has no incoming connections and may never execute`, node.id);
676
829
  }
677
830
  }
@@ -768,19 +921,22 @@ var N8nValidator = class {
768
921
  }
769
922
  }
770
923
  }
771
- // Rule 19 (WARN): typeVersion is within known safe range for registered node types
924
+ // Rule 19 (WARN): typeVersion is within known safe range for registered node types.
925
+ // In lenient mode (KAIROS_REGISTRY_STRICT != 'true'), versions higher than the known
926
+ // max are allowed — they likely represent newer n8n releases Kairos hasn't catalogued yet.
772
927
  checkRule19(w, issues) {
773
928
  if (!Array.isArray(w.nodes)) return;
929
+ const strict = process.env["KAIROS_REGISTRY_STRICT"] === "true";
774
930
  for (const node of w.nodes) {
775
931
  if (typeof node.type !== "string" || typeof node.typeVersion !== "number") continue;
776
- if (!this.registry.isVersionSafe(node.type, node.typeVersion)) {
777
- this.warn(
778
- issues,
779
- 19,
780
- `Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
781
- node.id
782
- );
783
- }
932
+ if (this.registry.isVersionSafe(node.type, node.typeVersion)) continue;
933
+ if (!strict && this.registry.isVersionNewer(node.type, node.typeVersion)) continue;
934
+ this.warn(
935
+ issues,
936
+ 19,
937
+ `Node "${node.name}" uses typeVersion ${node.typeVersion} for type "${node.type}" which is not in the known safe list`,
938
+ node.id
939
+ );
784
940
  }
785
941
  }
786
942
  // Rule 20 (WARN): cycle detection — no node should be reachable from itself
@@ -829,6 +985,27 @@ var N8nValidator = class {
829
985
  }
830
986
  }
831
987
  }
988
+ // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
989
+ checkRule21(w, issues) {
990
+ if (!Array.isArray(w.nodes)) return;
991
+ const webhooksNeedingResponse = w.nodes.filter((n) => {
992
+ if (!n.type.includes("webhook")) return false;
993
+ const params = n.parameters;
994
+ return params?.responseMode === "responseNode";
995
+ });
996
+ if (webhooksNeedingResponse.length === 0) return;
997
+ const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
998
+ if (!hasRespondNode) {
999
+ for (const wh of webhooksNeedingResponse) {
1000
+ this.warn(
1001
+ issues,
1002
+ 21,
1003
+ `Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
1004
+ wh.id
1005
+ );
1006
+ }
1007
+ }
1008
+ }
832
1009
  // Rule 22 (WARN): check requiredParams from registry
833
1010
  checkRule22(w, issues) {
834
1011
  if (!Array.isArray(w.nodes)) return;
@@ -867,23 +1044,232 @@ var N8nValidator = class {
867
1044
  }
868
1045
  }
869
1046
  }
870
- // Rule 21 (WARN): webhook with responseMode="responseNode" must have respondToWebhook node
871
- checkRule21(w, issues) {
1047
+ // Rule 24 (WARN): deprecated accessor syntax in expressions
1048
+ checkRule24(w, issues) {
872
1049
  if (!Array.isArray(w.nodes)) return;
873
- const webhooksNeedingResponse = w.nodes.filter((n) => {
874
- if (!n.type.includes("webhook")) return false;
875
- const params = n.parameters;
876
- return params?.responseMode === "responseNode";
877
- });
878
- if (webhooksNeedingResponse.length === 0) return;
879
- const hasRespondNode = w.nodes.some((n) => n.type.includes("respondToWebhook"));
880
- if (!hasRespondNode) {
881
- for (const wh of webhooksNeedingResponse) {
1050
+ const deprecated = /\$node\s*\[/;
1051
+ for (const node of w.nodes) {
1052
+ for (const expr of this.extractExpressions(node.parameters)) {
1053
+ if (deprecated.test(expr)) {
1054
+ this.warn(
1055
+ issues,
1056
+ 24,
1057
+ `Node "${node.name}" uses deprecated accessor $node["..."] \u2014 use $('NodeName').item.json.field instead`,
1058
+ node.id
1059
+ );
1060
+ break;
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+ // Rule 25 (WARN): wrong item index assumptions in expressions
1066
+ checkRule25(w, issues) {
1067
+ if (!Array.isArray(w.nodes)) return;
1068
+ const itemIndex = /\$json\s*\.\s*items\s*\[/;
1069
+ for (const node of w.nodes) {
1070
+ for (const expr of this.extractExpressions(node.parameters)) {
1071
+ if (itemIndex.test(expr)) {
1072
+ this.warn(
1073
+ issues,
1074
+ 25,
1075
+ `Node "${node.name}" accesses $json.items[n] \u2014 n8n flattens items automatically, use $json.field directly`,
1076
+ node.id
1077
+ );
1078
+ break;
1079
+ }
1080
+ }
1081
+ }
1082
+ }
1083
+ // Rule 26 (WARN): missing .first() or .all() on node references
1084
+ checkRule26(w, issues) {
1085
+ if (!Array.isArray(w.nodes)) return;
1086
+ const bareRef = /\$\(\s*'[^']+'\s*\)\s*\.json/;
1087
+ for (const node of w.nodes) {
1088
+ for (const expr of this.extractExpressions(node.parameters)) {
1089
+ if (bareRef.test(expr)) {
1090
+ this.warn(
1091
+ issues,
1092
+ 26,
1093
+ `Node "${node.name}" references $('NodeName').json without .first() or .all() \u2014 use $('NodeName').first().json.field`,
1094
+ node.id
1095
+ );
1096
+ break;
1097
+ }
1098
+ }
1099
+ }
1100
+ }
1101
+ extractExpressions(params) {
1102
+ const expressions = [];
1103
+ const walk = (val) => {
1104
+ if (typeof val === "string") {
1105
+ if (val.includes("={{") || val.includes("$node") || val.includes("$('")) {
1106
+ expressions.push(val);
1107
+ }
1108
+ } else if (Array.isArray(val)) {
1109
+ for (const item of val) walk(item);
1110
+ } else if (val !== null && typeof val === "object") {
1111
+ for (const v of Object.values(val)) walk(v);
1112
+ }
1113
+ };
1114
+ walk(params);
1115
+ return expressions;
1116
+ }
1117
+ // Rule 27 (WARN): httpRequest URL is a placeholder
1118
+ checkRule27(w, issues) {
1119
+ if (!Array.isArray(w.nodes)) return;
1120
+ const PLACEHOLDER_RE = [
1121
+ /^https?:\/\/example\.com/i,
1122
+ /your[-_]?(api[-_]?)?url/i,
1123
+ /^https?:\/\/$/,
1124
+ /^<.+>$/,
1125
+ /placeholder/i
1126
+ ];
1127
+ for (const node of w.nodes) {
1128
+ if (node.type !== "n8n-nodes-base.httpRequest") continue;
1129
+ const params = node.parameters;
1130
+ const url = params?.["url"];
1131
+ if (typeof url !== "string" || url.trim() === "") continue;
1132
+ if (PLACEHOLDER_RE.some((re) => re.test(url.trim()))) {
882
1133
  this.warn(
883
1134
  issues,
884
- 21,
885
- `Webhook "${wh.name}" uses responseMode "responseNode" but no respondToWebhook node exists in the workflow`,
886
- wh.id
1135
+ 27,
1136
+ `Node "${node.name}" httpRequest URL appears to be a placeholder: "${url}" \u2014 replace with your actual endpoint`,
1137
+ node.id
1138
+ );
1139
+ }
1140
+ }
1141
+ }
1142
+ // Rule 28 (WARN): code node with empty or comment-only code
1143
+ checkRule28(w, issues) {
1144
+ if (!Array.isArray(w.nodes)) return;
1145
+ for (const node of w.nodes) {
1146
+ if (node.type !== "n8n-nodes-base.code") continue;
1147
+ const params = node.parameters;
1148
+ const jsCode = typeof params?.["jsCode"] === "string" ? params["jsCode"] : "";
1149
+ const pythonCode = typeof params?.["pythonCode"] === "string" ? params["pythonCode"] : "";
1150
+ const code = jsCode || pythonCode;
1151
+ const stripped = code.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/#[^\n]*/g, "").trim();
1152
+ if (!stripped) {
1153
+ this.warn(issues, 28, `Node "${node.name}" code node has no executable code`, node.id);
1154
+ }
1155
+ }
1156
+ }
1157
+ // Rule 29 (WARN): slack node message operation missing channel
1158
+ checkRule29(w, issues) {
1159
+ if (!Array.isArray(w.nodes)) return;
1160
+ for (const node of w.nodes) {
1161
+ if (node.type !== "n8n-nodes-base.slack") continue;
1162
+ const params = node.parameters;
1163
+ const resource = params?.["resource"];
1164
+ const operation = params?.["operation"];
1165
+ const isMessageOp = resource === "message" || operation === "sendMessage" || operation === "post";
1166
+ if (!isMessageOp) continue;
1167
+ const channel = params?.["channel"] ?? params?.["channelId"];
1168
+ const rlValue = typeof channel === "object" && channel !== null ? channel["value"] : void 0;
1169
+ const isEmpty = channel === void 0 || channel === null || typeof channel === "string" && channel.trim() === "" || typeof channel === "object" && (!rlValue || typeof rlValue === "string" && rlValue.trim() === "");
1170
+ if (isEmpty) {
1171
+ this.warn(issues, 29, `Node "${node.name}" Slack message has no channel specified`, node.id);
1172
+ }
1173
+ }
1174
+ }
1175
+ // Rule 30 (WARN): gmail node send operation missing recipient
1176
+ checkRule30(w, issues) {
1177
+ if (!Array.isArray(w.nodes)) return;
1178
+ for (const node of w.nodes) {
1179
+ if (node.type !== "n8n-nodes-base.gmail") continue;
1180
+ const params = node.parameters;
1181
+ const operation = params?.["operation"];
1182
+ if (operation !== "send") continue;
1183
+ const to = params?.["to"] ?? params?.["toList"];
1184
+ const isEmpty = to === void 0 || to === null || typeof to === "string" && to.trim() === "" || Array.isArray(to) && to.length === 0;
1185
+ if (isEmpty) {
1186
+ this.warn(issues, 30, `Node "${node.name}" gmail send has no recipient (to) specified`, node.id);
1187
+ }
1188
+ }
1189
+ }
1190
+ // Rule 31 (WARN): if node with empty conditions
1191
+ checkRule31(w, issues) {
1192
+ if (!Array.isArray(w.nodes)) return;
1193
+ for (const node of w.nodes) {
1194
+ if (node.type !== "n8n-nodes-base.if") continue;
1195
+ const params = node.parameters;
1196
+ const conditions = params?.["conditions"];
1197
+ if (conditions === void 0 || conditions === null) {
1198
+ this.warn(issues, 31, `Node "${node.name}" if node has no conditions defined`, node.id);
1199
+ continue;
1200
+ }
1201
+ if (typeof conditions === "object" && !Array.isArray(conditions)) {
1202
+ const conds = conditions["conditions"];
1203
+ if (!Array.isArray(conds) || conds.length === 0) {
1204
+ this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
1205
+ }
1206
+ } else if (Array.isArray(conditions) && conditions.length === 0) {
1207
+ this.warn(issues, 31, `Node "${node.name}" if node conditions array is empty`, node.id);
1208
+ }
1209
+ }
1210
+ }
1211
+ // Rule 32 (WARN): set node with no assignments
1212
+ checkRule32(w, issues) {
1213
+ if (!Array.isArray(w.nodes)) return;
1214
+ for (const node of w.nodes) {
1215
+ if (node.type !== "n8n-nodes-base.set") continue;
1216
+ const params = node.parameters;
1217
+ const assignmentsObj = params?.["assignments"];
1218
+ const assignmentsArr = assignmentsObj?.["assignments"];
1219
+ const valuesObj = params?.["values"];
1220
+ const hasV1 = valuesObj && Object.values(valuesObj).some((v) => Array.isArray(v) && v.length > 0);
1221
+ const hasV3 = Array.isArray(assignmentsArr) && assignmentsArr.length > 0;
1222
+ if (!hasV1 && !hasV3) {
1223
+ this.warn(
1224
+ issues,
1225
+ 32,
1226
+ `Node "${node.name}" set node has no fields defined \u2014 it will pass data through unchanged`,
1227
+ node.id
1228
+ );
1229
+ }
1230
+ }
1231
+ }
1232
+ // Rule 33 (WARN): scheduleTrigger with no schedule rules
1233
+ checkRule33(w, issues) {
1234
+ if (!Array.isArray(w.nodes)) return;
1235
+ for (const node of w.nodes) {
1236
+ if (node.type !== "n8n-nodes-base.scheduleTrigger") continue;
1237
+ const params = node.parameters;
1238
+ const rule = params?.["rule"];
1239
+ const intervals = rule?.["interval"];
1240
+ if (!Array.isArray(intervals) || intervals.length === 0) {
1241
+ this.warn(issues, 33, `Node "${node.name}" scheduleTrigger has no schedule rules defined`, node.id);
1242
+ }
1243
+ }
1244
+ }
1245
+ // Rule 34 (WARN): webhook path contains spaces, starts with slash, or looks like a full URL
1246
+ checkRule34(w, issues) {
1247
+ if (!Array.isArray(w.nodes)) return;
1248
+ for (const node of w.nodes) {
1249
+ if (node.type !== "n8n-nodes-base.webhook") continue;
1250
+ const params = node.parameters;
1251
+ const path = params?.["path"];
1252
+ if (typeof path !== "string") continue;
1253
+ if (/\s/.test(path)) {
1254
+ this.warn(
1255
+ issues,
1256
+ 34,
1257
+ `Node "${node.name}" webhook path contains spaces: "${path}" \u2014 use hyphens or underscores instead`,
1258
+ node.id
1259
+ );
1260
+ } else if (/^https?:\/\//i.test(path)) {
1261
+ this.warn(
1262
+ issues,
1263
+ 34,
1264
+ `Node "${node.name}" webhook path looks like a full URL \u2014 it should be a relative path (e.g. "my-hook")`,
1265
+ node.id
1266
+ );
1267
+ } else if (path.startsWith("/")) {
1268
+ this.warn(
1269
+ issues,
1270
+ 34,
1271
+ `Node "${node.name}" webhook path starts with "/" \u2014 n8n adds the leading slash automatically`,
1272
+ node.id
887
1273
  );
888
1274
  }
889
1275
  }
@@ -954,9 +1340,11 @@ id, active, createdAt, updatedAt, versionId, meta, isArchived, activeVersionId,
954
1340
  - Never reuse IDs, never use sequential fake IDs like "node-1"
955
1341
 
956
1342
  ### Credentials:
957
- - Only reference credentials with exact type names (see catalog below)
958
- - If credential ID is unknown, OMIT the credentials block entirely \u2014 never invent credential IDs
959
- - Never put API keys or tokens in parameters when a credential type exists
1343
+ - Each credential is keyed by its type string, with an object value containing id and name:
1344
+ "credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack Credential" } }
1345
+ - Use "placeholder-id" as the id \u2014 users replace this with their real credential ID from n8n after deployment
1346
+ - The credentialsNeeded field in your response declares what credentials the user must configure
1347
+ - Never put API keys or tokens directly in node parameters when a credential type exists
960
1348
 
961
1349
  ### Node names:
962
1350
  - All node names must be unique within the workflow
@@ -1003,6 +1391,23 @@ Node parameters like conditions, assignments, and rule intervals MUST include al
1003
1391
 
1004
1392
  ---
1005
1393
 
1394
+ ## EXPRESSION SYNTAX \u2014 how to reference upstream node data
1395
+
1396
+ ### Accessing a field from an upstream node:
1397
+ - CORRECT: $('NodeName').item.json.field
1398
+ - WRONG: $node["NodeName"].json.field \u2190 deprecated accessor, fails at runtime (Rule 24)
1399
+
1400
+ ### Accessing array items from $json:
1401
+ - CORRECT: $json.field \u2190 n8n auto-flattens items; each item is already a flat object
1402
+ - WRONG: $json.items[0].field \u2190 do not index into items[] (Rule 25)
1403
+
1404
+ ### Calling node data \u2014 always qualify with .first() or .all():
1405
+ - CORRECT: $('NodeName').first().json.field \u2190 single item
1406
+ - CORRECT: $('NodeName').all() \u2190 array of all items
1407
+ - WRONG: $('NodeName').json \u2190 throws at runtime without .first() or .all() (Rule 26)
1408
+
1409
+ ---
1410
+
1006
1411
  ## NODE CATALOG \u2014 exact type strings and safe typeVersions
1007
1412
 
1008
1413
  ### Triggers (always at least one required):
@@ -1102,6 +1507,17 @@ Cron: { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 *
1102
1507
  5. At least one trigger node present
1103
1508
  6. Every AI Agent has an ai_languageModel sub-node
1104
1509
  7. settings block is complete with executionOrder: "v1"
1510
+ 8. No deprecated $node["NodeName"].json \u2014 use $('NodeName').item.json.field
1511
+ 9. No $json.items[0] array indexing \u2014 access fields directly as $json.field
1512
+ 10. No bare $('NodeName').json \u2014 always use .first().json.field or .all()
1513
+ 11. httpRequest URL is a real endpoint (not "example.com" or "YOUR_URL")
1514
+ 12. code nodes contain actual logic \u2014 not empty or comment-only
1515
+ 13. Slack message nodes have a channel specified (channelId or channel)
1516
+ 14. Gmail send nodes have a recipient (to field non-empty)
1517
+ 15. if nodes have at least one condition in conditions.conditions[]
1518
+ 16. set nodes have at least one entry in assignments.assignments[]
1519
+ 17. scheduleTrigger has at least one rule in rule.interval[]
1520
+ 18. webhook path is relative (no spaces, no leading slash, no http://)
1105
1521
 
1106
1522
  ---
1107
1523
 
@@ -1118,7 +1534,7 @@ function scoreToMode(score) {
1118
1534
  }
1119
1535
 
1120
1536
  // src/validation/rule-metadata.ts
1121
- var VALIDATOR_RULE_IDS = Array.from({ length: 23 }, (_, i) => i + 1);
1537
+ var VALIDATOR_RULE_IDS = Array.from({ length: 34 }, (_, i) => i + 1);
1122
1538
  var RULE_PIPELINE_STAGES = {
1123
1539
  1: "node_generation",
1124
1540
  2: "node_generation",
@@ -1142,7 +1558,68 @@ var RULE_PIPELINE_STAGES = {
1142
1558
  20: "connection_wiring",
1143
1559
  21: "workflow_structure",
1144
1560
  22: "workflow_structure",
1145
- 23: "node_generation"
1561
+ 23: "node_generation",
1562
+ 24: "expression_syntax",
1563
+ 25: "expression_syntax",
1564
+ 26: "expression_syntax",
1565
+ 27: "node_generation",
1566
+ 28: "node_generation",
1567
+ 29: "node_generation",
1568
+ 30: "node_generation",
1569
+ 31: "node_generation",
1570
+ 32: "node_generation",
1571
+ 33: "node_generation",
1572
+ 34: "node_generation"
1573
+ };
1574
+ var RULE_EXAMPLES = {
1575
+ 17: {
1576
+ bad: '"credentials": { "slackOAuth2Api": "my-token" }',
1577
+ good: '"credentials": { "slackOAuth2Api": { "id": "placeholder-id", "name": "My Slack OAuth" } }'
1578
+ },
1579
+ 24: {
1580
+ bad: '$node["Fetch Data"].json.email',
1581
+ good: "$('Fetch Data').item.json.email"
1582
+ },
1583
+ 25: {
1584
+ bad: "$json.items[0].email",
1585
+ good: "$json.email"
1586
+ },
1587
+ 26: {
1588
+ bad: "$('Fetch Data').json.email",
1589
+ good: "$('Fetch Data').first().json.email"
1590
+ },
1591
+ 27: {
1592
+ bad: '"url": "https://example.com/api/data"',
1593
+ good: '"url": "https://api.yourservice.com/v1/endpoint"'
1594
+ },
1595
+ 28: {
1596
+ bad: '"jsCode": "// TODO: implement this"',
1597
+ good: '"jsCode": "return items.map(item => ({ json: { result: item.json.value * 2 } }))"'
1598
+ },
1599
+ 29: {
1600
+ bad: '"channelId": ""',
1601
+ good: '"channelId": { "__rl": true, "value": "C0123456789", "mode": "id" }'
1602
+ },
1603
+ 30: {
1604
+ bad: '"operation": "send", "to": ""',
1605
+ good: '"operation": "send", "to": "recipient@example.com"'
1606
+ },
1607
+ 31: {
1608
+ bad: '"conditions": { "combinator": "and", "conditions": [] }',
1609
+ good: '"conditions": { "combinator": "and", "conditions": [{ "leftValue": "={{ $json.status }}", "rightValue": "active", "operator": { "type": "string", "operation": "equals" } }] }'
1610
+ },
1611
+ 32: {
1612
+ bad: '"assignments": { "assignments": [] }',
1613
+ good: '"assignments": { "assignments": [{ "id": "f1", "name": "status", "value": "processed", "type": "string" }] }'
1614
+ },
1615
+ 33: {
1616
+ bad: '"rule": { "interval": [] }',
1617
+ good: '"rule": { "interval": [{ "field": "cronExpression", "expression": "0 9 * * 1-5" }] }'
1618
+ },
1619
+ 34: {
1620
+ bad: '"path": "/my webhook"',
1621
+ good: '"path": "my-webhook"'
1622
+ }
1146
1623
  };
1147
1624
  var RULE_MITIGATIONS = {
1148
1625
  1: "Provide a non-empty workflow name string",
@@ -1161,36 +1638,86 @@ var RULE_MITIGATIONS = {
1161
1638
  14: "Include at least one trigger node (e.g. scheduleTrigger, webhookTrigger, manualTrigger, or service-specific)",
1162
1639
  15: 'Node type strings must be fully qualified: "n8n-nodes-base.httpRequest" not just "httpRequest"',
1163
1640
  16: "All node names must be unique within the workflow",
1164
- 17: 'Credentials must be an object with non-empty string id and name fields: { id: "placeholder-id", name: "My Credential" }',
1641
+ 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',
1165
1642
  18: "AI sub-nodes (languageModel, memory, tool) must be the CONNECTION SOURCE pointing TO the agent \u2014 not the reverse",
1166
1643
  19: "Use known safe typeVersion values for each node type",
1167
1644
  20: "Remove connection cycles \u2014 ensure no node can reach itself through the connection graph",
1168
1645
  21: 'When using webhook with responseMode "responseNode", include a respondToWebhook node in the flow',
1169
1646
  22: "Ensure all required parameters are set for each node type (e.g. webhook needs httpMethod and path)",
1170
- 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync"
1647
+ 23: "Use node types that exist in the n8n registry \u2014 check with kairos_sync",
1648
+ 24: 'Use modern accessor syntax: $("NodeName").item.json.field instead of deprecated $node["NodeName"].json.field',
1649
+ 25: "Access item fields directly with $json.field \u2014 n8n flattens items automatically, do not use $json.items[0]",
1650
+ 26: 'Use $("NodeName").first().json.field or $("NodeName").all() \u2014 bare $("NodeName").json without .first() or .all() throws at runtime',
1651
+ 27: 'Replace placeholder URLs with your actual API endpoint \u2014 do not use "example.com" or "YOUR_URL" patterns',
1652
+ 28: "Add executable code to the code node \u2014 empty or comment-only code nodes do nothing at runtime",
1653
+ 29: "Set the channel parameter for Slack message operations (channelId with __rl object, or channel as string)",
1654
+ 30: "Set the to parameter for Gmail send operations with at least one recipient email address",
1655
+ 31: "Add at least one condition to the if node \u2014 conditions.conditions array must be non-empty",
1656
+ 32: "Add field assignments to the set node \u2014 assignments.assignments array must be non-empty for typeVersion 3.x",
1657
+ 33: "Add at least one schedule rule to scheduleTrigger \u2014 rule.interval array must have at least one entry",
1658
+ 34: 'Webhook path must be a relative path without spaces, leading slashes, or protocol prefixes (e.g. "my-hook")'
1171
1659
  };
1172
1660
 
1173
1661
  // src/generation/prompt-builder.ts
1174
1662
  var CRITICAL_SCORE_THRESHOLD = 0.15;
1663
+ function resolveProfile() {
1664
+ const env = process.env["KAIROS_PROMPT_PROFILE"];
1665
+ if (env === "minimal" || env === "standard" || env === "rich") return env;
1666
+ return "standard";
1667
+ }
1668
+ var PROACTIVE_EXPRESSION_GUIDANCE = `## Expression Syntax Quick Reference
1669
+
1670
+ Always use these patterns in expressions:
1671
+ - Access node data: $('NodeName').item.json.field (not $node["NodeName"].json)
1672
+ - Access JSON field: $json.field (not $json.items[0].field)
1673
+ - Single item: $('NodeName').first().json.field
1674
+ - All items: $('NodeName').all()`;
1175
1675
  var PromptBuilder = class {
1176
1676
  patternsPath;
1177
- constructor(patternsPath) {
1677
+ profile;
1678
+ _lastActivePatterns = null;
1679
+ constructor(patternsPath, profile) {
1178
1680
  this.patternsPath = patternsPath ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".kairos", "patterns.json");
1681
+ this.profile = profile ?? resolveProfile();
1682
+ }
1683
+ resolveMaxPatterns() {
1684
+ if (this.profile === "minimal") return 3;
1685
+ if (this.profile === "rich") return 15;
1686
+ return 10;
1179
1687
  }
1180
1688
  build(request, matches, globalFailureRates = [], dynamicCatalog) {
1181
1689
  const mode = this.resolveMode(matches);
1182
- const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog);
1690
+ const system = this.buildSystem(matches, mode, globalFailureRates, dynamicCatalog, request.description);
1183
1691
  const userMessage = this.buildUserMessage(request, matches, mode);
1184
1692
  return { system, userMessage, mode, matches };
1185
1693
  }
1186
- buildCorrectionMessage(request, matches, allIssues, attempt) {
1694
+ buildCorrectionMessage(request, matches, allIssues, attempt, failingRuleIds) {
1187
1695
  const base = this.buildUserMessage(request, matches, this.resolveMode(matches));
1696
+ let examplesSection = "";
1697
+ if (failingRuleIds && failingRuleIds.length > 0) {
1698
+ const uniqueRules = [...new Set(failingRuleIds)];
1699
+ const exampleLines = [];
1700
+ for (const rule of uniqueRules) {
1701
+ const ex = RULE_EXAMPLES[rule];
1702
+ if (ex) {
1703
+ exampleLines.push(`Rule ${rule}:
1704
+ Bad: ${ex.bad}
1705
+ Good: ${ex.good}`);
1706
+ }
1707
+ }
1708
+ if (exampleLines.length > 0) {
1709
+ examplesSection = `
1710
+
1711
+ ## Concrete Fix Examples
1712
+ ${exampleLines.join("\n\n")}`;
1713
+ }
1714
+ }
1188
1715
  return `${base}
1189
1716
 
1190
1717
  IMPORTANT: A previous generation attempt (attempt ${attempt}) failed validation with these issues:
1191
1718
  ${allIssues.join("\n")}
1192
1719
 
1193
- Fix ALL of the above issues in your new response. Do not repeat any of these mistakes.`;
1720
+ Fix ALL of the above issues in your new response. Do not repeat any of these mistakes.${examplesSection}`;
1194
1721
  }
1195
1722
  resolveMode(matches) {
1196
1723
  if (matches.length === 0) return "scratch";
@@ -1198,7 +1725,7 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
1198
1725
  if (!top) return "scratch";
1199
1726
  return scoreToMode(top.score);
1200
1727
  }
1201
- buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog) {
1728
+ buildSystem(matches, mode, globalFailureRates = [], dynamicCatalog, description) {
1202
1729
  let basePrompt = SYSTEM_PROMPT_V1;
1203
1730
  if (dynamicCatalog) {
1204
1731
  basePrompt = basePrompt.replace(
@@ -1213,53 +1740,62 @@ Fix ALL of the above issues in your new response. Do not repeat any of these mis
1213
1740
  cache_control: { type: "ephemeral" }
1214
1741
  }
1215
1742
  ];
1216
- if (mode === "reference" && matches.length > 0) {
1217
- const refText = matches.slice(0, 3).map((m) => {
1218
- const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1219
- return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1743
+ if (this.profile !== "minimal") {
1744
+ if (mode === "reference" && matches.length > 0) {
1745
+ const refText = matches.slice(0, 3).map((m) => {
1746
+ const nodes = m.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1747
+ return `Reference workflow: "${m.workflow.description}" (similarity: ${m.score.toFixed(2)})
1220
1748
  Nodes:
1221
1749
  ${nodes}`;
1222
- }).join("\n\n");
1223
- blocks.push({
1224
- type: "text",
1225
- text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1226
-
1227
- ${refText}`
1228
- });
1229
- }
1230
- if (mode === "direct" && matches[0]) {
1231
- const match = matches[0];
1232
- const json = JSON.stringify(match.workflow.workflow, null, 2);
1233
- if (json.length > 3e4) {
1234
- const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1750
+ }).join("\n\n");
1235
1751
  blocks.push({
1236
1752
  type: "text",
1237
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1753
+ text: `## Similar Workflows From Library (for reference only \u2014 adapt, do not copy verbatim)
1754
+
1755
+ ${refText}`
1756
+ });
1757
+ }
1758
+ if (mode === "direct" && matches[0]) {
1759
+ const match = matches[0];
1760
+ const json = JSON.stringify(match.workflow.workflow, null, 2);
1761
+ if (json.length > 3e4) {
1762
+ const nodes = match.workflow.workflow.nodes.map((n) => ` - ${n.name} (${n.type} v${n.typeVersion})`).join("\n");
1763
+ blocks.push({
1764
+ type: "text",
1765
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 too large for full JSON, using reference:
1238
1766
  Nodes:
1239
1767
  ${nodes}`
1240
- });
1241
- } else {
1242
- blocks.push({
1243
- type: "text",
1244
- text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1768
+ });
1769
+ } else {
1770
+ blocks.push({
1771
+ type: "text",
1772
+ text: `## Closely Matched Workflow (score: ${match.score.toFixed(2)}) \u2014 adapt this structure:
1245
1773
 
1246
1774
  ${json}`
1247
- });
1775
+ });
1776
+ }
1248
1777
  }
1249
- }
1250
- if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1251
- const hint = matches[0];
1252
- const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1253
- blocks.push({
1254
- type: "text",
1255
- text: `## Weak Structural Hint
1778
+ if (mode === "scratch" && matches.length > 0 && matches[0].score >= 0.4) {
1779
+ const hint = matches[0];
1780
+ const nodeTypes = hint.workflow.workflow.nodes.map((n) => n.type.split(".").pop()).join(", ");
1781
+ blocks.push({
1782
+ type: "text",
1783
+ text: `## Weak Structural Hint
1256
1784
  A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node types: ${nodeTypes}`
1257
- });
1785
+ });
1786
+ }
1258
1787
  }
1259
- const warnings = this.buildFailureWarnings(matches, globalFailureRates);
1788
+ const warnings = this.buildFailureWarnings(matches, globalFailureRates, description);
1260
1789
  if (warnings) {
1261
1790
  blocks.push({ type: "text", text: warnings });
1262
1791
  }
1792
+ if (this.profile === "rich") {
1793
+ const expressionRules = /* @__PURE__ */ new Set([24, 25, 26]);
1794
+ const expressionAlreadyCovered = (this._lastActivePatterns ?? []).some((p) => expressionRules.has(p.rule));
1795
+ if (!expressionAlreadyCovered) {
1796
+ blocks.push({ type: "text", text: PROACTIVE_EXPRESSION_GUIDANCE });
1797
+ }
1798
+ }
1263
1799
  return blocks;
1264
1800
  }
1265
1801
  loadPatterns() {
@@ -1273,18 +1809,38 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1273
1809
  }
1274
1810
  }
1275
1811
  getWarnedRules() {
1276
- return this.getActivePatterns().map((p) => p.rule);
1812
+ const patterns = this._lastActivePatterns ?? this.getActivePatterns(this.resolveMaxPatterns());
1813
+ return patterns.map((p) => p.rule);
1277
1814
  }
1278
- getActivePatterns() {
1279
- const MAX_WARNED = 10;
1815
+ getActivePatterns(maxCount = 10, description) {
1280
1816
  const all = this.loadPatterns().filter((p) => p.state !== "resolved" && p.confidence > 0);
1281
1817
  const regressed = all.filter((p) => p.regressed).sort((a, b) => b.compositeScore - a.compositeScore);
1282
1818
  const confirmed = all.filter((p) => !p.regressed && p.state === "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1283
1819
  const drafts = all.filter((p) => !p.regressed && p.state !== "confirmed").sort((a, b) => b.compositeScore - a.compositeScore);
1284
- return [...regressed, ...confirmed, ...drafts].slice(0, MAX_WARNED);
1285
- }
1286
- buildFailureWarnings(matches, globalFailureRates) {
1287
- const richPatterns = this.getActivePatterns();
1820
+ const ordered = [...regressed, ...confirmed, ...drafts];
1821
+ if (this.profile === "minimal" && description) {
1822
+ return this.rankByRelevance(ordered, description).slice(0, maxCount);
1823
+ }
1824
+ return ordered.slice(0, maxCount);
1825
+ }
1826
+ rankByRelevance(patterns, description) {
1827
+ const lower = description.toLowerCase();
1828
+ const STAGE_KEYWORDS = {
1829
+ credential_injection: ["credential", "auth", "api key", "token", "oauth", "smtp", "imap", "password", "secret"],
1830
+ connection_wiring: ["connect", "link", "wire", "chain", "merge", "branch", "join"],
1831
+ expression_syntax: ["expression", "variable", "json", "field", "data", "$json", "item"],
1832
+ workflow_structure: ["trigger", "webhook", "schedule", "structure", "workflow"],
1833
+ node_generation: ["node", "generate", "create", "build", "send", "fetch", "email", "slack", "http"]
1834
+ };
1835
+ return patterns.map((p) => {
1836
+ const keywords = STAGE_KEYWORDS[p.pipelineStage] ?? [];
1837
+ const relevanceBoost = keywords.some((kw) => lower.includes(kw)) ? 1 : 0;
1838
+ return { pattern: p, sort: relevanceBoost * 10 + p.compositeScore };
1839
+ }).sort((a, b) => b.sort - a.sort).map((x) => x.pattern);
1840
+ }
1841
+ buildFailureWarnings(matches, globalFailureRates, description) {
1842
+ const richPatterns = this.getActivePatterns(this.resolveMaxPatterns(), description);
1843
+ this._lastActivePatterns = richPatterns;
1288
1844
  if (richPatterns.length > 0) {
1289
1845
  return this.buildStageGroupedWarnings(richPatterns, matches);
1290
1846
  }
@@ -1295,7 +1851,8 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1295
1851
  credential_injection: "CREDENTIAL FORMATTING",
1296
1852
  connection_wiring: "CONNECTION WIRING",
1297
1853
  node_generation: "NODE GENERATION",
1298
- workflow_structure: "WORKFLOW STRUCTURE"
1854
+ workflow_structure: "WORKFLOW STRUCTURE",
1855
+ expression_syntax: "EXPRESSION SYNTAX"
1299
1856
  };
1300
1857
  const byStage = /* @__PURE__ */ new Map();
1301
1858
  for (const p of patterns) {
@@ -1323,7 +1880,11 @@ A loosely similar workflow (score: ${hint.score.toFixed(2)}) used these node typ
1323
1880
  const remedy = p.mitigation ?? RULE_MITIGATIONS[p.rule];
1324
1881
  const remedyStr = remedy ? `
1325
1882
  Fix: ${remedy}` : "";
1326
- lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}`);
1883
+ const ex = RULE_EXAMPLES[p.rule];
1884
+ const exampleStr = ex ? `
1885
+ Bad: ${ex.bad}
1886
+ Good: ${ex.good}` : "";
1887
+ lines.push(`- ${urgency}${statePrefix}Rule ${p.rule}${trendSuffix}: ${p.exampleMessages[0] ?? "No example"}${remedyStr}${exampleStr}`);
1327
1888
  } else {
1328
1889
  const ruleNums = group.map((p) => p.rule).join(", ");
1329
1890
  const totalFailures = group.reduce((s, p) => s + p.failureCount, 0);
@@ -1430,12 +1991,12 @@ var GENERATE_WORKFLOW_TOOL = {
1430
1991
  }
1431
1992
  };
1432
1993
  var WorkflowDesigner = class {
1433
- constructor(anthropic, model, logger) {
1994
+ constructor(anthropic, model, logger, patternsPath) {
1434
1995
  this.anthropic = anthropic;
1435
1996
  this.model = model;
1436
1997
  this.logger = logger;
1437
1998
  this.validator = new N8nValidator();
1438
- this.promptBuilder = new PromptBuilder();
1999
+ this.promptBuilder = new PromptBuilder(patternsPath);
1439
2000
  }
1440
2001
  anthropic;
1441
2002
  model;
@@ -1458,7 +2019,8 @@ var WorkflowDesigner = class {
1458
2019
  const issueLines = lastErrors.map(
1459
2020
  (i) => `- [Rule ${i.rule}] ${i.message}${i.nodeId ? ` (node: ${i.nodeId})` : ""}`
1460
2021
  );
1461
- userMessage = this.promptBuilder.buildCorrectionMessage(request, matches, issueLines, attempt - 1);
2022
+ const failingRuleIds = lastErrors.map((i) => i.rule);
2023
+ userMessage = this.promptBuilder.buildCorrectionMessage(request, matches, issueLines, attempt - 1, failingRuleIds);
1462
2024
  this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: lastErrors.length });
1463
2025
  }
1464
2026
  const start = Date.now();
@@ -1519,6 +2081,11 @@ var WorkflowDesigner = class {
1519
2081
  }
1520
2082
  }
1521
2083
  extractToolUse(message) {
2084
+ if (message.stop_reason === "max_tokens") {
2085
+ throw new GenerationError(
2086
+ "Claude response was truncated (max_tokens reached) \u2014 the workflow may be too large. Try a simpler description or break it into smaller workflows."
2087
+ );
2088
+ }
1522
2089
  const toolUseBlock = message.content.find(
1523
2090
  (block) => block.type === "tool_use"
1524
2091
  );
@@ -1561,11 +2128,12 @@ var TelemetryCollector = class {
1561
2128
  this.dir = dir ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".kairos", "telemetry");
1562
2129
  this.sessionId = generateUUID();
1563
2130
  }
1564
- async emit(eventType, data) {
2131
+ async emit(eventType, data, runId) {
1565
2132
  const event = {
1566
2133
  schemaVersion: TELEMETRY_SCHEMA_VERSION,
1567
2134
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1568
2135
  sessionId: this.sessionId,
2136
+ ...runId ? { runId } : {},
1569
2137
  eventType,
1570
2138
  data
1571
2139
  };
@@ -1638,19 +2206,20 @@ var TelemetryReader = class {
1638
2206
  }
1639
2207
  const events = await this.readRecentEvents(days);
1640
2208
  const buildSessions = new Set(
1641
- events.filter((e) => e.eventType === "build_complete").map((e) => e.sessionId)
2209
+ events.filter((e) => e.eventType === "build_complete").map((e) => e.runId ?? e.sessionId)
1642
2210
  );
1643
2211
  const MIN_BUILDS_FOR_RATES = 3;
1644
2212
  if (buildSessions.size < MIN_BUILDS_FOR_RATES) return [];
1645
2213
  const ruleSessions = /* @__PURE__ */ new Map();
1646
2214
  for (const event of events) {
1647
2215
  if (event.eventType !== "generation_attempt") continue;
1648
- if (!buildSessions.has(event.sessionId)) continue;
2216
+ const eventKey = event.runId ?? event.sessionId;
2217
+ if (!buildSessions.has(eventKey)) continue;
1649
2218
  const data = event.data;
1650
2219
  if (data.validationPassed || !data.issues) continue;
1651
2220
  for (const issue of data.issues) {
1652
2221
  const entry = ruleSessions.get(issue.rule) ?? { sessions: /* @__PURE__ */ new Set(), messages: /* @__PURE__ */ new Map() };
1653
- entry.sessions.add(event.sessionId);
2222
+ entry.sessions.add(eventKey);
1654
2223
  entry.messages.set(issue.message, (entry.messages.get(issue.message) ?? 0) + 1);
1655
2224
  ruleSessions.set(issue.rule, entry);
1656
2225
  }
@@ -1691,22 +2260,25 @@ var PATTERN_SCHEMA_VERSION = 2;
1691
2260
  var PatternAnalyzer = class _PatternAnalyzer {
1692
2261
  telemetryDir;
1693
2262
  outputDir;
2263
+ _cachedEvents = null;
2264
+ _cachedPreviousPatterns = null;
1694
2265
  constructor(telemetryDir) {
1695
2266
  const defaultDir = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos", "telemetry");
1696
2267
  this.telemetryDir = telemetryDir ?? defaultDir;
1697
2268
  this.outputDir = telemetryDir ? (0, import_node_path5.join)(telemetryDir, "..") : (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".kairos");
1698
2269
  }
1699
2270
  async loadPreviousPatterns() {
2271
+ if (this._cachedPreviousPatterns !== null) return this._cachedPreviousPatterns;
1700
2272
  try {
1701
2273
  const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "patterns.json"), "utf-8");
1702
2274
  const prev = JSON.parse(raw);
1703
2275
  const version = prev.schemaVersion ?? 0;
1704
2276
  const patterns = prev.topFailureRules ?? [];
1705
- if (version === PATTERN_SCHEMA_VERSION) return patterns;
1706
- return this.migratePatterns(patterns, version);
2277
+ this._cachedPreviousPatterns = version === PATTERN_SCHEMA_VERSION ? patterns : this.migratePatterns(patterns, version);
1707
2278
  } catch {
1708
- return [];
2279
+ this._cachedPreviousPatterns = [];
1709
2280
  }
2281
+ return this._cachedPreviousPatterns;
1710
2282
  }
1711
2283
  migratePatterns(patterns, fromVersion) {
1712
2284
  let migrated = patterns;
@@ -1719,19 +2291,23 @@ var PatternAnalyzer = class _PatternAnalyzer {
1719
2291
  }));
1720
2292
  }
1721
2293
  if (fromVersion < 2) {
1722
- migrated = migrated.map((p) => ({
1723
- ...p,
1724
- scoringFactors: {
1725
- ...p.scoringFactors,
1726
- stickinessBoost: p.scoringFactors.stickinessBoost ?? p.scoringFactors["validationBoost"] ?? 0
1727
- }
1728
- }));
2294
+ migrated = migrated.map((p) => {
2295
+ const sf = p.scoringFactors ?? { rawConfidence: 0, impact: 0, recency: 0, stickinessBoost: 0 };
2296
+ return {
2297
+ ...p,
2298
+ scoringFactors: {
2299
+ ...sf,
2300
+ stickinessBoost: sf.stickinessBoost ?? sf["validationBoost"] ?? 0
2301
+ }
2302
+ };
2303
+ });
1729
2304
  }
1730
2305
  return migrated;
1731
2306
  }
1732
2307
  async analyze(days = 30) {
1733
2308
  const previousPatterns = await this.loadPreviousPatterns();
1734
2309
  const events = await this.readAllEvents(days);
2310
+ this._cachedEvents = events;
1735
2311
  const starts = events.filter((e) => e.eventType === "build_start");
1736
2312
  const attempts = events.filter((e) => e.eventType === "generation_attempt");
1737
2313
  const passed = attempts.filter(
@@ -1744,13 +2320,18 @@ var PatternAnalyzer = class _PatternAnalyzer {
1744
2320
  const credentialFailures = /* @__PURE__ */ new Map();
1745
2321
  for (const a of failed) {
1746
2322
  const weight = this.recencyWeight(a.fileDate);
2323
+ const buildId = a.runId ?? a.sessionId;
1747
2324
  const data = a.data;
1748
2325
  for (const issue of data.issues ?? []) {
1749
- const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [] };
2326
+ if (issue.severity === "warn") continue;
2327
+ const entry = ruleFailures.get(issue.rule) ?? { count: 0, sessions: /* @__PURE__ */ new Set(), recencyWeights: [], allMessages: [], workflowTypes: /* @__PURE__ */ new Map() };
1750
2328
  entry.count++;
1751
- entry.sessions.add(a.sessionId);
2329
+ entry.sessions.add(buildId);
1752
2330
  entry.recencyWeights.push(weight);
1753
2331
  entry.allMessages.push(issue.message);
2332
+ if (data.workflowType) {
2333
+ entry.workflowTypes.set(data.workflowType, (entry.workflowTypes.get(data.workflowType) ?? 0) + 1);
2334
+ }
1754
2335
  ruleFailures.set(issue.rule, entry);
1755
2336
  if (issue.rule === 17) {
1756
2337
  const credPatterns = [
@@ -1803,9 +2384,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
1803
2384
  }
1804
2385
  const sessions = /* @__PURE__ */ new Map();
1805
2386
  for (const a of attempts) {
1806
- const list = sessions.get(a.sessionId) ?? [];
2387
+ const buildId = a.runId ?? a.sessionId;
2388
+ const list = sessions.get(buildId) ?? [];
1807
2389
  list.push(a);
1808
- sessions.set(a.sessionId, list);
2390
+ sessions.set(buildId, list);
1809
2391
  }
1810
2392
  let firstTryPass = 0;
1811
2393
  let correctionNeeded = 0;
@@ -1852,7 +2434,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1852
2434
  const avgRecency = entry.recencyWeights.length > 0 ? entry.recencyWeights.reduce((s, w) => s + w, 0) / entry.recencyWeights.length : 1;
1853
2435
  const stickiness = stickinessCount.get(rule) ?? 0;
1854
2436
  const { compositeScore, factors } = this.computeCompositeScore(rawConfidence, entry.count, state, avgRecency, stickiness);
1855
- return {
2437
+ const pattern = {
1856
2438
  rule,
1857
2439
  failureCount: entry.count,
1858
2440
  confidence: Math.round(rawConfidence * 1e3) / 1e3,
@@ -1864,6 +2446,10 @@ var PatternAnalyzer = class _PatternAnalyzer {
1864
2446
  exampleMessages: this.deduplicateMessages(entry.allMessages),
1865
2447
  mitigation: RULE_MITIGATIONS[rule] ?? null
1866
2448
  };
2449
+ if (entry.workflowTypes.size > 0) {
2450
+ pattern.workflowTypeBreakdown = Object.fromEntries(entry.workflowTypes);
2451
+ }
2452
+ return pattern;
1867
2453
  }).sort((a, b) => b.compositeScore - a.compositeScore);
1868
2454
  const activeRules = new Set(activePatterns.map((p) => p.rule));
1869
2455
  for (const p of activePatterns) {
@@ -1920,7 +2506,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1920
2506
  const warned = bcData.warnedRules ?? [];
1921
2507
  if (warned.length === 0) continue;
1922
2508
  const sessionFailedRules = /* @__PURE__ */ new Set();
1923
- const sessionAttempts = sessions.get(bc.sessionId) ?? [];
2509
+ const sessionAttempts = sessions.get(bc.runId ?? bc.sessionId) ?? [];
1924
2510
  for (const a of sessionAttempts) {
1925
2511
  const ad = a.data;
1926
2512
  if (ad.validationPassed === false) {
@@ -1992,6 +2578,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
1992
2578
  const tmpPath = `${outputPath}.tmp`;
1993
2579
  await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(analysis, null, 2), "utf-8");
1994
2580
  await (0, import_promises3.rename)(tmpPath, outputPath);
2581
+ this._cachedPreviousPatterns = null;
1995
2582
  const historySummary = {
1996
2583
  timestamp: analysis.generatedAt,
1997
2584
  totalBuilds: analysis.summary.totalBuilds,
@@ -2003,8 +2590,55 @@ var PatternAnalyzer = class _PatternAnalyzer {
2003
2590
  };
2004
2591
  const historyPath = (0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl");
2005
2592
  await (0, import_promises3.appendFile)(historyPath, JSON.stringify(historySummary) + "\n", "utf-8");
2593
+ const sessions = await this.buildSessionSummaries(days);
2594
+ const sessionHistoryPath = (0, import_node_path5.join)(this.outputDir, "session-history.json");
2595
+ const sessionHistoryTmp = `${sessionHistoryPath}.tmp`;
2596
+ await (0, import_promises3.writeFile)(sessionHistoryTmp, JSON.stringify(sessions, null, 2), "utf-8");
2597
+ await (0, import_promises3.rename)(sessionHistoryTmp, sessionHistoryPath);
2006
2598
  return analysis;
2007
2599
  }
2600
+ async getSessions(limit = 20) {
2601
+ try {
2602
+ const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "session-history.json"), "utf-8");
2603
+ const all = JSON.parse(raw);
2604
+ return all.slice(-limit);
2605
+ } catch {
2606
+ return [];
2607
+ }
2608
+ }
2609
+ async buildSessionSummaries(days = 30) {
2610
+ const events = this._cachedEvents ?? await this.readAllEvents(days);
2611
+ const buildCompletes = events.filter((e) => e.eventType === "build_complete");
2612
+ const attemptsByBuild = /* @__PURE__ */ new Map();
2613
+ for (const e of events.filter((e2) => e2.eventType === "generation_attempt")) {
2614
+ const buildId = e.runId ?? e.sessionId;
2615
+ const list = attemptsByBuild.get(buildId) ?? [];
2616
+ list.push(e);
2617
+ attemptsByBuild.set(buildId, list);
2618
+ }
2619
+ const summaries = buildCompletes.map((bc) => {
2620
+ const data = bc.data;
2621
+ const sessionAttempts = attemptsByBuild.get(bc.runId ?? bc.sessionId) ?? [];
2622
+ const failedRules = Array.from(new Set(
2623
+ sessionAttempts.flatMap((a) => {
2624
+ const ad = a.data;
2625
+ if (ad.validationPassed !== false) return [];
2626
+ return (ad.issues ?? []).map((i) => i.rule);
2627
+ })
2628
+ ));
2629
+ return {
2630
+ sessionId: bc.runId ?? bc.sessionId,
2631
+ date: bc.fileDate,
2632
+ description: data.description ?? "",
2633
+ workflowType: data.workflowType ?? null,
2634
+ attempts: data.totalAttempts ?? 1,
2635
+ success: data.success ?? false,
2636
+ failedRules,
2637
+ workflowName: data.workflowName ?? null
2638
+ };
2639
+ });
2640
+ return summaries.sort((a, b) => a.date.localeCompare(b.date));
2641
+ }
2008
2642
  async getHistory(limit = 20) {
2009
2643
  try {
2010
2644
  const raw = await (0, import_promises3.readFile)((0, import_node_path5.join)(this.outputDir, "pattern-history.jsonl"), "utf-8");
@@ -2026,7 +2660,7 @@ var PatternAnalyzer = class _PatternAnalyzer {
2026
2660
  alerts.push({
2027
2661
  type: "stale_pattern",
2028
2662
  rule: p.rule,
2029
- message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-23)`
2663
+ message: `Pattern references Rule ${p.rule} which does not exist in the current validator (rules 1-34)`
2030
2664
  });
2031
2665
  }
2032
2666
  }
@@ -2113,8 +2747,60 @@ var nullLogger = {
2113
2747
  }
2114
2748
  };
2115
2749
 
2750
+ // src/utils/workflow-type.ts
2751
+ var TYPE_KEYWORDS = [
2752
+ ["gmail", "email"],
2753
+ ["imap", "email"],
2754
+ ["smtp", "email"],
2755
+ [" email", "email"],
2756
+ ["slack", "slack"],
2757
+ ["telegram", "messaging"],
2758
+ ["discord", "messaging"],
2759
+ [" sms", "messaging"],
2760
+ ["twilio", "messaging"],
2761
+ ["webhook", "webhook"],
2762
+ ["google sheets", "data"],
2763
+ ["spreadsheet", "data"],
2764
+ ["airtable", "data"],
2765
+ ["notion", "data"],
2766
+ ["github", "devops"],
2767
+ ["gitlab", "devops"],
2768
+ ["schedule", "schedule"],
2769
+ [" cron", "schedule"],
2770
+ ["daily", "schedule"],
2771
+ ["weekly", "schedule"],
2772
+ ["hourly", "schedule"],
2773
+ ["every day", "schedule"],
2774
+ ["every hour", "schedule"],
2775
+ ["every morning", "schedule"],
2776
+ ["postgres", "database"],
2777
+ ["mysql", "database"],
2778
+ ["supabase", "database"],
2779
+ ["redis", "database"],
2780
+ [" database", "database"],
2781
+ [" llm", "ai"],
2782
+ [" gpt", "ai"],
2783
+ ["claude", "ai"],
2784
+ [" agent", "ai"],
2785
+ ["langchain", "ai"],
2786
+ [" ai ", "ai"],
2787
+ [" ai", "ai"],
2788
+ ["http request", "api"],
2789
+ ["rest api", "api"],
2790
+ [" api", "api"]
2791
+ ];
2792
+ function inferWorkflowType(description) {
2793
+ const lower = " " + description.toLowerCase();
2794
+ for (const [keyword, type] of TYPE_KEYWORDS) {
2795
+ if (lower.includes(keyword)) return type;
2796
+ }
2797
+ return null;
2798
+ }
2799
+
2116
2800
  // src/client.ts
2117
- var DEFAULT_MODEL = "claude-sonnet-4-6";
2801
+ var import_node_os5 = require("os");
2802
+ var import_node_path6 = require("path");
2803
+ var DEFAULT_MODEL = process.env["KAIROS_MODEL"] ?? "claude-sonnet-4-6";
2118
2804
  var Kairos = class {
2119
2805
  provider;
2120
2806
  designer;
@@ -2142,7 +2828,8 @@ var Kairos = class {
2142
2828
  this.provider = null;
2143
2829
  }
2144
2830
  const anthropic = new import_sdk.default({ apiKey: options.anthropicApiKey });
2145
- this.designer = new WorkflowDesigner(anthropic, this.model, logger);
2831
+ 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");
2832
+ this.designer = new WorkflowDesigner(anthropic, this.model, logger, patternsPath);
2146
2833
  this.validator = new N8nValidator();
2147
2834
  this.library = options.library ?? new NullLibrary();
2148
2835
  this.logger = logger;
@@ -2175,11 +2862,13 @@ var Kairos = class {
2175
2862
  this.validateDescription(description);
2176
2863
  this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
2177
2864
  const buildStart = Date.now();
2865
+ const runId = generateUUID();
2866
+ const workflowType = inferWorkflowType(description);
2178
2867
  await this.telemetry?.emit("build_start", {
2179
2868
  description,
2180
2869
  model: this.model,
2181
2870
  dryRun: options?.dryRun ?? false
2182
- });
2871
+ }, runId);
2183
2872
  await this.library.initialize();
2184
2873
  const matches = await this.library.search(description);
2185
2874
  if (matches.length > 0) {
@@ -2214,8 +2903,9 @@ var Kairos = class {
2214
2903
  tokensOutput: meta.tokensOutput,
2215
2904
  validationPassed: meta.validationPassed,
2216
2905
  issueCount: meta.issues.length,
2217
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
2218
- });
2906
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
2907
+ workflowType
2908
+ }, runId);
2219
2909
  }
2220
2910
  await this.telemetry?.emit("build_complete", {
2221
2911
  description,
@@ -2228,13 +2918,14 @@ var Kairos = class {
2228
2918
  workflowId: null,
2229
2919
  dryRun: options?.dryRun ?? false,
2230
2920
  credentialsNeeded: 0,
2231
- warnedRules: err.warnedRules ?? []
2232
- });
2921
+ warnedRules: err.warnedRules ?? [],
2922
+ workflowType
2923
+ }, runId);
2233
2924
  this.updatePatterns();
2234
2925
  }
2235
2926
  throw err;
2236
2927
  }
2237
- await this.emitAttemptTelemetry(description, designResult);
2928
+ await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
2238
2929
  const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
2239
2930
  this.saveToLibrary(workflow, description, designResult, matches);
2240
2931
  if (options?.dryRun) {
@@ -2251,8 +2942,9 @@ var Kairos = class {
2251
2942
  workflowId: null,
2252
2943
  dryRun: true,
2253
2944
  credentialsNeeded: designResult.credentialsNeeded.length,
2254
- warnedRules: designResult.warnedRules
2255
- });
2945
+ warnedRules: designResult.warnedRules,
2946
+ workflowType
2947
+ }, runId);
2256
2948
  this.updatePatterns();
2257
2949
  return {
2258
2950
  workflowId: null,
@@ -2266,10 +2958,19 @@ var Kairos = class {
2266
2958
  }
2267
2959
  const provider = this.requireProvider();
2268
2960
  const deployed = await provider.deploy(workflow);
2269
- this.recordDeploy();
2961
+ this.logger.info("Workflow deployed to n8n", { workflowId: deployed.workflowId, name: deployed.name });
2962
+ this.recordDeploy(deployed.workflowId);
2270
2963
  if (options?.activate) {
2271
2964
  await provider.activate(deployed.workflowId);
2272
2965
  }
2966
+ let smokeTestResult;
2967
+ if (options?.smokeTest) {
2968
+ smokeTestResult = await provider.smokeTest(deployed.workflowId, workflow).catch((err) => {
2969
+ this.logger.warn("Smoke test threw unexpectedly", { err: String(err) });
2970
+ return { status: "error", triggerType: "manual", error: String(err) };
2971
+ });
2972
+ this.logger.info("Smoke test complete", { status: smokeTestResult.status, triggerType: smokeTestResult.triggerType });
2973
+ }
2273
2974
  const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
2274
2975
  const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
2275
2976
  await this.telemetry?.emit("build_complete", {
@@ -2283,8 +2984,9 @@ var Kairos = class {
2283
2984
  workflowId: deployed.workflowId,
2284
2985
  dryRun: false,
2285
2986
  credentialsNeeded: designResult.credentialsNeeded.length,
2286
- warnedRules: designResult.warnedRules
2287
- });
2987
+ warnedRules: designResult.warnedRules,
2988
+ workflowType
2989
+ }, runId);
2288
2990
  this.updatePatterns();
2289
2991
  return {
2290
2992
  workflowId: deployed.workflowId,
@@ -2293,18 +2995,21 @@ var Kairos = class {
2293
2995
  credentialsNeeded: designResult.credentialsNeeded,
2294
2996
  activationRequired: !options?.activate,
2295
2997
  generationAttempts: designResult.attempts,
2296
- dryRun: false
2998
+ dryRun: false,
2999
+ ...smokeTestResult !== void 0 ? { smokeTest: smokeTestResult } : {}
2297
3000
  };
2298
3001
  }
2299
3002
  async replace(id, description) {
2300
3003
  this.validateDescription(description);
2301
3004
  this.logger.info("Kairos.update", { id, description });
2302
3005
  const buildStart = Date.now();
3006
+ const runId = generateUUID();
3007
+ const workflowType = inferWorkflowType(description);
2303
3008
  await this.telemetry?.emit("build_start", {
2304
3009
  description,
2305
3010
  model: this.model,
2306
3011
  dryRun: false
2307
- });
3012
+ }, runId);
2308
3013
  await this.library.initialize();
2309
3014
  const matches = await this.library.search(description);
2310
3015
  const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
@@ -2323,8 +3028,9 @@ var Kairos = class {
2323
3028
  tokensOutput: meta.tokensOutput,
2324
3029
  validationPassed: meta.validationPassed,
2325
3030
  issueCount: meta.issues.length,
2326
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
2327
- });
3031
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
3032
+ workflowType
3033
+ }, runId);
2328
3034
  }
2329
3035
  await this.telemetry?.emit("build_complete", {
2330
3036
  description,
@@ -2337,16 +3043,18 @@ var Kairos = class {
2337
3043
  workflowId: null,
2338
3044
  dryRun: false,
2339
3045
  credentialsNeeded: 0,
2340
- warnedRules: err.warnedRules ?? []
2341
- });
3046
+ warnedRules: err.warnedRules ?? [],
3047
+ workflowType
3048
+ }, runId);
2342
3049
  this.updatePatterns();
2343
3050
  }
2344
3051
  throw err;
2345
3052
  }
2346
- await this.emitAttemptTelemetry(description, designResult);
3053
+ await this.emitAttemptTelemetry(description, designResult, workflowType, runId);
2347
3054
  const provider = this.requireProvider();
2348
3055
  const deployed = await provider.update(id, designResult.workflow);
2349
- this.saveToLibrary(designResult.workflow, description, designResult, matches);
3056
+ this.logger.info("Workflow updated in n8n", { workflowId: deployed.workflowId, name: deployed.name });
3057
+ this.saveToLibrary(designResult.workflow, description, designResult, matches, deployed.workflowId);
2350
3058
  this.recordDeploy();
2351
3059
  const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
2352
3060
  const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
@@ -2361,8 +3069,9 @@ var Kairos = class {
2361
3069
  workflowId: deployed.workflowId,
2362
3070
  dryRun: false,
2363
3071
  credentialsNeeded: designResult.credentialsNeeded.length,
2364
- warnedRules: designResult.warnedRules
2365
- });
3072
+ warnedRules: designResult.warnedRules,
3073
+ workflowType
3074
+ }, runId);
2366
3075
  this.updatePatterns();
2367
3076
  return {
2368
3077
  workflowId: deployed.workflowId,
@@ -2385,7 +3094,7 @@ var Kairos = class {
2385
3094
  return null;
2386
3095
  });
2387
3096
  }
2388
- async emitAttemptTelemetry(description, designResult) {
3097
+ async emitAttemptTelemetry(description, designResult, workflowType, runId) {
2389
3098
  for (const meta of designResult.attemptMetadata) {
2390
3099
  await this.telemetry?.emit("generation_attempt", {
2391
3100
  description,
@@ -2396,14 +3105,15 @@ var Kairos = class {
2396
3105
  tokensOutput: meta.tokensOutput,
2397
3106
  validationPassed: meta.validationPassed,
2398
3107
  issueCount: meta.issues.length,
2399
- issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null }))
2400
- });
3108
+ issues: meta.issues.map((i) => ({ rule: i.rule, severity: i.severity, message: i.message, nodeId: i.nodeId ?? null, nodeType: i.nodeType ?? null })),
3109
+ workflowType
3110
+ }, runId);
2401
3111
  }
2402
3112
  }
2403
- recordDeploy() {
3113
+ recordDeploy(n8nWorkflowId) {
2404
3114
  this.saveQueue = this.saveQueue.then(async (savedId) => {
2405
3115
  if (savedId) {
2406
- await this.library.recordDeployment(savedId);
3116
+ await this.library.recordDeployment(savedId, n8nWorkflowId);
2407
3117
  }
2408
3118
  return savedId;
2409
3119
  }).catch((err) => {
@@ -2411,7 +3121,7 @@ var Kairos = class {
2411
3121
  return null;
2412
3122
  });
2413
3123
  }
2414
- saveToLibrary(workflow, description, designResult, matches) {
3124
+ saveToLibrary(workflow, description, designResult, matches, n8nWorkflowId) {
2415
3125
  const failedAttempts = designResult.attemptMetadata.filter((m) => !m.validationPassed);
2416
3126
  const failurePatterns = failedAttempts.flatMap(
2417
3127
  (m) => m.issues.map((i) => ({ rule: i.rule, message: i.message }))
@@ -2437,6 +3147,7 @@ var Kairos = class {
2437
3147
  if (matches.length > 0) metadata.sourceWorkflowIds = matches.map((m) => m.workflow.id);
2438
3148
  if (topMatch) metadata.topMatchScore = topMatch.score;
2439
3149
  if (designResult.credentialsNeeded.length > 0) metadata.credentialsNeeded = designResult.credentialsNeeded;
3150
+ if (n8nWorkflowId) metadata.n8nWorkflowId = n8nWorkflowId;
2440
3151
  const firstTryPass = designResult.attemptMetadata.length > 0 && designResult.attemptMetadata[0].validationPassed;
2441
3152
  const failedRules = Array.from(new Set(
2442
3153
  designResult.attemptMetadata.filter((m) => !m.validationPassed).flatMap((m) => m.issues.map((i) => i.rule))
@@ -2496,16 +3207,36 @@ var Kairos = class {
2496
3207
 
2497
3208
  // src/library/file-library.ts
2498
3209
  var import_promises4 = require("fs/promises");
2499
- var import_node_path6 = require("path");
2500
- var import_node_os5 = require("os");
3210
+ var import_node_path7 = require("path");
3211
+ var import_node_os6 = require("os");
2501
3212
 
2502
3213
  // src/library/scorer.ts
2503
- var WEIGHTS = {
2504
- tfidf: 0.35,
2505
- nodeFingerprint: 0.3,
2506
- outcome: 0.2,
2507
- deploy: 0.15
2508
- };
3214
+ function loadWeights() {
3215
+ const raw = {
3216
+ tfidf: parseFloat(process.env["KAIROS_WEIGHT_TFIDF"] ?? ""),
3217
+ nodeFingerprint: parseFloat(process.env["KAIROS_WEIGHT_JACCARD"] ?? ""),
3218
+ outcome: parseFloat(process.env["KAIROS_WEIGHT_OUTCOME"] ?? ""),
3219
+ deploy: parseFloat(process.env["KAIROS_WEIGHT_DEPLOY"] ?? "")
3220
+ };
3221
+ const defaults = { tfidf: 0.35, nodeFingerprint: 0.3, outcome: 0.2, deploy: 0.15 };
3222
+ const anySet = Object.values(raw).some((v) => !isNaN(v) && v >= 0);
3223
+ if (!anySet) return defaults;
3224
+ const w = {
3225
+ tfidf: !isNaN(raw.tfidf) && raw.tfidf >= 0 ? raw.tfidf : defaults.tfidf,
3226
+ nodeFingerprint: !isNaN(raw.nodeFingerprint) && raw.nodeFingerprint >= 0 ? raw.nodeFingerprint : defaults.nodeFingerprint,
3227
+ outcome: !isNaN(raw.outcome) && raw.outcome >= 0 ? raw.outcome : defaults.outcome,
3228
+ deploy: !isNaN(raw.deploy) && raw.deploy >= 0 ? raw.deploy : defaults.deploy
3229
+ };
3230
+ const total = w.tfidf + w.nodeFingerprint + w.outcome + w.deploy;
3231
+ if (total <= 0) return defaults;
3232
+ return {
3233
+ tfidf: w.tfidf / total,
3234
+ nodeFingerprint: w.nodeFingerprint / total,
3235
+ outcome: w.outcome / total,
3236
+ deploy: w.deploy / total
3237
+ };
3238
+ }
3239
+ var WEIGHTS = loadWeights();
2509
3240
  var NODE_KEYWORDS = {
2510
3241
  slack: ["slack", "slackApi"],
2511
3242
  email: ["gmail", "sendEmail", "emailSend", "emailReadImap"],
@@ -2690,6 +3421,8 @@ function clusterWorkflows(workflows) {
2690
3421
  }
2691
3422
  return clusters.sort((a, b) => b.members.length - a.members.length);
2692
3423
  }
3424
+ var NOVELTY_BOOST = 0.05;
3425
+ var NOVELTY_PENALTY = 0.03;
2693
3426
  function rerank(candidates, clusters) {
2694
3427
  const clusterMap = /* @__PURE__ */ new Map();
2695
3428
  for (const cluster of clusters) {
@@ -2697,7 +3430,7 @@ function rerank(candidates, clusters) {
2697
3430
  clusterMap.set(member.id, cluster);
2698
3431
  }
2699
3432
  }
2700
- return candidates.map((c) => {
3433
+ const pass1 = candidates.map((c) => {
2701
3434
  const cluster = clusterMap.get(c.workflow.id);
2702
3435
  let boost = 0;
2703
3436
  if (cluster && cluster.avgFirstTryPassRate > 0) {
@@ -2709,7 +3442,25 @@ function rerank(candidates, clusters) {
2709
3442
  return {
2710
3443
  workflow: c.workflow,
2711
3444
  score: Math.max(0, Math.min(1, c.score + boost)),
2712
- ...cluster ? { clusterPattern: cluster.pattern } : {}
3445
+ cluster
3446
+ };
3447
+ }).sort((a, b) => b.score - a.score);
3448
+ const seenFingerprints = /* @__PURE__ */ new Set();
3449
+ return pass1.map((c) => {
3450
+ const fpKey = c.cluster ? fingerprintKey(c.cluster.fingerprint) : null;
3451
+ let noveltyAdjust = 0;
3452
+ if (fpKey !== null) {
3453
+ if (!seenFingerprints.has(fpKey)) {
3454
+ seenFingerprints.add(fpKey);
3455
+ noveltyAdjust = NOVELTY_BOOST;
3456
+ } else {
3457
+ noveltyAdjust = -NOVELTY_PENALTY;
3458
+ }
3459
+ }
3460
+ return {
3461
+ workflow: c.workflow,
3462
+ score: Math.max(0, Math.min(1, c.score + noveltyAdjust)),
3463
+ ...c.cluster ? { clusterPattern: c.cluster.pattern } : {}
2713
3464
  };
2714
3465
  }).sort((a, b) => b.score - a.score);
2715
3466
  }
@@ -2726,14 +3477,32 @@ function buildSearchCorpus(w) {
2726
3477
  });
2727
3478
  return `${w.description} ${w.workflow.name} ${w.tags.join(" ")} ${nodeTokens.join(" ")}`;
2728
3479
  }
2729
- var MAX_LIBRARY_SIZE = 500;
3480
+ var _rawSize = parseInt(process.env["KAIROS_LIBRARY_SIZE"] ?? "500", 10);
3481
+ var MAX_LIBRARY_SIZE = Number.isFinite(_rawSize) && _rawSize >= 10 ? _rawSize : 500;
3482
+ function evictionScore(m) {
3483
+ return (m.deployCount ?? 0) * 3 + (m.timesRetrieved ?? 0) + (m.outcomeStats?.totalUses ?? 0);
3484
+ }
3485
+ function isValidMeta(item) {
3486
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflowName === "string" && Array.isArray(item.cachedNodeTypes);
3487
+ }
3488
+ function isValidOldEntry(item) {
3489
+ return typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(
3490
+ item.workflow.nodes
3491
+ );
3492
+ }
2730
3493
  var FileLibrary = class {
2731
3494
  dir;
2732
- workflows = [];
3495
+ meta = [];
2733
3496
  initPromise = null;
2734
3497
  writeQueue = Promise.resolve();
2735
3498
  constructor(dir) {
2736
- this.dir = dir ?? (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".kairos", "library");
3499
+ this.dir = dir ?? (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".kairos", "library");
3500
+ }
3501
+ get workflowsDir() {
3502
+ return (0, import_node_path7.join)(this.dir, "workflows");
3503
+ }
3504
+ workflowFilePath(id) {
3505
+ return (0, import_node_path7.join)(this.workflowsDir, `${id}.json`);
2737
3506
  }
2738
3507
  async initialize() {
2739
3508
  if (!this.initPromise) {
@@ -2743,61 +3512,197 @@ var FileLibrary = class {
2743
3512
  }
2744
3513
  async doInitialize() {
2745
3514
  await (0, import_promises4.mkdir)(this.dir, { recursive: true });
2746
- const indexPath = (0, import_node_path6.join)(this.dir, "index.json");
3515
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
3516
+ let workflowsDirExists = false;
2747
3517
  try {
2748
- const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
2749
- const parsed = JSON.parse(raw);
2750
- if (!Array.isArray(parsed)) {
2751
- this.workflows = [];
2752
- } else {
2753
- this.workflows = parsed.filter(
2754
- (item) => typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.description === "string" && typeof item.workflow === "object" && item.workflow !== null && Array.isArray(item.workflow.nodes)
2755
- );
3518
+ await (0, import_promises4.stat)(this.workflowsDir);
3519
+ workflowsDirExists = true;
3520
+ } catch {
3521
+ }
3522
+ if (workflowsDirExists) {
3523
+ try {
3524
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3525
+ const parsed = JSON.parse(raw);
3526
+ if (Array.isArray(parsed)) {
3527
+ this.meta = parsed.filter(isValidMeta);
3528
+ }
3529
+ } catch {
3530
+ this.meta = [];
3531
+ }
3532
+ await this.scanForOrphansAndCleanup();
3533
+ } else {
3534
+ try {
3535
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3536
+ const parsed = JSON.parse(raw);
3537
+ if (Array.isArray(parsed) && parsed.length > 0 && isValidOldEntry(parsed[0])) {
3538
+ await this.migrateFromMonolithic(parsed.filter(isValidOldEntry));
3539
+ return;
3540
+ }
3541
+ } catch {
3542
+ }
3543
+ this.meta = [];
3544
+ await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
3545
+ }
3546
+ }
3547
+ async scanForOrphansAndCleanup() {
3548
+ let entries;
3549
+ try {
3550
+ entries = await (0, import_promises4.readdir)(this.workflowsDir);
3551
+ } catch {
3552
+ return;
3553
+ }
3554
+ const indexedIds = new Set(this.meta.map((m) => m.id));
3555
+ const orphanIds = [];
3556
+ for (const filename of entries) {
3557
+ if (filename.endsWith(".tmp")) {
3558
+ await (0, import_promises4.unlink)((0, import_node_path7.join)(this.workflowsDir, filename)).catch(() => {
3559
+ });
3560
+ continue;
3561
+ }
3562
+ if (!filename.endsWith(".json")) continue;
3563
+ const id = filename.slice(0, -5);
3564
+ if (!indexedIds.has(id)) {
3565
+ orphanIds.push(id);
2756
3566
  }
3567
+ }
3568
+ if (orphanIds.length > 0) {
3569
+ console.warn(`[FileLibrary] Found ${orphanIds.length} orphaned workflow file(s) not in index: ${orphanIds.join(", ")}`);
3570
+ }
3571
+ }
3572
+ /**
3573
+ * One-time transparent migration from v0.4.x monolithic index.json.
3574
+ * Splits each stored workflow into a per-file workflow JSON and a lightweight
3575
+ * meta entry. Rewrites index.json in the new format.
3576
+ */
3577
+ async migrateFromMonolithic(oldEntries) {
3578
+ await (0, import_promises4.mkdir)(this.workflowsDir, { recursive: true });
3579
+ const newMeta = [];
3580
+ for (const entry of oldEntries) {
3581
+ const wfPath = this.workflowFilePath(entry.id);
3582
+ const tmpPath = `${wfPath}.tmp`;
3583
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(entry.workflow), "utf-8");
3584
+ await (0, import_promises4.rename)(tmpPath, wfPath);
3585
+ const { workflow, ...metaFields } = entry;
3586
+ newMeta.push({
3587
+ ...metaFields,
3588
+ workflowName: workflow.name,
3589
+ cachedNodeTypes: workflow.nodes.map((n) => n.type)
3590
+ });
3591
+ }
3592
+ this.meta = newMeta;
3593
+ await this.persistNow();
3594
+ }
3595
+ async loadWorkflowFile(id) {
3596
+ try {
3597
+ const raw = await (0, import_promises4.readFile)(this.workflowFilePath(id), "utf-8");
3598
+ return JSON.parse(raw);
2757
3599
  } catch {
2758
- this.workflows = [];
3600
+ return null;
2759
3601
  }
2760
3602
  }
3603
+ async writeWorkflowFile(id, workflow) {
3604
+ const wfPath = this.workflowFilePath(id);
3605
+ const tmpPath = `${wfPath}.tmp`;
3606
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(workflow), "utf-8");
3607
+ await (0, import_promises4.rename)(tmpPath, wfPath);
3608
+ }
3609
+ /**
3610
+ * Build a lightweight StoredWorkflow shell from a meta entry for use in
3611
+ * scoring / clustering. Only node.type is populated in each node — no other
3612
+ * node fields are used by hybridScore or clusterWorkflows.
3613
+ */
3614
+ makeSearchShell(m) {
3615
+ return {
3616
+ ...m,
3617
+ workflow: {
3618
+ name: m.workflowName,
3619
+ nodes: m.cachedNodeTypes.map((type) => ({
3620
+ id: "",
3621
+ name: "",
3622
+ type,
3623
+ typeVersion: 1,
3624
+ position: [0, 0],
3625
+ parameters: {}
3626
+ })),
3627
+ connections: {}
3628
+ }
3629
+ };
3630
+ }
2761
3631
  async search(description, options) {
2762
- const searchable = this.workflows.filter((w) => w.trustLevel !== "blocked");
2763
- if (searchable.length === 0) return [];
3632
+ const filteredMeta = this.meta.filter((m) => m.trustLevel !== "blocked");
3633
+ if (filteredMeta.length === 0) return [];
2764
3634
  const limit = options?.limit ?? 3;
2765
3635
  const queryTokens = tokenize(description);
2766
3636
  if (queryTokens.length === 0) return [];
2767
- const docTokenArrays = searchable.map((w) => tokenize(buildSearchCorpus(w)));
3637
+ const shells = filteredMeta.map((m) => this.makeSearchShell(m));
3638
+ const docTokenArrays = shells.map((w) => tokenize(buildSearchCorpus(w)));
2768
3639
  const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
2769
- const docCount = searchable.length;
3640
+ const docCount = shells.length;
2770
3641
  const idf = /* @__PURE__ */ new Map();
3642
+ const idfCeiling = Math.log(docCount + 1) + 1;
2771
3643
  const allTokens = new Set(queryTokens);
2772
3644
  for (const token of allTokens) {
2773
3645
  const docsWithToken = docTokenSets.filter((d) => d.has(token)).length;
2774
- idf.set(token, Math.log((docCount + 1) / (docsWithToken + 1)) + 1);
3646
+ const rawIdf = Math.log((docCount + 1) / (docsWithToken + 1)) + 1;
3647
+ idf.set(token, rawIdf / idfCeiling);
2775
3648
  }
2776
- const scored = hybridScore(queryTokens, description, searchable, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
2777
- const clusters = clusterWorkflows(searchable);
3649
+ const scored = hybridScore(queryTokens, description, shells, docTokenArrays, idf).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
3650
+ const clusters = clusterWorkflows(shells);
2778
3651
  const reranked = rerank(scored, clusters).slice(0, limit);
2779
- const results = reranked.map((m) => {
2780
- return { workflow: m.workflow, score: m.score, mode: scoreToMode(m.score) };
2781
- });
2782
- if (results.length > 0) {
2783
- for (const r of results) {
2784
- r.workflow.timesRetrieved = (r.workflow.timesRetrieved ?? 0) + 1;
2785
- }
2786
- this.persist();
3652
+ if (reranked.length === 0) return [];
3653
+ for (const r of reranked) {
3654
+ const m = this.meta.find((m2) => m2.id === r.workflow.id);
3655
+ if (m) m.timesRetrieved = (m.timesRetrieved ?? 0) + 1;
2787
3656
  }
2788
- return results;
3657
+ this.persist();
3658
+ const results = await Promise.all(
3659
+ reranked.map(async (r) => {
3660
+ const m = this.meta.find((meta) => meta.id === r.workflow.id);
3661
+ const workflow = await this.loadWorkflowFile(r.workflow.id);
3662
+ if (!workflow) return null;
3663
+ return {
3664
+ workflow: { ...m, workflow },
3665
+ score: r.score,
3666
+ mode: scoreToMode(r.score)
3667
+ };
3668
+ })
3669
+ );
3670
+ return results.filter((r) => r !== null);
2789
3671
  }
2790
3672
  async save(workflow, metadata) {
3673
+ const existingByN8nId = metadata.n8nWorkflowId ? this.meta.find((m) => m.n8nWorkflowId === metadata.n8nWorkflowId) : void 0;
3674
+ const normalizedDesc = metadata.description.trim().toLowerCase();
3675
+ const existing = existingByN8nId ?? this.meta.find((m) => m.description.trim().toLowerCase() === normalizedDesc);
3676
+ if (existing) {
3677
+ existing.description = metadata.description;
3678
+ existing.workflowName = workflow.name;
3679
+ existing.cachedNodeTypes = workflow.nodes.map((n) => n.type);
3680
+ if (metadata.n8nWorkflowId) existing.n8nWorkflowId = metadata.n8nWorkflowId;
3681
+ if (metadata.generationAttempts != null) {
3682
+ existing.generationAttempts = metadata.generationAttempts;
3683
+ }
3684
+ if (metadata.failurePatterns?.length) {
3685
+ existing.failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
3686
+ }
3687
+ if (metadata.tags?.length) {
3688
+ existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...metadata.tags])];
3689
+ }
3690
+ await this.writeWorkflowFile(existing.id, workflow);
3691
+ await this.persist();
3692
+ return existing.id;
3693
+ }
2791
3694
  const id = generateUUID();
3695
+ await this.writeWorkflowFile(id, workflow);
2792
3696
  const failurePatterns = this.deduplicateFailurePatterns(metadata.failurePatterns);
2793
- const stored = {
3697
+ const meta = {
2794
3698
  id,
2795
- workflow,
2796
3699
  description: metadata.description,
2797
3700
  tags: metadata.tags ?? [],
2798
3701
  platform: metadata.platform ?? "n8n",
2799
3702
  deployCount: 0,
2800
3703
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3704
+ workflowName: workflow.name,
3705
+ cachedNodeTypes: workflow.nodes.map((n) => n.type),
2801
3706
  ...failurePatterns?.length ? { failurePatterns } : {},
2802
3707
  ...metadata.sourceWorkflowIds?.length ? { sourceWorkflowIds: metadata.sourceWorkflowIds } : {},
2803
3708
  ...metadata.generationMode ? { generationMode: metadata.generationMode } : {},
@@ -2807,33 +3712,39 @@ var FileLibrary = class {
2807
3712
  ...metadata.sourceKind ? { sourceKind: metadata.sourceKind } : {},
2808
3713
  ...metadata.sourceId ? { sourceId: metadata.sourceId } : {},
2809
3714
  ...metadata.sourceUrl ? { sourceUrl: metadata.sourceUrl } : {},
2810
- ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {}
3715
+ ...metadata.trustLevel ? { trustLevel: metadata.trustLevel } : {},
3716
+ ...metadata.n8nWorkflowId ? { n8nWorkflowId: metadata.n8nWorkflowId } : {}
2811
3717
  };
2812
- this.workflows.push(stored);
2813
- if (this.workflows.length > MAX_LIBRARY_SIZE) {
2814
- this.workflows.sort((a, b) => (b.deployCount ?? 1) - (a.deployCount ?? 1));
2815
- this.workflows = this.workflows.slice(0, MAX_LIBRARY_SIZE);
3718
+ this.meta.push(meta);
3719
+ if (this.meta.length > MAX_LIBRARY_SIZE) {
3720
+ this.meta.sort((a, b) => {
3721
+ if (a.id === id) return -1;
3722
+ if (b.id === id) return 1;
3723
+ return evictionScore(b) - evictionScore(a);
3724
+ });
3725
+ this.meta = this.meta.slice(0, MAX_LIBRARY_SIZE);
2816
3726
  }
2817
3727
  await this.persist();
2818
3728
  return id;
2819
3729
  }
2820
- async recordDeployment(id) {
2821
- const w = this.workflows.find((w2) => w2.id === id);
2822
- if (w) {
2823
- w.deployCount++;
2824
- w.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
3730
+ async recordDeployment(id, n8nWorkflowId) {
3731
+ const m = this.meta.find((m2) => m2.id === id);
3732
+ if (m) {
3733
+ m.deployCount++;
3734
+ m.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
3735
+ if (n8nWorkflowId) m.n8nWorkflowId = n8nWorkflowId;
2825
3736
  await this.persist();
2826
3737
  }
2827
3738
  }
2828
3739
  async recordOutcome(id, outcome) {
2829
- const w = this.workflows.find((w2) => w2.id === id);
2830
- if (!w) return;
3740
+ const m = this.meta.find((m2) => m2.id === id);
3741
+ if (!m) return;
2831
3742
  if (outcome.mode === "direct") {
2832
- w.timesUsedAsDirect = (w.timesUsedAsDirect ?? 0) + 1;
3743
+ m.timesUsedAsDirect = (m.timesUsedAsDirect ?? 0) + 1;
2833
3744
  } else {
2834
- w.timesUsedAsReference = (w.timesUsedAsReference ?? 0) + 1;
3745
+ m.timesUsedAsReference = (m.timesUsedAsReference ?? 0) + 1;
2835
3746
  }
2836
- const stats = w.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
3747
+ const stats = m.outcomeStats ?? { totalUses: 0, totalAttempts: 0, firstTryPasses: 0, failedRules: {} };
2837
3748
  stats.totalUses++;
2838
3749
  stats.totalAttempts += outcome.attempts;
2839
3750
  if (outcome.firstTryPass) stats.firstTryPasses++;
@@ -2841,24 +3752,35 @@ var FileLibrary = class {
2841
3752
  const key = String(rule);
2842
3753
  stats.failedRules[key] = (stats.failedRules[key] ?? 0) + 1;
2843
3754
  }
2844
- w.outcomeStats = stats;
3755
+ m.outcomeStats = stats;
2845
3756
  await this.persist();
2846
3757
  }
2847
3758
  async drain() {
2848
3759
  await this.writeQueue;
2849
3760
  }
2850
3761
  async get(id) {
2851
- return this.workflows.find((w) => w.id === id) ?? null;
3762
+ const m = this.meta.find((m2) => m2.id === id);
3763
+ if (!m) return null;
3764
+ const workflow = await this.loadWorkflowFile(id);
3765
+ if (!workflow) return null;
3766
+ return { ...m, workflow };
2852
3767
  }
2853
3768
  async list(filters) {
2854
- let result = this.workflows;
3769
+ let filtered = this.meta;
2855
3770
  if (filters?.platform) {
2856
- result = result.filter((w) => w.platform === filters.platform);
3771
+ filtered = filtered.filter((m) => m.platform === filters.platform);
2857
3772
  }
2858
3773
  if (filters?.tags && filters.tags.length > 0) {
2859
- result = result.filter((w) => filters.tags.some((t) => w.tags.includes(t)));
3774
+ filtered = filtered.filter((m) => filters.tags.some((t) => m.tags.includes(t)));
2860
3775
  }
2861
- return result;
3776
+ const results = await Promise.all(
3777
+ filtered.map(async (m) => {
3778
+ const workflow = await this.loadWorkflowFile(m.id);
3779
+ if (!workflow) return null;
3780
+ return { ...m, workflow };
3781
+ })
3782
+ );
3783
+ return results.filter((r) => r !== null);
2862
3784
  }
2863
3785
  deduplicateFailurePatterns(patterns) {
2864
3786
  if (!patterns?.length) return void 0;
@@ -2873,12 +3795,98 @@ var FileLibrary = class {
2873
3795
  }
2874
3796
  return [...map.values()];
2875
3797
  }
2876
- persist() {
2877
- this.writeQueue = this.writeQueue.then(async () => {
2878
- const indexPath = (0, import_node_path6.join)(this.dir, "index.json");
3798
+ // ── Cross-process file locking ────────────────────────────────────────────
3799
+ // Uses O_EXCL (exclusive create) which is atomic on POSIX and Windows NTFS.
3800
+ // Protects the read-modify-write cycle in persist() from concurrent writers
3801
+ // in separate OS processes (e.g. MCP server + CLI running simultaneously).
3802
+ get lockPath() {
3803
+ return (0, import_node_path7.join)(this.dir, ".index.lock");
3804
+ }
3805
+ async acquireLock(timeoutMs = 3e3) {
3806
+ const deadline = Date.now() + timeoutMs;
3807
+ let delayMs = 10;
3808
+ while (true) {
3809
+ try {
3810
+ const fh = await (0, import_promises4.open)(this.lockPath, "wx");
3811
+ await fh.writeFile(String(process.pid));
3812
+ await fh.close();
3813
+ return async () => {
3814
+ await (0, import_promises4.unlink)(this.lockPath).catch(() => {
3815
+ });
3816
+ };
3817
+ } catch {
3818
+ try {
3819
+ const content = await (0, import_promises4.readFile)(this.lockPath, "utf-8");
3820
+ const lockPid = parseInt(content.trim(), 10);
3821
+ const fileStat = await (0, import_promises4.stat)(this.lockPath);
3822
+ const ageMs = Date.now() - fileStat.mtimeMs;
3823
+ if (ageMs > 1e4) {
3824
+ await (0, import_promises4.unlink)(this.lockPath).catch(() => {
3825
+ });
3826
+ continue;
3827
+ }
3828
+ if (!isNaN(lockPid)) {
3829
+ try {
3830
+ process.kill(lockPid, 0);
3831
+ } catch {
3832
+ await (0, import_promises4.unlink)(this.lockPath).catch(() => {
3833
+ });
3834
+ continue;
3835
+ }
3836
+ }
3837
+ } catch {
3838
+ continue;
3839
+ }
3840
+ if (Date.now() > deadline) {
3841
+ return async () => {
3842
+ };
3843
+ }
3844
+ await new Promise((r) => setTimeout(r, delayMs));
3845
+ delayMs = Math.min(delayMs * 1.5, 200);
3846
+ }
3847
+ }
3848
+ }
3849
+ /**
3850
+ * Direct write used only during migration (before writeQueue is needed).
3851
+ */
3852
+ async persistNow() {
3853
+ const releaseLock = await this.acquireLock();
3854
+ try {
3855
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
2879
3856
  const tmpPath = `${indexPath}.tmp`;
2880
- await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.workflows, null, 2), "utf-8");
3857
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(this.meta, null, 2), "utf-8");
2881
3858
  await (0, import_promises4.rename)(tmpPath, indexPath);
3859
+ } finally {
3860
+ await releaseLock();
3861
+ }
3862
+ }
3863
+ persist() {
3864
+ this.writeQueue = this.writeQueue.then(async () => {
3865
+ const releaseLock = await this.acquireLock();
3866
+ try {
3867
+ const indexPath = (0, import_node_path7.join)(this.dir, "index.json");
3868
+ let onDisk = [];
3869
+ try {
3870
+ const raw = await (0, import_promises4.readFile)(indexPath, "utf-8");
3871
+ const parsed = JSON.parse(raw);
3872
+ if (Array.isArray(parsed)) {
3873
+ onDisk = parsed.filter(isValidMeta);
3874
+ }
3875
+ } catch {
3876
+ }
3877
+ const ourIds = new Set(this.meta.map((m) => m.id));
3878
+ const external = onDisk.filter((m) => !ourIds.has(m.id));
3879
+ let merged = [...this.meta, ...external];
3880
+ if (merged.length > MAX_LIBRARY_SIZE) {
3881
+ merged.sort((a, b) => evictionScore(b) - evictionScore(a));
3882
+ merged = merged.slice(0, MAX_LIBRARY_SIZE);
3883
+ }
3884
+ const tmpPath = `${indexPath}.tmp`;
3885
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
3886
+ await (0, import_promises4.rename)(tmpPath, indexPath);
3887
+ } finally {
3888
+ await releaseLock();
3889
+ }
2882
3890
  });
2883
3891
  return this.writeQueue;
2884
3892
  }
@@ -2900,6 +3908,19 @@ var SECRET_PATTERNS = [
2900
3908
  /AIza[a-zA-Z0-9_-]{35}/,
2901
3909
  /AKIA[A-Z0-9]{16}/
2902
3910
  ];
3911
+ var SECRET_PREFIXES = ["sk-", "ghp_", "xoxb-", "AIza", "AKIA"];
3912
+ function collectExpressionStrings(obj, out = []) {
3913
+ if (typeof obj === "string") {
3914
+ if (obj.includes("={{")) out.push(obj);
3915
+ } else if (Array.isArray(obj)) {
3916
+ for (const item of obj) collectExpressionStrings(item, out);
3917
+ } else if (obj !== null && typeof obj === "object") {
3918
+ for (const val of Object.values(obj)) {
3919
+ collectExpressionStrings(val, out);
3920
+ }
3921
+ }
3922
+ return out;
3923
+ }
2903
3924
  function assessTemplateSafety(workflow) {
2904
3925
  const reasons = [];
2905
3926
  let worst = "safe";
@@ -2922,6 +3943,15 @@ function assessTemplateSafety(workflow) {
2922
3943
  break;
2923
3944
  }
2924
3945
  }
3946
+ const expressions = collectExpressionStrings(node.parameters);
3947
+ for (const expr of expressions) {
3948
+ for (const prefix of SECRET_PREFIXES) {
3949
+ if (expr.includes(prefix)) {
3950
+ escalate("review", `Node "${node.name}" has an expression containing credential-like prefix "${prefix}"`);
3951
+ break;
3952
+ }
3953
+ }
3954
+ }
2925
3955
  }
2926
3956
  return { trustLevel: worst, reasons };
2927
3957
  }
@@ -2979,12 +4009,26 @@ var TemplateSyncer = class {
2979
4009
  }
2980
4010
  return progress;
2981
4011
  }
4012
+ async fetchWithBackoff(url, maxRetries = 3) {
4013
+ let delayMs = DELAY_BETWEEN_FETCHES_MS;
4014
+ let lastResponse;
4015
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
4016
+ lastResponse = await fetch(url);
4017
+ if (lastResponse.status !== 429 && lastResponse.status !== 503) return lastResponse;
4018
+ if (attempt === maxRetries) break;
4019
+ const retryAfterHeader = lastResponse.headers.get("Retry-After");
4020
+ const waitMs = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : delayMs * Math.pow(2, attempt);
4021
+ this.logger.warn(`HTTP ${lastResponse.status} from template API, retrying in ${waitMs}ms`, { url, attempt });
4022
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
4023
+ }
4024
+ return lastResponse;
4025
+ }
2982
4026
  async fetchTemplateIds(max, progress) {
2983
4027
  const ids = [];
2984
4028
  let page = 1;
2985
4029
  while (ids.length < max) {
2986
4030
  const url = `${N8N_TEMPLATE_API}/search?page=${page}&rows=${PAGE_SIZE}`;
2987
- const response = await fetch(url);
4031
+ const response = await this.fetchWithBackoff(url);
2988
4032
  if (!response.ok) break;
2989
4033
  const data = await response.json();
2990
4034
  progress.total = Math.min(data.totalWorkflows, max);
@@ -3004,7 +4048,7 @@ var TemplateSyncer = class {
3004
4048
  }
3005
4049
  async processTemplate(id, progress) {
3006
4050
  const url = `${N8N_TEMPLATE_API}/workflows/${id}`;
3007
- const response = await fetch(url);
4051
+ const response = await this.fetchWithBackoff(url);
3008
4052
  if (!response.ok) return;
3009
4053
  const data = await response.json();
3010
4054
  const templateMeta = data.workflow;
@@ -3066,7 +4110,9 @@ Kairos SDK \u2014 LLM-powered n8n workflow generation
3066
4110
  Usage:
3067
4111
  kairos init First-time setup wizard
3068
4112
  kairos build <description> [options]
4113
+ kairos replace <n8n-id> <description>
3069
4114
  kairos patterns [options]
4115
+ kairos sessions [options]
3070
4116
  kairos list
3071
4117
  kairos get <id>
3072
4118
  kairos activate <id>
@@ -3083,15 +4129,23 @@ Patterns options:
3083
4129
  --days <days> Analysis window (default: 30)
3084
4130
  --json Output raw JSON instead of summary
3085
4131
 
4132
+ Sessions options:
4133
+ --limit <n> Number of recent sessions to show (default: 20)
4134
+ --json Output raw JSON instead of summary
4135
+
3086
4136
  Sync options:
3087
4137
  --max <count> Maximum templates to fetch (default: 500)
3088
4138
 
3089
4139
  Environment variables:
3090
- ANTHROPIC_API_KEY Anthropic API key (required)
3091
- N8N_BASE_URL n8n instance URL (required for deploy, optional for --dry-run)
3092
- N8N_API_KEY n8n API key (required for deploy, optional for --dry-run)
3093
- KAIROS_MODEL Claude model override (default: claude-sonnet-4-6)
3094
- KAIROS_TELEMETRY Set to "true" or a directory path to enable telemetry logging
4140
+ ANTHROPIC_API_KEY Anthropic API key (required)
4141
+ N8N_BASE_URL n8n instance URL (required for deploy, optional for --dry-run)
4142
+ N8N_API_KEY n8n API key (required for deploy, optional for --dry-run)
4143
+ KAIROS_MODEL Claude model override (default: claude-sonnet-4-6)
4144
+ KAIROS_TELEMETRY Set to "true" or a directory path to enable telemetry logging
4145
+ KAIROS_PROMPT_PROFILE minimal | standard | rich (default: standard)
4146
+ minimal: base prompt only, no library context, top 3 patterns
4147
+ standard: full library context, top 10 patterns (default)
4148
+ rich: full library context, top 15 patterns, proactive expression guidance
3095
4149
  `;
3096
4150
  function getEnvOrExit(name) {
3097
4151
  const val = process.env[name];
@@ -3189,6 +4243,27 @@ async function handleBuild(positional, flags) {
3189
4243
  ...result.dryRun ? { workflow: result.workflow } : {}
3190
4244
  }, null, 2));
3191
4245
  }
4246
+ async function handleReplace(positional) {
4247
+ const id = positional[0];
4248
+ const description = positional.slice(1).join(" ");
4249
+ if (!id || !description) {
4250
+ console.error("Usage: kairos replace <n8n-workflow-id> <description>");
4251
+ process.exit(1);
4252
+ }
4253
+ const kairos = createClient();
4254
+ const start = Date.now();
4255
+ console.error(`Replacing workflow ${id}...`);
4256
+ const result = await kairos.replace(id, description);
4257
+ await kairos.drain();
4258
+ const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
4259
+ console.error(`Done in ${elapsed}s (${result.generationAttempts} attempt${result.generationAttempts > 1 ? "s" : ""})`);
4260
+ console.error("");
4261
+ console.log(JSON.stringify({
4262
+ workflowId: result.workflowId,
4263
+ name: result.name,
4264
+ generationAttempts: result.generationAttempts
4265
+ }, null, 2));
4266
+ }
3192
4267
  async function handleList() {
3193
4268
  const kairos = createClient();
3194
4269
  const workflows = await kairos.list();
@@ -3253,16 +4328,10 @@ async function handleDelete(positional, flags) {
3253
4328
  console.log(`Deleted workflow ${id}`);
3254
4329
  }
3255
4330
  async function handleSyncTemplates(flags) {
3256
- const max = typeof flags["max"] === "string" ? parseInt(flags["max"], 10) : 500;
4331
+ const maxRaw = typeof flags["max"] === "string" ? parseInt(flags["max"], 10) : NaN;
4332
+ const max = Number.isNaN(maxRaw) ? 500 : maxRaw;
3257
4333
  const library = new FileLibrary();
3258
- const logger = {
3259
- debug: () => {
3260
- },
3261
- info: (msg, meta) => console.error(meta ? `${msg} ${JSON.stringify(meta)}` : msg),
3262
- warn: (msg, meta) => console.error(meta ? `[warn] ${msg} ${JSON.stringify(meta)}` : `[warn] ${msg}`),
3263
- error: (msg, meta) => console.error(meta ? `[error] ${msg} ${JSON.stringify(meta)}` : `[error] ${msg}`)
3264
- };
3265
- const syncer = new TemplateSyncer(library, logger);
4334
+ const syncer = new TemplateSyncer(library, CLI_LOGGER);
3266
4335
  console.error(`Syncing up to ${max} templates from n8n community library...`);
3267
4336
  const result = await syncer.sync({
3268
4337
  maxTemplates: max,
@@ -3281,7 +4350,8 @@ async function handleSyncTemplates(flags) {
3281
4350
  console.error(` Paid: ${result.skippedPaid} (skipped)`);
3282
4351
  }
3283
4352
  async function handlePatterns(flags) {
3284
- const days = typeof flags["days"] === "string" ? parseInt(flags["days"], 10) : 30;
4353
+ const daysRaw = typeof flags["days"] === "string" ? parseInt(flags["days"], 10) : NaN;
4354
+ const days = Number.isNaN(daysRaw) ? 30 : daysRaw;
3285
4355
  const analyzer = PatternAnalyzer.fromEnv();
3286
4356
  const analysis = await analyzer.analyzeAndSave(days);
3287
4357
  if (flags["json"] === true) {
@@ -3315,6 +4385,10 @@ Active Failure Patterns:`);
3315
4385
  console.log(` Factors: confidence=${f.rawConfidence} \xD7 impact=${f.impact} \xD7 recency=${f.recency} + boost=${f.stickinessBoost}`);
3316
4386
  if (p.mitigation) console.log(` Fix: ${p.mitigation}`);
3317
4387
  if (p.exampleMessages.length > 0) console.log(` e.g. ${p.exampleMessages[0]}`);
4388
+ if (p.workflowTypeBreakdown) {
4389
+ const topType = Object.entries(p.workflowTypeBreakdown).sort((a, b) => b[1] - a[1])[0];
4390
+ if (topType) console.log(` Top workflow type: ${topType[0]} (${topType[1]} failures)`);
4391
+ }
3318
4392
  }
3319
4393
  } else {
3320
4394
  console.log(`
@@ -3355,10 +4429,35 @@ Drift Detection: ${drift.healthy ? "HEALTHY" : "ALERTS FOUND"}`);
3355
4429
  console.log(`
3356
4430
  Patterns saved to ~/.kairos/patterns.json`);
3357
4431
  }
4432
+ async function handleSessions(flags) {
4433
+ const limitRaw = typeof flags["limit"] === "string" ? parseInt(flags["limit"], 10) : NaN;
4434
+ const limit = Number.isNaN(limitRaw) ? 20 : limitRaw;
4435
+ const analyzer = PatternAnalyzer.fromEnv();
4436
+ const sessions = await analyzer.getSessions(limit);
4437
+ if (flags["json"] === true) {
4438
+ console.log(JSON.stringify(sessions, null, 2));
4439
+ return;
4440
+ }
4441
+ if (sessions.length === 0) {
4442
+ console.log("No session history found. Run kairos patterns first to generate session data.");
4443
+ return;
4444
+ }
4445
+ console.log(`
4446
+ Recent Sessions (last ${sessions.length})`);
4447
+ console.log("\u2500".repeat(60));
4448
+ for (const s of [...sessions].reverse()) {
4449
+ const status = s.success ? "\u2713" : "\u2717";
4450
+ const typeTag = s.workflowType ? ` [${s.workflowType}]` : "";
4451
+ const attemptsStr = s.attempts > 1 ? ` (${s.attempts} attempts)` : "";
4452
+ const nameStr = s.workflowName ? ` ${s.workflowName}` : ` ${s.description.slice(0, 50)}`;
4453
+ const rulesStr = s.failedRules.length > 0 ? ` \u2014 rules ${s.failedRules.join(", ")} failed` : "";
4454
+ console.log(`${s.date} ${status}${nameStr}${attemptsStr}${typeTag}${rulesStr}`);
4455
+ }
4456
+ }
3358
4457
  async function handleInit() {
3359
4458
  const { writeFile: writeFile3, readFile: readFile2, mkdir: mkdir4 } = await import("fs/promises");
3360
- const { join: join7 } = await import("path");
3361
- const { homedir: homedir6 } = await import("os");
4459
+ const { join: join8 } = await import("path");
4460
+ const { homedir: homedir7 } = await import("os");
3362
4461
  const readline = await import("readline");
3363
4462
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
3364
4463
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
@@ -3366,7 +4465,7 @@ async function handleInit() {
3366
4465
  console.error(" Kairos SDK \u2014 Setup Wizard");
3367
4466
  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");
3368
4467
  console.error("");
3369
- const envPath = join7(process.cwd(), ".env");
4468
+ const envPath = join8(process.cwd(), ".env");
3370
4469
  let existingEnv = "";
3371
4470
  try {
3372
4471
  existingEnv = await readFile2(envPath, "utf-8");
@@ -3430,8 +4529,8 @@ async function handleInit() {
3430
4529
  });
3431
4530
  console.error(` Synced ${result.saved} templates (${result.blocked} blocked, ${result.skippedDuplicate} duplicates)`);
3432
4531
  }
3433
- const kairosDir = join7(homedir6(), ".kairos");
3434
- await mkdir4(join7(kairosDir, "telemetry"), { recursive: true });
4532
+ const kairosDir = join8(homedir7(), ".kairos");
4533
+ await mkdir4(join8(kairosDir, "telemetry"), { recursive: true });
3435
4534
  console.error("");
3436
4535
  console.error(" Setup complete! Try:");
3437
4536
  console.error("");
@@ -3440,7 +4539,7 @@ async function handleInit() {
3440
4539
  }
3441
4540
  async function main() {
3442
4541
  const { command, positional, flags } = parseArgs(process.argv);
3443
- if (!command || command === "help" || flags["help"] === true) {
4542
+ if (!command || command === "help" || command === "--help" || flags["help"] === true) {
3444
4543
  console.log(HELP);
3445
4544
  return;
3446
4545
  }
@@ -3451,9 +4550,15 @@ async function main() {
3451
4550
  case "build":
3452
4551
  await handleBuild(positional, flags);
3453
4552
  break;
4553
+ case "replace":
4554
+ await handleReplace(positional);
4555
+ break;
3454
4556
  case "patterns":
3455
4557
  await handlePatterns(flags);
3456
4558
  break;
4559
+ case "sessions":
4560
+ await handleSessions(flags);
4561
+ break;
3457
4562
  case "list":
3458
4563
  await handleList();
3459
4564
  break;