@kairos-sdk/core 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,809 @@
1
+ import {
2
+ KairosError,
3
+ N8nApiClient,
4
+ N8nFieldStripper,
5
+ N8nValidator,
6
+ PromptBuilder,
7
+ TelemetryReader,
8
+ generateUUID,
9
+ nullLogger,
10
+ scoreToMode
11
+ } from "./chunk-RYGYNOR6.js";
12
+
13
+ // src/library/null-library.ts
14
+ var NullLibrary = class {
15
+ async initialize() {
16
+ }
17
+ async search(_description, _options) {
18
+ return [];
19
+ }
20
+ async save(_workflow, _metadata) {
21
+ return generateUUID();
22
+ }
23
+ async recordDeployment(_id) {
24
+ }
25
+ async recordOutcome(_id, _outcome) {
26
+ }
27
+ async get(_id) {
28
+ return null;
29
+ }
30
+ async list(_filters) {
31
+ return [];
32
+ }
33
+ };
34
+
35
+ // src/errors/guard-error.ts
36
+ var GuardError = class extends KairosError {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "GuardError";
40
+ }
41
+ };
42
+
43
+ // src/providers/n8n/provider.ts
44
+ var N8nProvider = class {
45
+ constructor(client, stripper) {
46
+ this.client = client;
47
+ this.stripper = stripper;
48
+ }
49
+ client;
50
+ stripper;
51
+ platform = "n8n";
52
+ async deploy(workflow) {
53
+ const stripped = this.stripper.stripForCreate(workflow);
54
+ const response = await this.client.createWorkflow(stripped);
55
+ return { workflowId: response.id, name: response.name };
56
+ }
57
+ async update(id, workflow) {
58
+ const stripped = this.stripper.stripForUpdate(workflow);
59
+ const response = await this.client.updateWorkflow(id, stripped);
60
+ return { workflowId: response.id, name: response.name };
61
+ }
62
+ async get(id) {
63
+ const response = await this.client.getWorkflow(id);
64
+ return {
65
+ name: response.name,
66
+ nodes: response.nodes,
67
+ connections: response.connections,
68
+ ...response.settings !== void 0 ? { settings: response.settings } : {},
69
+ ...response.tags !== void 0 ? { tags: response.tags } : {}
70
+ };
71
+ }
72
+ async list() {
73
+ return this.client.listWorkflows();
74
+ }
75
+ async activate(id) {
76
+ await this.client.activateWorkflow(id);
77
+ }
78
+ async deactivate(id) {
79
+ await this.client.deactivateWorkflow(id);
80
+ }
81
+ async delete(id, options) {
82
+ if (options.confirm !== true) {
83
+ throw new GuardError("delete() requires { confirm: true } to prevent accidental deletion");
84
+ }
85
+ await this.client.deleteWorkflow(id);
86
+ }
87
+ async executions(workflowId, filter) {
88
+ return this.client.getExecutions(workflowId, filter);
89
+ }
90
+ async execution(id) {
91
+ return this.client.getExecution(id);
92
+ }
93
+ async listTags() {
94
+ return this.client.listTags();
95
+ }
96
+ async createTag(name) {
97
+ return this.client.createTag(name);
98
+ }
99
+ async tag(workflowId, tagIds) {
100
+ await this.client.tagWorkflow(workflowId, tagIds);
101
+ }
102
+ async untag(workflowId, tagIds) {
103
+ await this.client.untagWorkflow(workflowId, tagIds);
104
+ }
105
+ };
106
+
107
+ // src/errors/generation-error.ts
108
+ var GenerationError = class extends KairosError {
109
+ constructor(message, cause) {
110
+ super(message, cause);
111
+ this.name = "GenerationError";
112
+ }
113
+ };
114
+
115
+ // src/errors/response-parse-error.ts
116
+ var ResponseParseError = class extends KairosError {
117
+ constructor(message, cause) {
118
+ super(message, cause);
119
+ this.name = "ResponseParseError";
120
+ }
121
+ };
122
+
123
+ // src/errors/validation-error.ts
124
+ var ValidationError = class extends KairosError {
125
+ constructor(message, issues) {
126
+ super(message);
127
+ this.issues = issues;
128
+ this.name = "ValidationError";
129
+ }
130
+ issues;
131
+ };
132
+
133
+ // src/telemetry/collector.ts
134
+ import { appendFile, mkdir } from "fs/promises";
135
+ import { join } from "path";
136
+ import { homedir } from "os";
137
+
138
+ // src/telemetry/types.ts
139
+ var TELEMETRY_SCHEMA_VERSION = 2;
140
+
141
+ // src/telemetry/collector.ts
142
+ var TelemetryCollector = class {
143
+ dir;
144
+ sessionId;
145
+ dirReady = null;
146
+ constructor(dir) {
147
+ this.dir = dir ?? join(homedir(), ".kairos", "telemetry");
148
+ this.sessionId = generateUUID();
149
+ }
150
+ async emit(eventType, data) {
151
+ const event = {
152
+ schemaVersion: TELEMETRY_SCHEMA_VERSION,
153
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
154
+ sessionId: this.sessionId,
155
+ eventType,
156
+ data
157
+ };
158
+ if (!this.dirReady) {
159
+ this.dirReady = mkdir(this.dir, { recursive: true }).then(() => {
160
+ });
161
+ }
162
+ await this.dirReady;
163
+ const filename = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + ".jsonl";
164
+ const filepath = join(this.dir, filename);
165
+ await appendFile(filepath, JSON.stringify(event) + "\n", "utf-8");
166
+ }
167
+ };
168
+
169
+ // src/client.ts
170
+ import Anthropic from "@anthropic-ai/sdk";
171
+
172
+ // src/generation/designer.ts
173
+ var MAX_ATTEMPTS = 3;
174
+ var BASE_TEMPERATURE = 0.2;
175
+ var FINAL_TEMPERATURE = 0.1;
176
+ var GENERATE_WORKFLOW_TOOL = {
177
+ name: "generate_workflow",
178
+ description: "Generate a valid n8n workflow JSON object",
179
+ input_schema: {
180
+ type: "object",
181
+ properties: {
182
+ workflow: {
183
+ type: "object",
184
+ description: "The complete n8n workflow object",
185
+ properties: {
186
+ name: { type: "string" },
187
+ nodes: { type: "array" },
188
+ connections: { type: "object" },
189
+ settings: { type: "object" }
190
+ },
191
+ required: ["name", "nodes", "connections"]
192
+ },
193
+ credentialsNeeded: {
194
+ type: "array",
195
+ description: "List of credentials the user must configure before activating",
196
+ items: {
197
+ type: "object",
198
+ properties: {
199
+ service: { type: "string" },
200
+ credentialType: { type: "string" },
201
+ description: { type: "string" }
202
+ },
203
+ required: ["service", "credentialType", "description"]
204
+ }
205
+ },
206
+ error: {
207
+ type: "string",
208
+ description: "Set this if the request cannot be fulfilled \u2014 explain why"
209
+ }
210
+ },
211
+ required: []
212
+ }
213
+ };
214
+ var WorkflowDesigner = class {
215
+ constructor(anthropic, model, logger) {
216
+ this.anthropic = anthropic;
217
+ this.model = model;
218
+ this.logger = logger;
219
+ this.validator = new N8nValidator();
220
+ this.promptBuilder = new PromptBuilder();
221
+ }
222
+ anthropic;
223
+ model;
224
+ logger;
225
+ validator;
226
+ promptBuilder;
227
+ async design(request, matches, globalFailureRates = []) {
228
+ const attemptMetadata = [];
229
+ let lastErrors = [];
230
+ let attempts = 0;
231
+ const built = this.promptBuilder.build(request, matches, globalFailureRates);
232
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
233
+ attempts = attempt;
234
+ const temperature = attempt === MAX_ATTEMPTS ? FINAL_TEMPERATURE : BASE_TEMPERATURE;
235
+ let userMessage;
236
+ if (attempt === 1) {
237
+ userMessage = built.userMessage;
238
+ this.logger.debug("WorkflowDesigner: attempt 1", { description: request.description });
239
+ } else {
240
+ const issueLines = lastErrors.map(
241
+ (i) => `- [Rule ${i.rule}] ${i.message}${i.nodeId ? ` (node: ${i.nodeId})` : ""}`
242
+ );
243
+ userMessage = this.promptBuilder.buildCorrectionMessage(request, matches, issueLines, attempt - 1);
244
+ this.logger.debug(`WorkflowDesigner: correction attempt ${attempt}`, { issueCount: lastErrors.length });
245
+ }
246
+ const start = Date.now();
247
+ const message = await this.callClaude(built.system, userMessage, temperature);
248
+ const durationMs = Date.now() - start;
249
+ const parsed = this.extractToolUse(message);
250
+ if (parsed.error) {
251
+ throw new GenerationError(`Claude declined to generate workflow: ${parsed.error}`);
252
+ }
253
+ const validation = this.validator.validate(parsed.workflow);
254
+ const errors = validation.issues.filter((i) => i.severity === "error");
255
+ attemptMetadata.push({
256
+ attempt,
257
+ temperature,
258
+ durationMs,
259
+ tokensInput: message.usage.input_tokens,
260
+ tokensOutput: message.usage.output_tokens,
261
+ validationPassed: validation.valid,
262
+ issues: validation.issues
263
+ });
264
+ if (validation.valid) {
265
+ return { workflow: parsed.workflow, credentialsNeeded: parsed.credentialsNeeded, attempts, attemptMetadata };
266
+ }
267
+ lastErrors = errors;
268
+ this.logger.warn(`WorkflowDesigner: validation failed on attempt ${attempt}`, {
269
+ errorCount: errors.length
270
+ });
271
+ }
272
+ const finalIssues = attemptMetadata.at(-1)?.issues ?? lastErrors;
273
+ throw new ValidationError(
274
+ `Workflow failed validation after ${MAX_ATTEMPTS} attempts`,
275
+ finalIssues
276
+ );
277
+ }
278
+ async callClaude(system, userMessage, temperature) {
279
+ const controller = new AbortController();
280
+ const timer = setTimeout(() => controller.abort(), 12e4);
281
+ try {
282
+ return await this.anthropic.messages.create(
283
+ {
284
+ model: this.model,
285
+ max_tokens: 8192,
286
+ temperature,
287
+ system: system.map((b) => ({ type: b.type, text: b.text, ...b.cache_control ? { cache_control: b.cache_control } : {} })),
288
+ messages: [{ role: "user", content: userMessage }],
289
+ tools: [GENERATE_WORKFLOW_TOOL],
290
+ tool_choice: { type: "tool", name: "generate_workflow" }
291
+ },
292
+ { signal: controller.signal }
293
+ );
294
+ } catch (err) {
295
+ const detail = err instanceof Error ? err.message : String(err);
296
+ throw new GenerationError(`Anthropic API call failed: ${detail}`, err);
297
+ } finally {
298
+ clearTimeout(timer);
299
+ }
300
+ }
301
+ extractToolUse(message) {
302
+ const toolUseBlock = message.content.find(
303
+ (block) => block.type === "tool_use"
304
+ );
305
+ if (!toolUseBlock) {
306
+ throw new ResponseParseError(
307
+ "Claude response contained no tool_use block \u2014 forced tool_choice failed unexpectedly"
308
+ );
309
+ }
310
+ const input = toolUseBlock.input;
311
+ if (typeof input["error"] === "string") {
312
+ return {
313
+ workflow: { name: "", nodes: [], connections: {} },
314
+ credentialsNeeded: [],
315
+ error: input["error"]
316
+ };
317
+ }
318
+ if (!input["workflow"] || typeof input["workflow"] !== "object") {
319
+ throw new ResponseParseError("generate_workflow tool call missing workflow field");
320
+ }
321
+ const workflow = input["workflow"];
322
+ const credentialsNeeded = input["credentialsNeeded"] ?? [];
323
+ return { workflow, credentialsNeeded };
324
+ }
325
+ };
326
+
327
+ // src/client.ts
328
+ var DEFAULT_MODEL = "claude-sonnet-4-6";
329
+ var Kairos = class {
330
+ provider;
331
+ designer;
332
+ validator;
333
+ library;
334
+ logger;
335
+ telemetry;
336
+ telemetryReader;
337
+ model;
338
+ saveQueue = Promise.resolve(null);
339
+ constructor(options) {
340
+ const logger = options.logger ?? nullLogger;
341
+ this.model = options.model ?? DEFAULT_MODEL;
342
+ if (options.n8nBaseUrl && options.n8nApiKey) {
343
+ try {
344
+ new URL(options.n8nBaseUrl);
345
+ } catch {
346
+ throw new GuardError(`Invalid n8nBaseUrl: "${options.n8nBaseUrl}" \u2014 must be a valid URL`);
347
+ }
348
+ const apiClient = new N8nApiClient(options.n8nBaseUrl, options.n8nApiKey, logger);
349
+ const stripper = new N8nFieldStripper();
350
+ this.provider = new N8nProvider(apiClient, stripper);
351
+ } else {
352
+ this.provider = null;
353
+ }
354
+ const anthropic = new Anthropic({ apiKey: options.anthropicApiKey });
355
+ this.designer = new WorkflowDesigner(anthropic, this.model, logger);
356
+ this.validator = new N8nValidator();
357
+ this.library = options.library ?? new NullLibrary();
358
+ this.logger = logger;
359
+ if (options.telemetry === true) {
360
+ this.telemetry = new TelemetryCollector();
361
+ this.telemetryReader = new TelemetryReader();
362
+ } else if (typeof options.telemetry === "string") {
363
+ this.telemetry = new TelemetryCollector(options.telemetry);
364
+ this.telemetryReader = new TelemetryReader(options.telemetry);
365
+ } else {
366
+ this.telemetry = null;
367
+ this.telemetryReader = null;
368
+ }
369
+ }
370
+ requireProvider() {
371
+ if (!this.provider) {
372
+ throw new GuardError("n8nBaseUrl and n8nApiKey are required for this operation \u2014 set them in the Kairos constructor, or use { dryRun: true } for generation-only mode");
373
+ }
374
+ return this.provider;
375
+ }
376
+ validateDescription(description) {
377
+ if (!description || description.trim().length === 0) {
378
+ throw new GuardError("Description is required and must be non-empty");
379
+ }
380
+ }
381
+ async build(description, options) {
382
+ this.validateDescription(description);
383
+ this.logger.info("Kairos.build", { description, dryRun: options?.dryRun });
384
+ const buildStart = Date.now();
385
+ await this.telemetry?.emit("build_start", {
386
+ description,
387
+ model: this.model,
388
+ dryRun: options?.dryRun ?? false
389
+ });
390
+ await this.library.initialize();
391
+ const matches = await this.library.search(description);
392
+ if (matches.length > 0) {
393
+ const top = matches[0];
394
+ this.logger.info(`Library: ${matches.length} match(es), top="${top.workflow.description.slice(0, 50)}" score=${top.score.toFixed(2)} mode=${top.mode}`);
395
+ } else {
396
+ this.logger.info("Library: no matches (scratch mode)");
397
+ }
398
+ const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
399
+ if (globalFailureRates.length > 0) {
400
+ const highFreq = globalFailureRates.filter((r) => r.rate >= 0.15);
401
+ if (highFreq.length > 0) {
402
+ this.logger.info(`Telemetry: ${highFreq.length} high-frequency failure rule(s) will be warned about`);
403
+ }
404
+ }
405
+ const designResult = await this.designer.design(
406
+ { description, ...options?.name ? { name: options.name } : {} },
407
+ matches,
408
+ globalFailureRates
409
+ );
410
+ await this.emitAttemptTelemetry(description, designResult);
411
+ const workflow = options?.name ? { ...designResult.workflow, name: options.name } : designResult.workflow;
412
+ this.saveToLibrary(workflow, description, designResult, matches);
413
+ if (options?.dryRun) {
414
+ const totalTokensInput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
415
+ const totalTokensOutput2 = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
416
+ await this.telemetry?.emit("build_complete", {
417
+ description,
418
+ success: true,
419
+ totalAttempts: designResult.attempts,
420
+ totalDurationMs: Date.now() - buildStart,
421
+ totalTokensInput: totalTokensInput2,
422
+ totalTokensOutput: totalTokensOutput2,
423
+ workflowName: workflow.name,
424
+ workflowId: null,
425
+ dryRun: true,
426
+ credentialsNeeded: designResult.credentialsNeeded.length
427
+ });
428
+ return {
429
+ workflowId: null,
430
+ name: workflow.name,
431
+ workflow,
432
+ credentialsNeeded: designResult.credentialsNeeded,
433
+ activationRequired: true,
434
+ generationAttempts: designResult.attempts,
435
+ dryRun: true
436
+ };
437
+ }
438
+ const provider = this.requireProvider();
439
+ const deployed = await provider.deploy(workflow);
440
+ this.recordDeploy();
441
+ if (options?.activate) {
442
+ await provider.activate(deployed.workflowId);
443
+ }
444
+ const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
445
+ const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
446
+ await this.telemetry?.emit("build_complete", {
447
+ description,
448
+ success: true,
449
+ totalAttempts: designResult.attempts,
450
+ totalDurationMs: Date.now() - buildStart,
451
+ totalTokensInput,
452
+ totalTokensOutput,
453
+ workflowName: deployed.name,
454
+ workflowId: deployed.workflowId,
455
+ dryRun: false,
456
+ credentialsNeeded: designResult.credentialsNeeded.length
457
+ });
458
+ return {
459
+ workflowId: deployed.workflowId,
460
+ name: deployed.name,
461
+ workflow,
462
+ credentialsNeeded: designResult.credentialsNeeded,
463
+ activationRequired: !options?.activate,
464
+ generationAttempts: designResult.attempts,
465
+ dryRun: false
466
+ };
467
+ }
468
+ async replace(id, description) {
469
+ this.validateDescription(description);
470
+ this.logger.info("Kairos.update", { id, description });
471
+ const buildStart = Date.now();
472
+ await this.telemetry?.emit("build_start", {
473
+ description,
474
+ model: this.model,
475
+ dryRun: false
476
+ });
477
+ await this.library.initialize();
478
+ const matches = await this.library.search(description);
479
+ const globalFailureRates = await this.telemetryReader?.getFailureRates() ?? [];
480
+ const designResult = await this.designer.design({ description }, matches, globalFailureRates);
481
+ await this.emitAttemptTelemetry(description, designResult);
482
+ const provider = this.requireProvider();
483
+ const deployed = await provider.update(id, designResult.workflow);
484
+ this.saveToLibrary(designResult.workflow, description, designResult, matches);
485
+ this.recordDeploy();
486
+ const totalTokensInput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensInput, 0);
487
+ const totalTokensOutput = designResult.attemptMetadata.reduce((s, m) => s + m.tokensOutput, 0);
488
+ await this.telemetry?.emit("build_complete", {
489
+ description,
490
+ success: true,
491
+ totalAttempts: designResult.attempts,
492
+ totalDurationMs: Date.now() - buildStart,
493
+ totalTokensInput,
494
+ totalTokensOutput,
495
+ workflowName: deployed.name,
496
+ workflowId: deployed.workflowId,
497
+ dryRun: false,
498
+ credentialsNeeded: designResult.credentialsNeeded.length
499
+ });
500
+ return {
501
+ workflowId: deployed.workflowId,
502
+ name: deployed.name,
503
+ workflow: designResult.workflow,
504
+ credentialsNeeded: designResult.credentialsNeeded,
505
+ activationRequired: true,
506
+ generationAttempts: designResult.attempts,
507
+ dryRun: false
508
+ };
509
+ }
510
+ async drain() {
511
+ await this.saveQueue.catch(() => {
512
+ });
513
+ }
514
+ async emitAttemptTelemetry(description, designResult) {
515
+ for (const meta of designResult.attemptMetadata) {
516
+ await this.telemetry?.emit("generation_attempt", {
517
+ description,
518
+ attempt: meta.attempt,
519
+ temperature: meta.temperature,
520
+ durationMs: meta.durationMs,
521
+ tokensInput: meta.tokensInput,
522
+ tokensOutput: meta.tokensOutput,
523
+ validationPassed: meta.validationPassed,
524
+ issueCount: meta.issues.length,
525
+ issues: meta.issues.map((i) => ({ rule: i.rule, message: i.message }))
526
+ });
527
+ }
528
+ }
529
+ recordDeploy() {
530
+ this.saveQueue = this.saveQueue.then(async (savedId) => {
531
+ if (savedId) {
532
+ await this.library.recordDeployment(savedId);
533
+ }
534
+ return savedId;
535
+ }).catch((err) => {
536
+ this.logger.warn("Failed to record deployment (non-fatal)", { err: String(err) });
537
+ return null;
538
+ });
539
+ }
540
+ saveToLibrary(workflow, description, designResult, matches) {
541
+ const failedAttempts = designResult.attemptMetadata.filter((m) => !m.validationPassed);
542
+ const failurePatterns = failedAttempts.flatMap(
543
+ (m) => m.issues.map((i) => ({ rule: i.rule, message: i.message }))
544
+ );
545
+ const topMatch = matches[0];
546
+ const generationMode = topMatch ? scoreToMode(topMatch.score) : "scratch";
547
+ const autoTags = Array.from(new Set(
548
+ workflow.nodes.flatMap((n) => {
549
+ const bare = n.type.split(".").pop() ?? "";
550
+ const tags = [bare];
551
+ if (n.type.includes("Trigger") || n.type.includes("trigger")) tags.push(`trigger:${bare}`);
552
+ if (n.type.includes("langchain")) tags.push("ai");
553
+ return tags;
554
+ })
555
+ ));
556
+ const metadata = {
557
+ description,
558
+ generationMode,
559
+ generationAttempts: designResult.attempts
560
+ };
561
+ if (autoTags.length > 0) metadata.tags = autoTags;
562
+ if (failurePatterns.length > 0) metadata.failurePatterns = failurePatterns;
563
+ if (matches.length > 0) metadata.sourceWorkflowIds = matches.map((m) => m.workflow.id);
564
+ if (topMatch) metadata.topMatchScore = topMatch.score;
565
+ if (designResult.credentialsNeeded.length > 0) metadata.credentialsNeeded = designResult.credentialsNeeded;
566
+ const firstTryPass = designResult.attemptMetadata.length > 0 && designResult.attemptMetadata[0].validationPassed;
567
+ const failedRules = Array.from(new Set(
568
+ designResult.attemptMetadata.filter((m) => !m.validationPassed).flatMap((m) => m.issues.map((i) => i.rule))
569
+ ));
570
+ this.saveQueue = this.saveQueue.then(async () => {
571
+ const savedId = await this.library.save(workflow, metadata);
572
+ for (const match of matches) {
573
+ if (match.mode === "direct" || match.mode === "reference") {
574
+ await this.library.recordOutcome(match.workflow.id, {
575
+ attempts: designResult.attempts,
576
+ firstTryPass,
577
+ failedRules,
578
+ mode: match.mode
579
+ });
580
+ }
581
+ }
582
+ return savedId;
583
+ }).catch((err) => {
584
+ this.logger.warn("Failed to save workflow to library (non-fatal)", { err: String(err) });
585
+ return null;
586
+ });
587
+ }
588
+ async get(id) {
589
+ return this.requireProvider().get(id);
590
+ }
591
+ async list() {
592
+ return this.requireProvider().list();
593
+ }
594
+ async activate(id) {
595
+ await this.requireProvider().activate(id);
596
+ }
597
+ async deactivate(id) {
598
+ await this.requireProvider().deactivate(id);
599
+ }
600
+ async delete(id, options) {
601
+ await this.requireProvider().delete(id, options);
602
+ }
603
+ async executions(workflowId, filter) {
604
+ return this.requireProvider().executions(workflowId, filter);
605
+ }
606
+ async execution(id) {
607
+ return this.requireProvider().execution(id);
608
+ }
609
+ async listTags() {
610
+ return this.requireProvider().listTags();
611
+ }
612
+ async createTag(name) {
613
+ return this.requireProvider().createTag(name);
614
+ }
615
+ async tag(workflowId, tagIds) {
616
+ await this.requireProvider().tag(workflowId, tagIds);
617
+ }
618
+ async untag(workflowId, tagIds) {
619
+ await this.requireProvider().untag(workflowId, tagIds);
620
+ }
621
+ };
622
+
623
+ // src/templates/safety.ts
624
+ var BLOCKED_NODE_TYPES = /* @__PURE__ */ new Set([
625
+ "n8n-nodes-base.code",
626
+ "n8n-nodes-base.executeCommand",
627
+ "n8n-nodes-base.ssh"
628
+ ]);
629
+ var REVIEW_NODE_TYPES = /* @__PURE__ */ new Set([
630
+ "n8n-nodes-base.httpRequest"
631
+ ]);
632
+ var SECRET_PATTERNS = [
633
+ /sk-[a-zA-Z0-9]{20,}/,
634
+ /ghp_[a-zA-Z0-9]{36}/,
635
+ /xoxb-[0-9]+-[0-9]+-[a-zA-Z0-9]+/,
636
+ /AIza[a-zA-Z0-9_-]{35}/,
637
+ /AKIA[A-Z0-9]{16}/
638
+ ];
639
+ function assessTemplateSafety(workflow) {
640
+ const reasons = [];
641
+ let worst = "safe";
642
+ const escalate = (level, reason) => {
643
+ reasons.push(reason);
644
+ if (level === "blocked") worst = "blocked";
645
+ else if (level === "review" && worst === "safe") worst = "review";
646
+ };
647
+ for (const node of workflow.nodes) {
648
+ if (BLOCKED_NODE_TYPES.has(node.type)) {
649
+ escalate("blocked", `Contains ${node.type} node "${node.name}"`);
650
+ }
651
+ if (REVIEW_NODE_TYPES.has(node.type)) {
652
+ escalate("review", `Contains ${node.type} node "${node.name}"`);
653
+ }
654
+ const paramStr = JSON.stringify(node.parameters);
655
+ for (const pattern of SECRET_PATTERNS) {
656
+ if (pattern.test(paramStr)) {
657
+ escalate("blocked", `Node "${node.name}" parameters contain a hardcoded secret`);
658
+ break;
659
+ }
660
+ }
661
+ }
662
+ return { trustLevel: worst, reasons };
663
+ }
664
+
665
+ // src/templates/syncer.ts
666
+ var N8N_TEMPLATE_API = "https://api.n8n.io/api/templates";
667
+ var PAGE_SIZE = 50;
668
+ var DELAY_BETWEEN_FETCHES_MS = 200;
669
+ var DEFAULT_SETTINGS = {
670
+ executionOrder: "v1",
671
+ saveManualExecutions: true,
672
+ timezone: "UTC"
673
+ };
674
+ var TemplateSyncer = class {
675
+ constructor(library, logger) {
676
+ this.library = library;
677
+ this.validator = new N8nValidator();
678
+ this.logger = logger;
679
+ }
680
+ library;
681
+ validator;
682
+ logger;
683
+ async sync(options) {
684
+ const maxTemplates = options?.maxTemplates ?? 500;
685
+ await this.library.initialize();
686
+ const existing = await this.library.list();
687
+ const existingSourceIds = new Set(
688
+ existing.filter((w) => w.sourceKind === "n8n-template" && w.sourceId).map((w) => w.sourceId)
689
+ );
690
+ const progress = {
691
+ total: 0,
692
+ processed: 0,
693
+ saved: 0,
694
+ skippedPaid: 0,
695
+ skippedDuplicate: 0,
696
+ blocked: 0,
697
+ reviewed: 0
698
+ };
699
+ const templateIds = await this.fetchTemplateIds(maxTemplates, progress);
700
+ for (const id of templateIds) {
701
+ if (existingSourceIds.has(String(id))) {
702
+ progress.skippedDuplicate++;
703
+ progress.processed++;
704
+ options?.onProgress?.(progress);
705
+ continue;
706
+ }
707
+ try {
708
+ await this.processTemplate(id, progress);
709
+ } catch (err) {
710
+ this.logger.warn(`Failed to process template ${id}`, { err: String(err) });
711
+ }
712
+ progress.processed++;
713
+ options?.onProgress?.(progress);
714
+ await new Promise((resolve) => setTimeout(resolve, DELAY_BETWEEN_FETCHES_MS));
715
+ }
716
+ return progress;
717
+ }
718
+ async fetchTemplateIds(max, progress) {
719
+ const ids = [];
720
+ let page = 1;
721
+ while (ids.length < max) {
722
+ const url = `${N8N_TEMPLATE_API}/search?page=${page}&rows=${PAGE_SIZE}`;
723
+ const response = await fetch(url);
724
+ if (!response.ok) break;
725
+ const data = await response.json();
726
+ progress.total = Math.min(data.totalWorkflows, max);
727
+ for (const template of data.workflows) {
728
+ if (ids.length >= max) break;
729
+ if (template.price && template.price > 0) {
730
+ progress.skippedPaid++;
731
+ continue;
732
+ }
733
+ ids.push(template.id);
734
+ }
735
+ if (data.workflows.length < PAGE_SIZE) break;
736
+ page++;
737
+ await new Promise((resolve) => setTimeout(resolve, DELAY_BETWEEN_FETCHES_MS));
738
+ }
739
+ return ids;
740
+ }
741
+ async processTemplate(id, progress) {
742
+ const url = `${N8N_TEMPLATE_API}/workflows/${id}`;
743
+ const response = await fetch(url);
744
+ if (!response.ok) return;
745
+ const data = await response.json();
746
+ const templateMeta = data.workflow;
747
+ const rawWorkflow = templateMeta.workflow;
748
+ if (!rawWorkflow?.nodes?.length) return;
749
+ const workflow = {
750
+ name: templateMeta.name,
751
+ nodes: rawWorkflow.nodes.filter((n) => n.type && n.name),
752
+ connections: rawWorkflow.connections,
753
+ settings: rawWorkflow.settings ? { executionOrder: "v1", ...rawWorkflow.settings } : { ...DEFAULT_SETTINGS }
754
+ };
755
+ const validation = this.validator.validate(workflow);
756
+ const validationErrors = validation.issues.filter((i) => i.severity === "error");
757
+ if (validationErrors.length > 0) {
758
+ progress.blocked++;
759
+ this.logger.debug(`Template ${id} blocked: ${validationErrors.length} validation errors`);
760
+ return;
761
+ }
762
+ const safety = assessTemplateSafety(workflow);
763
+ if (safety.trustLevel === "blocked") {
764
+ progress.blocked++;
765
+ this.logger.debug(`Template ${id} blocked: ${safety.reasons.join(", ")}`);
766
+ return;
767
+ }
768
+ if (safety.trustLevel === "review") {
769
+ progress.reviewed++;
770
+ }
771
+ const description = this.cleanDescription(templateMeta.description);
772
+ const autoTags = Array.from(new Set(
773
+ workflow.nodes.flatMap((n) => {
774
+ const bare = n.type.split(".").pop() ?? "";
775
+ const tags = [bare];
776
+ if (n.type.includes("Trigger") || n.type.includes("trigger")) tags.push(`trigger:${bare}`);
777
+ if (n.type.includes("langchain")) tags.push("ai");
778
+ return tags;
779
+ })
780
+ ));
781
+ const metadata = {
782
+ description,
783
+ tags: autoTags,
784
+ sourceKind: "n8n-template",
785
+ sourceId: String(id),
786
+ sourceUrl: `https://n8n.io/workflows/${id}`,
787
+ trustLevel: safety.trustLevel
788
+ };
789
+ await this.library.save(workflow, metadata);
790
+ progress.saved++;
791
+ this.logger.debug(`Template ${id} saved: "${templateMeta.name}" (${safety.trustLevel})`);
792
+ }
793
+ cleanDescription(raw) {
794
+ return raw.replace(/#{1,6}\s*/g, "").replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\n{3,}/g, "\n\n").trim().slice(0, 500);
795
+ }
796
+ };
797
+
798
+ export {
799
+ NullLibrary,
800
+ GuardError,
801
+ N8nProvider,
802
+ GenerationError,
803
+ ResponseParseError,
804
+ ValidationError,
805
+ TelemetryCollector,
806
+ Kairos,
807
+ TemplateSyncer
808
+ };
809
+ //# sourceMappingURL=chunk-KQSNT3HZ.js.map