@prompd/core 0.5.0-beta.10

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/index.js ADDED
@@ -0,0 +1,4627 @@
1
+ import * as yaml from 'yaml';
2
+ import semver from 'semver';
3
+ import * as nunjucks from 'nunjucks';
4
+
5
+ // src/lib/parser.ts
6
+ var PrompdParser = class {
7
+ parseContent(content, _filePath) {
8
+ const cleanContent = content.replace(/^\ufeff/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
9
+ if (!cleanContent.startsWith("---\n")) {
10
+ throw new Error("File must start with YAML frontmatter (---)");
11
+ }
12
+ const afterOpening = cleanContent.slice(4);
13
+ const closingIndex = afterOpening.indexOf("\n---\n");
14
+ const closingAtEnd = afterOpening.indexOf("\n---");
15
+ let delimiterIndex = -1;
16
+ if (closingIndex !== -1) {
17
+ delimiterIndex = closingIndex;
18
+ } else if (closingAtEnd !== -1 && afterOpening.substring(closingAtEnd) === "\n---") {
19
+ delimiterIndex = closingAtEnd;
20
+ }
21
+ if (delimiterIndex === -1) {
22
+ throw new Error("Invalid frontmatter format");
23
+ }
24
+ const yamlContent = afterOpening.substring(0, delimiterIndex);
25
+ const bodyStart = delimiterIndex + 4;
26
+ const markdownContent = bodyStart < afterOpening.length ? afterOpening.substring(bodyStart) : "";
27
+ let metadata;
28
+ try {
29
+ metadata = yaml.parse(yamlContent);
30
+ } catch (error) {
31
+ throw new Error(`Failed to parse YAML frontmatter: ${error instanceof Error ? error.message : "Unknown error"}`);
32
+ }
33
+ if (!metadata.id) {
34
+ throw new Error("id field is required");
35
+ }
36
+ const kebabCaseRegex = /^[a-z0-9-]+$/;
37
+ if (!kebabCaseRegex.test(metadata.id)) {
38
+ throw new Error(`id '${metadata.id}' must use kebab-case (lowercase letters, numbers, hyphens only)`);
39
+ }
40
+ const sections = {};
41
+ const sectionRegex = /^## (.+)$/gm;
42
+ let match;
43
+ while ((match = sectionRegex.exec(markdownContent)) !== null) {
44
+ sections[match[1].toLowerCase()] = match[1];
45
+ }
46
+ return {
47
+ metadata,
48
+ content: markdownContent.trim(),
49
+ sections
50
+ };
51
+ }
52
+ /**
53
+ * Parse + validate .prmd content. Pure (no file system) so it runs in the
54
+ * browser. Returns parse errors as a single issue when content is malformed.
55
+ */
56
+ validateContent(content, _filePath) {
57
+ let prompd;
58
+ try {
59
+ prompd = this.parseContent(content, _filePath);
60
+ } catch (error) {
61
+ return [{
62
+ level: "error",
63
+ message: error instanceof Error ? error.message : "Unknown parsing error"
64
+ }];
65
+ }
66
+ return this.validatePrompdFile(prompd);
67
+ }
68
+ validatePrompdFile(prompd) {
69
+ const issues = [];
70
+ if (!prompd.metadata.name) {
71
+ issues.push({
72
+ level: "error",
73
+ message: "name field is required"
74
+ });
75
+ }
76
+ if (prompd.metadata.version && !this.isValidSemver(prompd.metadata.version)) {
77
+ issues.push({
78
+ level: "error",
79
+ message: `invalid semantic version: ${prompd.metadata.version}`
80
+ });
81
+ }
82
+ const variables = /* @__PURE__ */ new Set();
83
+ const allParams = [
84
+ ...prompd.metadata.parameters || [],
85
+ ...prompd.metadata.variables || []
86
+ ];
87
+ for (const param of allParams) {
88
+ if (!param.name) {
89
+ issues.push({
90
+ level: "error",
91
+ message: "parameter name cannot be empty"
92
+ });
93
+ continue;
94
+ }
95
+ variables.add(param.name);
96
+ const validTypes = ["string", "number", "boolean", "array", "object"];
97
+ if (!validTypes.includes(param.type)) {
98
+ issues.push({
99
+ level: "error",
100
+ message: `invalid parameter type: ${param.type}. Must be one of: ${validTypes.join(", ")}`
101
+ });
102
+ }
103
+ if (param.pattern && param.type !== "string") {
104
+ issues.push({
105
+ level: "warning",
106
+ message: `pattern validation only applies to string parameters: ${param.name}`
107
+ });
108
+ }
109
+ if ((param.minimum !== void 0 || param.maximum !== void 0) && param.type !== "number") {
110
+ issues.push({
111
+ level: "warning",
112
+ message: `minimum/maximum validation only applies to number parameters: ${param.name}`
113
+ });
114
+ }
115
+ }
116
+ const variableReferences = prompd.content.match(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g);
117
+ if (variableReferences) {
118
+ for (const ref of variableReferences) {
119
+ const varName = ref.slice(1, -1);
120
+ if (!variables.has(varName) && varName !== "inputs") {
121
+ issues.push({
122
+ level: "error",
123
+ message: `undefined variable referenced: ${varName}`
124
+ });
125
+ }
126
+ }
127
+ }
128
+ return issues;
129
+ }
130
+ isValidSemver(version) {
131
+ const semverRegex = /^(\d+)\.(\d+)\.(\d+)$/;
132
+ return semverRegex.test(version);
133
+ }
134
+ };
135
+
136
+ // src/types/index.ts
137
+ var CODE_EXTENSIONS = [
138
+ // JavaScript/TypeScript ecosystem
139
+ ".ts",
140
+ ".tsx",
141
+ ".js",
142
+ ".jsx",
143
+ ".mjs",
144
+ ".cjs",
145
+ // Python
146
+ ".py",
147
+ ".pyw",
148
+ ".pyi",
149
+ // Shell/Scripts
150
+ ".sh",
151
+ ".bash",
152
+ ".zsh",
153
+ ".fish",
154
+ ".ps1",
155
+ ".psm1",
156
+ ".psd1",
157
+ ".bat",
158
+ ".cmd",
159
+ // Ruby
160
+ ".rb",
161
+ ".rake",
162
+ ".gemspec",
163
+ // Go
164
+ ".go",
165
+ // Rust
166
+ ".rs",
167
+ // C/C++
168
+ ".c",
169
+ ".cpp",
170
+ ".cc",
171
+ ".cxx",
172
+ ".h",
173
+ ".hpp",
174
+ ".hxx",
175
+ // Java/JVM
176
+ ".java",
177
+ ".kt",
178
+ ".kts",
179
+ ".scala",
180
+ ".groovy",
181
+ // .NET
182
+ ".cs",
183
+ ".fs",
184
+ ".vb",
185
+ // PHP
186
+ ".php",
187
+ ".phtml",
188
+ // Perl
189
+ ".pl",
190
+ ".pm",
191
+ // Swift
192
+ ".swift",
193
+ // Lua
194
+ ".lua",
195
+ // R
196
+ ".r",
197
+ ".R",
198
+ // Julia
199
+ ".jl",
200
+ // Elixir/Erlang
201
+ ".ex",
202
+ ".exs",
203
+ ".erl",
204
+ // Haskell
205
+ ".hs",
206
+ ".lhs",
207
+ // Web frameworks
208
+ ".vue",
209
+ ".svelte",
210
+ // SQL (can be dangerous with stored procedures)
211
+ ".sql"
212
+ ];
213
+ var CONTENT_TYPES = {
214
+ // JavaScript/TypeScript
215
+ ".ts": "typescript",
216
+ ".tsx": "typescript-react",
217
+ ".js": "javascript",
218
+ ".jsx": "javascript-react",
219
+ ".mjs": "javascript-module",
220
+ ".cjs": "javascript-commonjs",
221
+ // Python
222
+ ".py": "python",
223
+ ".pyw": "python-windows",
224
+ ".pyi": "python-stub",
225
+ // Shell
226
+ ".sh": "shell",
227
+ ".bash": "bash",
228
+ ".zsh": "zsh",
229
+ ".fish": "fish",
230
+ ".ps1": "powershell",
231
+ ".psm1": "powershell-module",
232
+ ".psd1": "powershell-data",
233
+ ".bat": "batch",
234
+ ".cmd": "batch",
235
+ // Ruby
236
+ ".rb": "ruby",
237
+ ".rake": "ruby-rake",
238
+ ".gemspec": "ruby-gemspec",
239
+ // Go
240
+ ".go": "go",
241
+ // Rust
242
+ ".rs": "rust",
243
+ // C/C++
244
+ ".c": "c",
245
+ ".cpp": "cpp",
246
+ ".cc": "cpp",
247
+ ".cxx": "cpp",
248
+ ".h": "c-header",
249
+ ".hpp": "cpp-header",
250
+ ".hxx": "cpp-header",
251
+ // Java/JVM
252
+ ".java": "java",
253
+ ".kt": "kotlin",
254
+ ".kts": "kotlin-script",
255
+ ".scala": "scala",
256
+ ".groovy": "groovy",
257
+ // .NET
258
+ ".cs": "csharp",
259
+ ".fs": "fsharp",
260
+ ".vb": "vb",
261
+ // PHP
262
+ ".php": "php",
263
+ ".phtml": "php-html",
264
+ // Perl
265
+ ".pl": "perl",
266
+ ".pm": "perl-module",
267
+ // Swift
268
+ ".swift": "swift",
269
+ // Lua
270
+ ".lua": "lua",
271
+ // R
272
+ ".r": "r",
273
+ ".R": "r",
274
+ // Julia
275
+ ".jl": "julia",
276
+ // Elixir/Erlang
277
+ ".ex": "elixir",
278
+ ".exs": "elixir-script",
279
+ ".erl": "erlang",
280
+ // Haskell
281
+ ".hs": "haskell",
282
+ ".lhs": "literate-haskell",
283
+ // Web frameworks
284
+ ".vue": "vue",
285
+ ".svelte": "svelte",
286
+ // SQL
287
+ ".sql": "sql"
288
+ };
289
+ function needsFrontmatterProtection(filePath) {
290
+ const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
291
+ return CODE_EXTENSIONS.includes(ext);
292
+ }
293
+ function getContentType(filePath) {
294
+ const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
295
+ return CONTENT_TYPES[ext] || "text";
296
+ }
297
+ var PACKAGE_TYPE_DIRS = {
298
+ "package": "packages",
299
+ "workflow": "workflows",
300
+ "skill": "skills",
301
+ "node-template": "templates"
302
+ };
303
+ var VALID_PACKAGE_TYPES = Object.keys(PACKAGE_TYPE_DIRS);
304
+ var TOOL_DEPLOY_DIRS = {
305
+ "claude": "~/.claude/skills"
306
+ };
307
+ function isValidPackageType(type) {
308
+ return VALID_PACKAGE_TYPES.includes(type);
309
+ }
310
+ function getInstallDirForType(type) {
311
+ return PACKAGE_TYPE_DIRS[type] || "packages";
312
+ }
313
+
314
+ // src/lib/errors.ts
315
+ var PrompdError = class _PrompdError extends Error {
316
+ constructor(message) {
317
+ super(message);
318
+ this.name = "PrompdError";
319
+ Object.setPrototypeOf(this, _PrompdError.prototype);
320
+ }
321
+ };
322
+ var ParseError = class _ParseError extends PrompdError {
323
+ constructor(message) {
324
+ super(message);
325
+ this.name = "ParseError";
326
+ Object.setPrototypeOf(this, _ParseError.prototype);
327
+ }
328
+ };
329
+ var ValidationError = class _ValidationError extends PrompdError {
330
+ constructor(message) {
331
+ super(message);
332
+ this.name = "ValidationError";
333
+ Object.setPrototypeOf(this, _ValidationError.prototype);
334
+ }
335
+ };
336
+ var CompilationError = class _CompilationError extends PrompdError {
337
+ constructor(message) {
338
+ super(message);
339
+ this.name = "CompilationError";
340
+ Object.setPrototypeOf(this, _CompilationError.prototype);
341
+ }
342
+ };
343
+ var SecurityError = class _SecurityError extends PrompdError {
344
+ constructor(message) {
345
+ super(message);
346
+ this.name = "SecurityError";
347
+ Object.setPrototypeOf(this, _SecurityError.prototype);
348
+ }
349
+ };
350
+
351
+ // src/lib/compiler/types.ts
352
+ var CompilationStage = /* @__PURE__ */ ((CompilationStage2) => {
353
+ CompilationStage2["LEXICAL_ANALYSIS"] = "lexical_analysis";
354
+ CompilationStage2["DEPENDENCY_RESOLUTION"] = "dependency_resolution";
355
+ CompilationStage2["SEMANTIC_ANALYSIS"] = "semantic_analysis";
356
+ CompilationStage2["ASSET_EXTRACTION"] = "asset_extraction";
357
+ CompilationStage2["TEMPLATE_PROCESSING"] = "template_processing";
358
+ CompilationStage2["CODE_GENERATION"] = "code_generation";
359
+ return CompilationStage2;
360
+ })(CompilationStage || {});
361
+ var CompilationContext = class {
362
+ // Injected; absent in browser (single-file compile)
363
+ constructor(sourceFile, options = {}) {
364
+ this.sourceFile = sourceFile;
365
+ this.dependencies = {};
366
+ this.parameters = options.parameters || {};
367
+ this.contexts = [];
368
+ this.errors = [];
369
+ this.warnings = [];
370
+ this.diagnostics = [];
371
+ this.outputFormat = options.outputFormat || "markdown";
372
+ this.verbose = options.verbose || false;
373
+ this.registryUrl = options.registryUrl;
374
+ this.workspaceRoot = options.workspaceRoot;
375
+ this.packageResolver = options.packageResolver;
376
+ }
377
+ /**
378
+ * Add an error to the compilation context (legacy string format).
379
+ */
380
+ addError(message) {
381
+ this.errors.push(message);
382
+ this.diagnostics.push({ message, severity: "error" });
383
+ }
384
+ /**
385
+ * Add a warning to the compilation context (legacy string format).
386
+ */
387
+ addWarning(message) {
388
+ this.warnings.push(message);
389
+ this.diagnostics.push({ message, severity: "warning" });
390
+ }
391
+ /**
392
+ * Add a structured diagnostic with location information.
393
+ */
394
+ addDiagnostic(diagnostic) {
395
+ this.diagnostics.push(diagnostic);
396
+ if (diagnostic.severity === "error") {
397
+ this.errors.push(diagnostic.message);
398
+ } else if (diagnostic.severity === "warning") {
399
+ this.warnings.push(diagnostic.message);
400
+ }
401
+ }
402
+ /**
403
+ * Find line and column for a pattern in the raw source.
404
+ * Returns 1-indexed line and column numbers.
405
+ */
406
+ findLocation(pattern) {
407
+ if (!this.rawSource) return null;
408
+ const match = typeof pattern === "string" ? this.rawSource.indexOf(pattern) : this.rawSource.search(pattern);
409
+ if (match === -1) return null;
410
+ const beforeMatch = this.rawSource.substring(0, match);
411
+ const lines = beforeMatch.split("\n");
412
+ const line = lines.length;
413
+ const column = (lines[lines.length - 1]?.length || 0) + 1;
414
+ const matchLength = typeof pattern === "string" ? pattern.length : this.rawSource.match(pattern)?.[0]?.length || 0;
415
+ const matchText = this.rawSource.substring(match, match + matchLength);
416
+ const matchLines = matchText.split("\n");
417
+ const endLine = line + matchLines.length - 1;
418
+ const endColumn = matchLines.length === 1 ? column + matchLength : matchLines[matchLines.length - 1].length + 1;
419
+ return { line, column, endLine, endColumn };
420
+ }
421
+ /**
422
+ * Check if compilation has errors.
423
+ */
424
+ hasErrors() {
425
+ return this.errors.length > 0 || this.diagnostics.some((d) => d.severity === "error");
426
+ }
427
+ /**
428
+ * Get all diagnostics (errors and warnings).
429
+ */
430
+ getDiagnostics() {
431
+ return this.diagnostics;
432
+ }
433
+ /**
434
+ * Get only error diagnostics.
435
+ */
436
+ getErrors() {
437
+ return this.diagnostics.filter((d) => d.severity === "error");
438
+ }
439
+ /**
440
+ * Get only warning diagnostics.
441
+ */
442
+ getWarnings() {
443
+ return this.diagnostics.filter((d) => d.severity === "warning");
444
+ }
445
+ };
446
+ var DEFAULT_SECURITY_CONFIG = {
447
+ maxFileSize: 10 * 1024 * 1024,
448
+ // 10MB
449
+ allowedExtensions: [
450
+ ".prmd",
451
+ ".md",
452
+ ".txt",
453
+ ".json",
454
+ ".yaml",
455
+ ".yml",
456
+ ".xlsx",
457
+ ".docx",
458
+ ".pdf",
459
+ ".png",
460
+ ".jpg",
461
+ ".jpeg",
462
+ ".gif"
463
+ ],
464
+ maxTemplateDepth: 10,
465
+ templateTimeout: 5e3
466
+ // 5 seconds
467
+ };
468
+ function posixNormalize(p) {
469
+ const isAbs = p.startsWith("/");
470
+ const out = [];
471
+ for (const part of p.split("/")) {
472
+ if (part === "" || part === ".") continue;
473
+ if (part === "..") {
474
+ if (out.length && out[out.length - 1] !== "..") out.pop();
475
+ else if (!isAbs) out.push("..");
476
+ } else {
477
+ out.push(part);
478
+ }
479
+ }
480
+ const joined = out.join("/");
481
+ return isAbs ? "/" + joined : joined || ".";
482
+ }
483
+ function posixIsAbsolute(p) {
484
+ return p.startsWith("/");
485
+ }
486
+ function stripFilePath(packageRef) {
487
+ const match = packageRef.match(/^(@[\w.-]+\/[\w.-]+(?:@[\w.-]+)?)/);
488
+ return match ? match[1] : packageRef;
489
+ }
490
+ function parsePackageReference(packageRef) {
491
+ const stripped = stripFilePath(packageRef);
492
+ const match = stripped.match(/^(@[\w.-]+\/[\w.-]+)@?([\w.-]+)?$/);
493
+ if (!match) {
494
+ throw new Error(`Invalid package reference format: ${packageRef}`);
495
+ }
496
+ const name = match[1];
497
+ const rawVersion = match[2] || "latest";
498
+ const version = semver.valid(rawVersion) || rawVersion;
499
+ const scopeMatch = name.match(/^(@[\w.-]+)\//);
500
+ const scope = scopeMatch ? scopeMatch[1] : void 0;
501
+ return { name, version, scope };
502
+ }
503
+ function parsePackageReferenceWithPath(packageRef) {
504
+ const { name, version, scope } = parsePackageReference(packageRef);
505
+ const stripped = stripFilePath(packageRef);
506
+ const remainder = packageRef.slice(stripped.length);
507
+ const filePath = remainder.length > 1 ? remainder.slice(1) : void 0;
508
+ return { name, version, scope, filePath };
509
+ }
510
+ function isValidPackageReference(packageRef) {
511
+ if (packageRef.includes("\0")) {
512
+ return false;
513
+ }
514
+ const base = stripFilePath(packageRef);
515
+ const pattern = /^@[\w.-]+\/[\w.-]+(@[\w.-]+)?$/;
516
+ if (!pattern.test(base)) {
517
+ return false;
518
+ }
519
+ const remainder = packageRef.slice(base.length);
520
+ if (remainder.length > 0) {
521
+ if (!remainder.startsWith("/")) {
522
+ return false;
523
+ }
524
+ const filePath = remainder.slice(1);
525
+ if (filePath.includes("..") || filePath.includes("//")) {
526
+ return false;
527
+ }
528
+ if (!/^[\w./\-]+$/.test(filePath)) {
529
+ return false;
530
+ }
531
+ }
532
+ if (base.includes("..") || base.includes("//")) {
533
+ return false;
534
+ }
535
+ return true;
536
+ }
537
+ function resolvePackageFile(packagePath, filePath) {
538
+ const normalized = posixNormalize(filePath.replace(/\\/g, "/"));
539
+ if (normalized.startsWith("..") || posixIsAbsolute(normalized)) {
540
+ throw new SecurityError(`Invalid file path within package: ${filePath}`);
541
+ }
542
+ const packageNorm = packagePath.replace(/\\/g, "/");
543
+ const resolvedPath = posixNormalize(`${packageNorm}/${normalized}`);
544
+ if (!resolvedPath.startsWith(packageNorm.replace(/\/$/, ""))) {
545
+ throw new SecurityError(`Path traversal detected: ${filePath}`);
546
+ }
547
+ return resolvedPath;
548
+ }
549
+
550
+ // src/lib/compiler/path-utils.ts
551
+ function normalizePosix(p) {
552
+ const isAbs = p.startsWith("/");
553
+ const out = [];
554
+ for (const seg of p.split("/")) {
555
+ if (seg === "" || seg === ".") continue;
556
+ if (seg === "..") {
557
+ if (out.length && out[out.length - 1] !== "..") out.pop();
558
+ else if (!isAbs) out.push("..");
559
+ } else {
560
+ out.push(seg);
561
+ }
562
+ }
563
+ const joined = out.join("/");
564
+ return isAbs ? "/" + joined : joined || ".";
565
+ }
566
+ function joinPosix(...segments) {
567
+ const filtered = segments.filter((s) => s && s.length > 0);
568
+ if (filtered.length === 0) return ".";
569
+ return normalizePosix(filtered.join("/"));
570
+ }
571
+ function resolvePosix(base, p) {
572
+ if (p.startsWith("/")) return normalizePosix(p);
573
+ return joinPosix(base, p);
574
+ }
575
+ function dirnamePosix(p) {
576
+ const norm = p.replace(/\/+$/, "");
577
+ const idx = norm.lastIndexOf("/");
578
+ if (idx === -1) return ".";
579
+ if (idx === 0) return "/";
580
+ return norm.slice(0, idx);
581
+ }
582
+ function basenamePosix(p) {
583
+ const norm = p.replace(/\/+$/, "");
584
+ const idx = norm.lastIndexOf("/");
585
+ return idx === -1 ? norm : norm.slice(idx + 1);
586
+ }
587
+ function isAbsolutePosix(p) {
588
+ return p.startsWith("/");
589
+ }
590
+ function extname(p) {
591
+ const base = basenamePosix(p);
592
+ const dot = base.lastIndexOf(".");
593
+ return dot <= 0 ? "" : base.slice(dot);
594
+ }
595
+ var PROMPD_EXTENSIONS = [".prmd", ".md"];
596
+ function isPrompdFile(p) {
597
+ const lower = p.toLowerCase();
598
+ return lower.endsWith(".prmd") || lower.endsWith(".md");
599
+ }
600
+
601
+ // src/lib/compiler/pipeline.ts
602
+ var CompilerPipeline = class {
603
+ constructor(stages, securityConfig) {
604
+ this.stages = stages || [];
605
+ this.securityConfig = securityConfig || DEFAULT_SECURITY_CONFIG;
606
+ }
607
+ /**
608
+ * Register a compilation stage.
609
+ */
610
+ registerStage(stage) {
611
+ this.stages.push(stage);
612
+ }
613
+ /**
614
+ * Execute the compilation pipeline.
615
+ *
616
+ * @param source - Path to .prmd file or package reference
617
+ * @param options - Compilation options
618
+ * @returns Compilation context with result or errors
619
+ */
620
+ async execute(source, options = {}) {
621
+ const fileSystem = options.fileSystem;
622
+ if (!fileSystem) {
623
+ throw new PrompdError("A fileSystem must be provided to the compiler (e.g. MemoryFileSystem).");
624
+ }
625
+ const sourcePath = await this.resolveSource(source, fileSystem, options.packageResolver, options.registryUrl, options.workspaceRoot);
626
+ const context = new CompilationContext(sourcePath, options);
627
+ context.fileSystem = fileSystem;
628
+ for (const stage of this.stages) {
629
+ try {
630
+ if (context.verbose) {
631
+ console.log(`Running stage: ${stage.getName()}`);
632
+ }
633
+ await stage.process(context);
634
+ } catch (error) {
635
+ const errorMessage = error instanceof Error ? error.message : String(error);
636
+ context.addError(`${stage.getName()} failed: ${errorMessage}`);
637
+ }
638
+ }
639
+ return context;
640
+ }
641
+ /**
642
+ * Resolve source to file path (handles package references).
643
+ *
644
+ * @param source - Package reference or file path
645
+ * @param fileSystem - File system to use for resolution
646
+ * @param customFileSystem - Optional custom file system (used to detect in-memory mode)
647
+ * @param registryUrl - Optional registry URL for package resolution
648
+ * @param workspaceRoot - Optional workspace root for package cache location
649
+ */
650
+ async resolveSource(source, fileSystem, packageResolver, registryUrl, workspaceRoot) {
651
+ if (source.startsWith("@")) {
652
+ if (!packageResolver) {
653
+ throw new PrompdError(
654
+ `Package resolution is unavailable in this environment: ${source}. Compile via the backend, or provide a packageResolver in the compilation options.`
655
+ );
656
+ }
657
+ try {
658
+ const { filePath } = parsePackageReferenceWithPath(source);
659
+ const packagePath = await packageResolver.resolvePackage(source, {
660
+ fileSystem,
661
+ registryUrl,
662
+ workspaceRoot
663
+ });
664
+ if (filePath) {
665
+ return resolvePackageFile(packagePath, filePath);
666
+ }
667
+ const prmdFiles = await this.findPromdFiles(packagePath, fileSystem);
668
+ if (prmdFiles.length === 0) {
669
+ throw new Error(`No .prmd files found in package: ${source}`);
670
+ }
671
+ return prmdFiles[0];
672
+ } catch (error) {
673
+ throw new PrompdError(`Failed to resolve package ${source}: ${error}`);
674
+ }
675
+ }
676
+ return source;
677
+ }
678
+ /**
679
+ * Find all .prmd files in a directory.
680
+ *
681
+ * @param dir - Directory path
682
+ * @param fileSystem - File system to use
683
+ */
684
+ async findPromdFiles(dir, fileSystem) {
685
+ const files = [];
686
+ try {
687
+ const entries = await fileSystem.readdir(dir);
688
+ for (const entry of entries) {
689
+ const fullPath = fileSystem.join(dir, entry);
690
+ if (await fileSystem.isDirectory(fullPath)) {
691
+ const subFiles = await this.findPromdFiles(fullPath, fileSystem);
692
+ files.push(...subFiles);
693
+ } else if (isPrompdFile(entry)) {
694
+ files.push(fullPath);
695
+ }
696
+ }
697
+ } catch (error) {
698
+ }
699
+ return files;
700
+ }
701
+ /**
702
+ * Get registered stages.
703
+ */
704
+ getStages() {
705
+ return [...this.stages];
706
+ }
707
+ };
708
+
709
+ // src/lib/compiler/stages/lexical.ts
710
+ var LexicalAnalysisStage = class {
711
+ constructor() {
712
+ this.parser = new PrompdParser();
713
+ }
714
+ async process(context) {
715
+ try {
716
+ const fileContent = await context.fileSystem.readFile(context.sourceFile);
717
+ context.rawSource = fileContent;
718
+ const prompd = this.parser.parseContent(fileContent, context.sourceFile);
719
+ context.metadata = prompd.metadata;
720
+ context.content = prompd.content;
721
+ context.content = this.injectYamlSections(prompd.metadata, context.content);
722
+ if (context.verbose) {
723
+ console.log(`\u2713 Parsed file: ${context.sourceFile}`);
724
+ console.log(` - Name: ${prompd.metadata.name}`);
725
+ console.log(` - Version: ${prompd.metadata.version || "N/A"}`);
726
+ console.log(` - Parameters: ${prompd.metadata.parameters?.length || 0}`);
727
+ }
728
+ } catch (error) {
729
+ const errorMessage = error instanceof Error ? error.message : String(error);
730
+ let line;
731
+ let column;
732
+ const lineMatch = errorMessage.match(/at line (\d+)(?:,? column (\d+))?/i);
733
+ if (lineMatch) {
734
+ line = parseInt(lineMatch[1], 10) + 1;
735
+ column = lineMatch[2] ? parseInt(lineMatch[2], 10) : 1;
736
+ }
737
+ context.addDiagnostic({
738
+ message: `Lexical analysis failed: ${errorMessage}`,
739
+ severity: "error",
740
+ source: "lexical",
741
+ line,
742
+ column,
743
+ code: "PARSE_ERROR"
744
+ });
745
+ }
746
+ }
747
+ /**
748
+ * Convert YAML-defined sections (system, user, assistant, etc.) to markdown sections.
749
+ * These are appended to the content after any markdown-defined sections.
750
+ */
751
+ injectYamlSections(metadata, content) {
752
+ const yamlSections = [];
753
+ const sectionOrder = ["system", "context", "task", "user", "assistant", "response", "output"];
754
+ for (const sectionName of sectionOrder) {
755
+ const sectionValue = metadata[sectionName];
756
+ if (sectionValue) {
757
+ const headingName = sectionName.charAt(0).toUpperCase() + sectionName.slice(1);
758
+ if (Array.isArray(sectionValue)) {
759
+ yamlSections.push(`# ${headingName}`);
760
+ yamlSections.push(`[YAML section with ${sectionValue.length} file reference(s) - will be processed by asset extraction]`);
761
+ } else if (typeof sectionValue === "string") {
762
+ yamlSections.push(`# ${headingName}`);
763
+ yamlSections.push(sectionValue);
764
+ }
765
+ }
766
+ }
767
+ if (yamlSections.length > 0) {
768
+ const yamlContent = yamlSections.join("\n\n");
769
+ return content ? `${content}
770
+
771
+ ${yamlContent}` : yamlContent;
772
+ }
773
+ return content;
774
+ }
775
+ getName() {
776
+ return "Lexical Analysis";
777
+ }
778
+ };
779
+
780
+ // src/lib/compiler/stages/dependency.ts
781
+ var DependencyResolutionStage = class {
782
+ /**
783
+ * Resolve a package reference via the injected resolver. Absent in the browser
784
+ * (single-file compile), where package/inherits resolution is the backend's job.
785
+ */
786
+ async resolvePackage(context, packageRef) {
787
+ if (!context.packageResolver) {
788
+ throw new Error(
789
+ `Package resolution is unavailable in this environment: ${packageRef}. Compile via the backend to resolve packages and inheritance.`
790
+ );
791
+ }
792
+ return context.packageResolver.resolvePackage(packageRef, {
793
+ fileSystem: context.fileSystem,
794
+ registryUrl: context.registryUrl,
795
+ workspaceRoot: context.workspaceRoot
796
+ });
797
+ }
798
+ async process(context) {
799
+ if (!context.metadata) {
800
+ return;
801
+ }
802
+ const resolvedPackages = {};
803
+ if (context.metadata.using) {
804
+ await this.processUsingField(context, context.metadata.using, resolvedPackages);
805
+ }
806
+ context.dependencies.imports = resolvedPackages;
807
+ if (context.metadata.inherits) {
808
+ await this.processInheritsField(context, context.metadata.inherits, resolvedPackages);
809
+ }
810
+ await this.processContentFieldAliases(context, resolvedPackages);
811
+ }
812
+ /**
813
+ * Process the 'using' field for package imports.
814
+ */
815
+ async processUsingField(context, usingImports, resolvedPackages) {
816
+ if (typeof usingImports === "string") {
817
+ await this.resolvePackageImport(context, usingImports, null, resolvedPackages);
818
+ } else if (Array.isArray(usingImports)) {
819
+ for (const item of usingImports) {
820
+ if (typeof item === "string") {
821
+ await this.resolvePackageImport(context, item, null, resolvedPackages);
822
+ } else if (typeof item === "object" && item !== null) {
823
+ const packageRef = item.name || item.package || "";
824
+ const prefix = item.prefix;
825
+ if (packageRef) {
826
+ if (!prefix) {
827
+ context.addError(
828
+ `Package '${packageRef}' in 'using' field must have a prefix for shorthand access`
829
+ );
830
+ continue;
831
+ }
832
+ await this.resolvePackageImport(context, packageRef, prefix, resolvedPackages);
833
+ }
834
+ }
835
+ }
836
+ } else if (typeof usingImports === "object" && usingImports !== null) {
837
+ for (const [key, value] of Object.entries(usingImports)) {
838
+ if (typeof value === "string") {
839
+ await this.resolvePackageImport(context, key, value, resolvedPackages);
840
+ } else if (typeof value === "object" && value !== null && "prefix" in value) {
841
+ const prefix = value.prefix;
842
+ await this.resolvePackageImport(context, key, prefix, resolvedPackages);
843
+ }
844
+ }
845
+ }
846
+ }
847
+ /**
848
+ * Resolve a single package import.
849
+ */
850
+ async resolvePackageImport(context, packageRef, prefix, resolvedPackages) {
851
+ try {
852
+ const packagePath = await this.resolvePackage(context, packageRef);
853
+ resolvedPackages[packageRef] = {
854
+ path: packagePath,
855
+ prefix: prefix || void 0
856
+ };
857
+ if (context.verbose) {
858
+ if (prefix) {
859
+ console.log(`\u2713 Resolved package: ${packageRef} -> ${packagePath} (prefix: ${prefix})`);
860
+ } else {
861
+ console.log(`\u2713 Resolved package: ${packageRef} -> ${packagePath}`);
862
+ }
863
+ }
864
+ } catch (error) {
865
+ const errorMessage = error instanceof Error ? error.message : String(error);
866
+ const location = context.findLocation(/using:/);
867
+ context.addDiagnostic({
868
+ message: `Failed to resolve package ${packageRef}: ${errorMessage}`,
869
+ severity: "warning",
870
+ source: "dependency",
871
+ code: "PACKAGE_NOT_FOUND",
872
+ ...location
873
+ });
874
+ }
875
+ }
876
+ /**
877
+ * Process the 'inherits' field.
878
+ */
879
+ async processInheritsField(context, inheritsRef, resolvedPackages) {
880
+ const resolvedInheritsRef = this.resolveAliasInPath(inheritsRef, resolvedPackages);
881
+ if (resolvedInheritsRef !== inheritsRef) {
882
+ if (context.metadata) {
883
+ context.metadata.inherits = resolvedInheritsRef;
884
+ }
885
+ if (context.verbose) {
886
+ console.log(`\u2713 Resolved alias in inherits: ${resolvedInheritsRef}`);
887
+ }
888
+ }
889
+ if (resolvedInheritsRef.startsWith("./") || resolvedInheritsRef.startsWith("../") || resolvedInheritsRef.startsWith("/") || !resolvedInheritsRef.startsWith("@") && isPrompdFile(resolvedInheritsRef)) {
890
+ const sourceDir = context.fileSystem.dirname(context.sourceFile);
891
+ const parentPath = context.fileSystem.resolve(sourceDir, resolvedInheritsRef);
892
+ const fileExists = await context.fileSystem.exists(parentPath);
893
+ if (!fileExists) {
894
+ const location = context.findLocation(/inherits:/);
895
+ context.addDiagnostic({
896
+ message: `Inherited file not found: "${resolvedInheritsRef}" (resolved to: ${parentPath})`,
897
+ severity: "error",
898
+ source: "dependency",
899
+ code: "INHERITS_FILE_NOT_FOUND",
900
+ ...location
901
+ });
902
+ return;
903
+ }
904
+ context.dependencies.inherits = parentPath;
905
+ if (context.verbose) {
906
+ console.log(`\u2713 Resolved inheritance file: ${resolvedInheritsRef} -> ${parentPath}`);
907
+ }
908
+ } else {
909
+ try {
910
+ const pattern = /^(@[\w.-]+\/[\w.-]+@[\w.-]+)\/(.+)$/;
911
+ const match = resolvedInheritsRef.match(pattern);
912
+ if (match) {
913
+ const packageRef = match[1];
914
+ const filePathInPackage = match[2];
915
+ if (context.verbose) {
916
+ console.log(`Parsing package inheritance: ${packageRef} / ${filePathInPackage}`);
917
+ }
918
+ const packagePath = await this.resolvePackage(context, packageRef);
919
+ const fullFilePath = resolvePackageFile(packagePath, filePathInPackage);
920
+ const fileExists = await context.fileSystem.exists(fullFilePath);
921
+ if (!fileExists) {
922
+ const location = context.findLocation(/inherits:/);
923
+ context.addDiagnostic({
924
+ message: `Inherited file not found in package: "${filePathInPackage}" (package: ${packageRef})`,
925
+ severity: "error",
926
+ source: "dependency",
927
+ code: "INHERITS_FILE_NOT_FOUND",
928
+ ...location
929
+ });
930
+ return;
931
+ }
932
+ context.dependencies.inherits = fullFilePath;
933
+ if (context.verbose) {
934
+ console.log(`\u2713 Resolved inheritance package: ${resolvedInheritsRef} -> ${fullFilePath}`);
935
+ }
936
+ } else {
937
+ const parentPath = await this.resolvePackage(context, resolvedInheritsRef);
938
+ const dirExists = await context.fileSystem.exists(parentPath);
939
+ if (!dirExists) {
940
+ const location = context.findLocation(/inherits:/);
941
+ context.addDiagnostic({
942
+ message: `Inherited package not found: "${resolvedInheritsRef}"`,
943
+ severity: "error",
944
+ source: "dependency",
945
+ code: "INHERITS_PACKAGE_NOT_FOUND",
946
+ ...location
947
+ });
948
+ return;
949
+ }
950
+ context.dependencies.inherits = parentPath;
951
+ if (context.verbose) {
952
+ console.log(`\u2713 Resolved inheritance package (direct): ${resolvedInheritsRef} -> ${parentPath}`);
953
+ }
954
+ }
955
+ } catch (error) {
956
+ const errorMessage = error instanceof Error ? error.message : String(error);
957
+ const location = context.findLocation(/inherits:/);
958
+ context.addDiagnostic({
959
+ message: `Failed to resolve inheritance "${resolvedInheritsRef}": ${errorMessage}`,
960
+ severity: "error",
961
+ source: "dependency",
962
+ code: "INHERITS_NOT_FOUND",
963
+ ...location
964
+ });
965
+ }
966
+ }
967
+ }
968
+ /**
969
+ * Process content reference fields (system, context, user, assistant, response) for alias resolution.
970
+ */
971
+ async processContentFieldAliases(context, resolvedPackages) {
972
+ if (!context.metadata) {
973
+ return;
974
+ }
975
+ const contentFields = ["system", "assistant", "context", "user", "response"];
976
+ for (const fieldName of contentFields) {
977
+ const fieldValue = context.metadata[fieldName];
978
+ if (fieldValue) {
979
+ if (typeof fieldValue === "string") {
980
+ const resolvedValue = this.resolveAliasInPath(fieldValue, resolvedPackages);
981
+ if (resolvedValue !== fieldValue) {
982
+ context.metadata[fieldName] = resolvedValue;
983
+ if (context.verbose) {
984
+ console.log(`\u2713 Resolved alias in ${fieldName}: ${fieldValue} -> ${resolvedValue}`);
985
+ }
986
+ }
987
+ } else if (Array.isArray(fieldValue)) {
988
+ const resolvedList = [];
989
+ for (const item of fieldValue) {
990
+ if (typeof item === "string") {
991
+ const resolvedItem = this.resolveAliasInPath(item, resolvedPackages);
992
+ resolvedList.push(resolvedItem);
993
+ if (resolvedItem !== item && context.verbose) {
994
+ console.log(`\u2713 Resolved alias in ${fieldName}: ${item} -> ${resolvedItem}`);
995
+ }
996
+ } else {
997
+ resolvedList.push(item);
998
+ }
999
+ }
1000
+ context.metadata[fieldName] = resolvedList;
1001
+ }
1002
+ }
1003
+ }
1004
+ }
1005
+ /**
1006
+ * Resolve alias prefixes in file paths.
1007
+ *
1008
+ * Examples:
1009
+ * - "@pkg/templates/file.prmd" with "@pkg" aliased to "@scope/package@version"
1010
+ * becomes "@scope/package@version/templates/file.prmd"
1011
+ */
1012
+ resolveAliasInPath(pathStr, resolvedPackages) {
1013
+ if (!pathStr.startsWith("@") || !pathStr.includes("/")) {
1014
+ return pathStr;
1015
+ }
1016
+ const firstSlashIndex = pathStr.indexOf("/");
1017
+ const potentialAlias = pathStr.substring(0, firstSlashIndex);
1018
+ const remainingPath = pathStr.substring(firstSlashIndex + 1);
1019
+ for (const [packageName, packageInfo] of Object.entries(resolvedPackages)) {
1020
+ if (packageInfo.prefix === potentialAlias) {
1021
+ const resolvedPath = `${packageName}/${remainingPath}`;
1022
+ return resolvedPath;
1023
+ }
1024
+ }
1025
+ return pathStr;
1026
+ }
1027
+ getName() {
1028
+ return "Dependency Resolution";
1029
+ }
1030
+ };
1031
+
1032
+ // src/lib/compiler/date-utils.ts
1033
+ function parseDateExpr(input) {
1034
+ const v = (input ?? "").trim();
1035
+ if (!v) return null;
1036
+ const lower = v.toLowerCase();
1037
+ if (lower === "now" || lower === "today") return /* @__PURE__ */ new Date();
1038
+ const rel = lower.match(/^(?:now|today)\s*([+-])\s*(\d+)\s*([a-z]+)$/);
1039
+ if (rel) {
1040
+ const n = (rel[1] === "-" ? -1 : 1) * parseInt(rel[2], 10);
1041
+ const u = rel[3];
1042
+ const d = /* @__PURE__ */ new Date();
1043
+ if (u === "d" || u.startsWith("day")) d.setDate(d.getDate() + n);
1044
+ else if (u === "w" || u.startsWith("week")) d.setDate(d.getDate() + n * 7);
1045
+ else if (u === "min" || u.startsWith("minute")) d.setMinutes(d.getMinutes() + n);
1046
+ else if (u === "mo" || u === "m" || u.startsWith("month")) d.setMonth(d.getMonth() + n);
1047
+ else if (u === "y" || u.startsWith("year")) d.setFullYear(d.getFullYear() + n);
1048
+ else if (u === "h" || u.startsWith("hour")) d.setHours(d.getHours() + n);
1049
+ else if (u === "s" || u === "sec" || u.startsWith("second")) d.setSeconds(d.getSeconds() + n);
1050
+ else return null;
1051
+ return d;
1052
+ }
1053
+ const dateOnly = v.match(/^(\d{4})-(\d{2})-(\d{2})$/);
1054
+ if (dateOnly) {
1055
+ return new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]));
1056
+ }
1057
+ const parsed = new Date(v);
1058
+ return isNaN(parsed.getTime()) ? null : parsed;
1059
+ }
1060
+ function pad(n) {
1061
+ return String(n).padStart(2, "0");
1062
+ }
1063
+ function formatDate(d) {
1064
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
1065
+ }
1066
+ function formatDateTime(d) {
1067
+ return `${formatDate(d)}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
1068
+ }
1069
+ function resolveDateValue(value, withTime) {
1070
+ if (typeof value !== "string") return value;
1071
+ const d = parseDateExpr(value);
1072
+ if (!d) return value;
1073
+ return withTime ? formatDateTime(d) : formatDate(d);
1074
+ }
1075
+
1076
+ // src/lib/compiler/stages/semantic.ts
1077
+ var SemanticAnalysisStage = class {
1078
+ async process(context) {
1079
+ if (!context.metadata) {
1080
+ return;
1081
+ }
1082
+ this.validateRequiredMetadata(context);
1083
+ if (context.metadata.parameters) {
1084
+ const isExecutionMode = Object.keys(context.parameters).length > 0;
1085
+ for (const param of context.metadata.parameters) {
1086
+ if (!(param.name in context.parameters) && param.default !== void 0) {
1087
+ context.parameters[param.name] = param.default;
1088
+ if (context.verbose) {
1089
+ console.log(`\u2713 Using default value for '${param.name}': ${param.default}`);
1090
+ }
1091
+ }
1092
+ if (isExecutionMode && param.required && !(param.name in context.parameters)) {
1093
+ context.addWarning(`Required parameter '${param.name}' not provided`);
1094
+ }
1095
+ if (param.name in context.parameters) {
1096
+ context.parameters[param.name] = this.coerceParameterValue(
1097
+ context.parameters[param.name],
1098
+ param.type
1099
+ );
1100
+ }
1101
+ if (param.name in context.parameters) {
1102
+ const value = context.parameters[param.name];
1103
+ const isValid = this.validateParameterType(value, param.type);
1104
+ if (!isValid) {
1105
+ context.addWarning(
1106
+ `Parameter '${param.name}' has incorrect type. Expected: ${param.type}, got: ${typeof value}`
1107
+ );
1108
+ }
1109
+ }
1110
+ if (param.pattern && param.name in context.parameters) {
1111
+ const value = String(context.parameters[param.name]);
1112
+ const regex = new RegExp(param.pattern);
1113
+ if (!regex.test(value)) {
1114
+ context.addWarning(
1115
+ `Parameter '${param.name}' does not match pattern: ${param.pattern}`
1116
+ );
1117
+ }
1118
+ }
1119
+ if (param.type === "number" && param.name in context.parameters) {
1120
+ const value = Number(context.parameters[param.name]);
1121
+ if (param.minimum !== void 0 && value < param.minimum) {
1122
+ context.addWarning(
1123
+ `Parameter '${param.name}' is below minimum value: ${param.minimum}`
1124
+ );
1125
+ }
1126
+ if (param.maximum !== void 0 && value > param.maximum) {
1127
+ context.addWarning(
1128
+ `Parameter '${param.name}' exceeds maximum value: ${param.maximum}`
1129
+ );
1130
+ }
1131
+ }
1132
+ }
1133
+ }
1134
+ if (context.verbose && context.parameters) {
1135
+ const paramCount = Object.keys(context.parameters).length;
1136
+ console.log(`\u2713 Validated ${paramCount} parameter(s)`);
1137
+ }
1138
+ }
1139
+ /**
1140
+ * Validate parameter type.
1141
+ */
1142
+ validateParameterType(value, expectedType) {
1143
+ switch (expectedType) {
1144
+ case "string":
1145
+ case "file":
1146
+ // file path provided as a string
1147
+ case "base64":
1148
+ return typeof value === "string";
1149
+ case "number":
1150
+ case "float":
1151
+ return typeof value === "number" && !isNaN(value);
1152
+ case "integer":
1153
+ return typeof value === "number" && !isNaN(value) && Number.isInteger(value);
1154
+ case "boolean":
1155
+ return typeof value === "boolean";
1156
+ case "array":
1157
+ return Array.isArray(value);
1158
+ case "object":
1159
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1160
+ case "json":
1161
+ if (value === null || value === void 0) return false;
1162
+ if (typeof value !== "string") return true;
1163
+ try {
1164
+ JSON.parse(value);
1165
+ return true;
1166
+ } catch {
1167
+ return false;
1168
+ }
1169
+ case "date":
1170
+ case "datetime":
1171
+ return typeof value === "string" && parseDateExpr(value) !== null;
1172
+ default:
1173
+ return true;
1174
+ }
1175
+ }
1176
+ /**
1177
+ * Coerce a parameter value to its declared type.
1178
+ * CLI args always arrive as strings; this converts them to the correct runtime type
1179
+ * before the value is handed to the template engine.
1180
+ * Already-correct types (e.g. from a JSON params file) are passed through unchanged.
1181
+ */
1182
+ coerceParameterValue(value, type) {
1183
+ if (value === null || value === void 0) return value;
1184
+ switch (type) {
1185
+ case "boolean":
1186
+ if (typeof value === "boolean") return value;
1187
+ if (typeof value === "string") {
1188
+ if (value.toLowerCase() === "true") return true;
1189
+ if (value.toLowerCase() === "false") return false;
1190
+ }
1191
+ return value;
1192
+ case "integer":
1193
+ if (typeof value === "number") return value;
1194
+ if (typeof value === "string") {
1195
+ const n = Number(value);
1196
+ if (!isNaN(n)) return n;
1197
+ }
1198
+ return value;
1199
+ case "number":
1200
+ case "float":
1201
+ if (typeof value === "number") return value;
1202
+ if (typeof value === "string") {
1203
+ const n = parseFloat(value);
1204
+ if (!isNaN(n)) return n;
1205
+ }
1206
+ return value;
1207
+ case "json":
1208
+ case "array":
1209
+ case "object":
1210
+ if (typeof value !== "string") return value;
1211
+ try {
1212
+ return JSON.parse(value);
1213
+ } catch {
1214
+ return value;
1215
+ }
1216
+ case "date":
1217
+ return resolveDateValue(value, false);
1218
+ case "datetime":
1219
+ return resolveDateValue(value, true);
1220
+ default:
1221
+ return value;
1222
+ }
1223
+ }
1224
+ /**
1225
+ * Validate required metadata fields (id, name, version).
1226
+ */
1227
+ validateRequiredMetadata(context) {
1228
+ const metadata = context.metadata;
1229
+ if (!metadata) return;
1230
+ if (!metadata.id) {
1231
+ const location = context.findLocation(/^---/m);
1232
+ context.addDiagnostic({
1233
+ message: "Required field 'id' is missing. Add a unique identifier for this prompt.",
1234
+ severity: "error",
1235
+ source: "semantic",
1236
+ code: "MISSING_ID",
1237
+ line: location?.line ? location.line + 1 : 2,
1238
+ column: 1
1239
+ });
1240
+ } else if (typeof metadata.id === "string") {
1241
+ if (!/^[a-z0-9-]+$/.test(metadata.id)) {
1242
+ const location = context.findLocation(/^\s*id:/m);
1243
+ context.addDiagnostic({
1244
+ message: `ID '${metadata.id}' should use kebab-case format (lowercase letters, numbers, hyphens only).`,
1245
+ severity: "warning",
1246
+ source: "semantic",
1247
+ code: "INVALID_ID_FORMAT",
1248
+ ...location
1249
+ });
1250
+ }
1251
+ }
1252
+ if (!metadata.name) {
1253
+ const location = context.findLocation(/^---/m);
1254
+ context.addDiagnostic({
1255
+ message: "Required field 'name' is missing. Add a display name for this prompt.",
1256
+ severity: "error",
1257
+ source: "semantic",
1258
+ code: "MISSING_NAME",
1259
+ line: location?.line ? location.line + 1 : 2,
1260
+ column: 1
1261
+ });
1262
+ }
1263
+ if (!metadata.version) {
1264
+ const location = context.findLocation(/^---/m);
1265
+ context.addDiagnostic({
1266
+ message: "Required field 'version' is missing. Add a semantic version (e.g., 1.0.0).",
1267
+ severity: "error",
1268
+ source: "semantic",
1269
+ code: "MISSING_VERSION",
1270
+ line: location?.line ? location.line + 1 : 2,
1271
+ column: 1
1272
+ });
1273
+ } else if (typeof metadata.version === "string") {
1274
+ if (!/^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/.test(metadata.version)) {
1275
+ const location = context.findLocation(/^\s*version:/m);
1276
+ context.addDiagnostic({
1277
+ message: `Invalid semantic version '${metadata.version}'. Use format: MAJOR.MINOR.PATCH (e.g., 1.0.0)`,
1278
+ severity: "error",
1279
+ source: "semantic",
1280
+ code: "INVALID_VERSION",
1281
+ ...location
1282
+ });
1283
+ }
1284
+ }
1285
+ }
1286
+ getName() {
1287
+ return "Semantic Analysis";
1288
+ }
1289
+ };
1290
+
1291
+ // src/lib/compiler/section-override.ts
1292
+ var _SectionOverrideProcessor = class _SectionOverrideProcessor {
1293
+ constructor() {
1294
+ this.headingPattern = /^(#{1,6})\s+(.+)$/gm;
1295
+ this.sectionIdPattern = /<!--\s*section-id:\s*([a-z0-9-]+)\s*-->/i;
1296
+ }
1297
+ /**
1298
+ * Pattern to detect any section-id comment (for validation)
1299
+ */
1300
+ detectSectionIdComment(line) {
1301
+ const match = /<!--\s*section-id:\s*(.+?)\s*-->/i.exec(line);
1302
+ return match ? match[1].trim() : null;
1303
+ }
1304
+ /**
1305
+ * Extract all sections from markdown content.
1306
+ */
1307
+ extractSections(content) {
1308
+ if (!content || !content.trim()) {
1309
+ return /* @__PURE__ */ new Map();
1310
+ }
1311
+ const sections = /* @__PURE__ */ new Map();
1312
+ const lines = content.split("\n");
1313
+ const encounteredSectionIds = /* @__PURE__ */ new Set();
1314
+ const explicitSectionIds = /* @__PURE__ */ new Map();
1315
+ for (let i = 0; i < lines.length; i++) {
1316
+ const detectedId = this.detectSectionIdComment(lines[i]);
1317
+ if (detectedId) {
1318
+ this.validateSectionId(detectedId, i + 1);
1319
+ const match2 = this.sectionIdPattern.exec(lines[i]);
1320
+ if (match2) {
1321
+ const sectionId = match2[1];
1322
+ explicitSectionIds.set(i, sectionId);
1323
+ }
1324
+ }
1325
+ }
1326
+ const headingMatches = [];
1327
+ let match;
1328
+ this.headingPattern.lastIndex = 0;
1329
+ while ((match = this.headingPattern.exec(content)) !== null) {
1330
+ const headingLevel = match[1].length;
1331
+ const headingText = match[2].trim();
1332
+ const headingLine = content.substring(0, match.index).split("\n").length - 1;
1333
+ headingMatches.push({
1334
+ level: headingLevel,
1335
+ text: headingText,
1336
+ line: headingLine,
1337
+ index: match.index
1338
+ });
1339
+ }
1340
+ for (let i = 0; i < headingMatches.length; i++) {
1341
+ const heading = headingMatches[i];
1342
+ let sectionId = null;
1343
+ for (let lineNum = heading.line; lineNum >= Math.max(0, heading.line - 5); lineNum--) {
1344
+ if (explicitSectionIds.has(lineNum)) {
1345
+ sectionId = explicitSectionIds.get(lineNum);
1346
+ break;
1347
+ }
1348
+ }
1349
+ if (!sectionId) {
1350
+ sectionId = this.generateSectionId(heading.text);
1351
+ }
1352
+ this.validateSectionId(sectionId, heading.line + 1);
1353
+ if (encounteredSectionIds.has(sectionId)) {
1354
+ throw new ParseError(
1355
+ `Duplicate section ID '${sectionId}' at line ${heading.line + 1}. Section IDs must be unique.`
1356
+ );
1357
+ }
1358
+ encounteredSectionIds.add(sectionId);
1359
+ const sectionStart = heading.index;
1360
+ const nextHeading = headingMatches[i + 1];
1361
+ const sectionEnd = nextHeading ? nextHeading.index : content.length;
1362
+ const sectionContent = content.substring(sectionStart, sectionEnd).trim();
1363
+ sections.set(sectionId, {
1364
+ id: sectionId,
1365
+ headingText: heading.text,
1366
+ content: sectionContent,
1367
+ startLine: heading.line,
1368
+ endLine: content.substring(0, sectionEnd).split("\n").length - 1,
1369
+ headingLevel: heading.level
1370
+ });
1371
+ }
1372
+ return sections;
1373
+ }
1374
+ /**
1375
+ * Apply overrides and merge parent/child sections.
1376
+ */
1377
+ async applyOverrides(parentSections, childSections, overrides, baseDir, verbose = false, fileSystem) {
1378
+ const mergedSections = new Map(parentSections);
1379
+ for (const [sectionId, overridePath] of Object.entries(overrides)) {
1380
+ if (!mergedSections.has(sectionId)) {
1381
+ if (verbose) {
1382
+ const available = Array.from(mergedSections.keys()).sort().join(", ");
1383
+ console.log(
1384
+ `Warning: Override section '${sectionId}' not found in parent. Available: ${available}`
1385
+ );
1386
+ }
1387
+ continue;
1388
+ }
1389
+ if (overridePath === null) {
1390
+ mergedSections.delete(sectionId);
1391
+ if (verbose) {
1392
+ console.log(` - Removing section '${sectionId}'`);
1393
+ }
1394
+ } else {
1395
+ try {
1396
+ const overrideContent = await this.loadOverrideContent(overridePath, baseDir, fileSystem);
1397
+ const originalSection = mergedSections.get(sectionId);
1398
+ mergedSections.set(sectionId, {
1399
+ ...originalSection,
1400
+ content: overrideContent
1401
+ });
1402
+ if (verbose) {
1403
+ console.log(` - Replacing section '${sectionId}' with content from ${overridePath}`);
1404
+ }
1405
+ } catch (error) {
1406
+ throw new Error(
1407
+ `Failed to apply override for section '${sectionId}': ${error instanceof Error ? error.message : String(error)}`
1408
+ );
1409
+ }
1410
+ }
1411
+ }
1412
+ for (const [sectionId, childSection] of childSections.entries()) {
1413
+ mergedSections.set(sectionId, childSection);
1414
+ if (verbose && parentSections.has(sectionId)) {
1415
+ console.log(` - Child overrides parent section '${sectionId}'`);
1416
+ } else if (verbose) {
1417
+ console.log(` - Adding child section '${sectionId}'`);
1418
+ }
1419
+ }
1420
+ const contentParts = [];
1421
+ for (const section of mergedSections.values()) {
1422
+ contentParts.push(section.content);
1423
+ }
1424
+ return contentParts.join("\n\n");
1425
+ }
1426
+ /**
1427
+ * Load override content from a file.
1428
+ */
1429
+ async loadOverrideContent(overridePath, baseDir, fileSystem) {
1430
+ if (!fileSystem) {
1431
+ throw new Error("A fileSystem is required to load section override content.");
1432
+ }
1433
+ const resolvedPath = this.resolveOverridePath(overridePath, baseDir, fileSystem);
1434
+ if (!await fileSystem.exists(resolvedPath)) {
1435
+ throw new Error(`Override file not found: ${resolvedPath}`);
1436
+ }
1437
+ try {
1438
+ const content = await fileSystem.readFile(resolvedPath);
1439
+ if (content.length > _SectionOverrideProcessor.MAX_OVERRIDE_SIZE) {
1440
+ throw new SecurityError(
1441
+ `Override file too large: ${content.length} bytes (max: ${_SectionOverrideProcessor.MAX_OVERRIDE_SIZE})`
1442
+ );
1443
+ }
1444
+ return content.trim();
1445
+ } catch (error) {
1446
+ if (error instanceof SecurityError) throw error;
1447
+ throw new Error(
1448
+ `Failed to read override file: ${error instanceof Error ? error.message : String(error)}`
1449
+ );
1450
+ }
1451
+ }
1452
+ /**
1453
+ * Resolve override path with security checks.
1454
+ */
1455
+ resolveOverridePath(overridePath, baseDir, fileSystem) {
1456
+ const cleaned = overridePath.replace(/\\/g, "/");
1457
+ if (cleaned.includes("..") && !cleaned.startsWith("./") && !cleaned.startsWith("../")) {
1458
+ throw new SecurityError(`Path traversal detected in override path: ${overridePath}`);
1459
+ }
1460
+ if (isAbsolutePosix(cleaned) || /^[A-Za-z]:/.test(overridePath)) {
1461
+ return overridePath;
1462
+ }
1463
+ const resolved = fileSystem.resolve(baseDir, overridePath);
1464
+ const baseResolved = fileSystem.resolve(baseDir);
1465
+ const resolvedNorm = resolved.replace(/\\/g, "/");
1466
+ const baseNorm = baseResolved.replace(/\\/g, "/");
1467
+ if (!resolvedNorm.startsWith(baseNorm)) {
1468
+ throw new SecurityError(`Override path escapes base directory: ${overridePath}`);
1469
+ }
1470
+ return resolved;
1471
+ }
1472
+ /**
1473
+ * Generate a section ID from heading text (kebab-case).
1474
+ */
1475
+ generateSectionId(headingText) {
1476
+ return headingText.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1477
+ }
1478
+ /**
1479
+ * Validate section ID format.
1480
+ */
1481
+ validateSectionId(sectionId, lineNumber) {
1482
+ const dangerousPatterns = ["__proto__", "constructor", "prototype"];
1483
+ if (dangerousPatterns.some((pattern) => sectionId.includes(pattern))) {
1484
+ throw new SecurityError(`Section ID contains forbidden pattern: ${sectionId}`);
1485
+ }
1486
+ if (sectionId.length > 100) {
1487
+ throw new ValidationError(`Section ID too long: ${sectionId.length} characters (max: 100)`);
1488
+ }
1489
+ const validPattern = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
1490
+ if (!validPattern.test(sectionId)) {
1491
+ const location = lineNumber ? ` at line ${lineNumber}` : "";
1492
+ throw new ValidationError(
1493
+ `Invalid section ID '${sectionId}'${location}. Section IDs must be kebab-case (lowercase letters, numbers, hyphens only).`
1494
+ );
1495
+ }
1496
+ }
1497
+ };
1498
+ /** Max override file size (DoS protection). */
1499
+ _SectionOverrideProcessor.MAX_OVERRIDE_SIZE = 10 * 1024 * 1024;
1500
+ var SectionOverrideProcessor = _SectionOverrideProcessor;
1501
+
1502
+ // src/lib/compiler/language-map.ts
1503
+ var EXTENSION_TO_LANGUAGE = {
1504
+ // TypeScript/JavaScript
1505
+ ".ts": "typescript",
1506
+ ".tsx": "typescript",
1507
+ ".js": "javascript",
1508
+ ".jsx": "javascript",
1509
+ ".mjs": "javascript",
1510
+ ".cjs": "javascript",
1511
+ // Python
1512
+ ".py": "python",
1513
+ ".pyw": "python",
1514
+ // Go
1515
+ ".go": "go",
1516
+ // Rust
1517
+ ".rs": "rust",
1518
+ // Java/Kotlin
1519
+ ".java": "java",
1520
+ ".kt": "kotlin",
1521
+ ".kts": "kotlin",
1522
+ // C/C++
1523
+ ".c": "c",
1524
+ ".h": "c",
1525
+ ".cpp": "cpp",
1526
+ ".hpp": "cpp",
1527
+ ".cc": "cpp",
1528
+ // C#
1529
+ ".cs": "csharp",
1530
+ // Ruby
1531
+ ".rb": "ruby",
1532
+ // PHP
1533
+ ".php": "php",
1534
+ // Swift
1535
+ ".swift": "swift",
1536
+ // Shell
1537
+ ".sh": "bash",
1538
+ ".bash": "bash",
1539
+ ".zsh": "zsh",
1540
+ ".fish": "fish",
1541
+ // SQL
1542
+ ".sql": "sql",
1543
+ // Web
1544
+ ".html": "html",
1545
+ ".htm": "html",
1546
+ ".css": "css",
1547
+ ".scss": "scss",
1548
+ ".sass": "sass",
1549
+ ".less": "less",
1550
+ // Data formats
1551
+ ".xml": "xml",
1552
+ ".svg": "xml",
1553
+ ".json": "json",
1554
+ ".yaml": "yaml",
1555
+ ".yml": "yaml",
1556
+ ".toml": "toml",
1557
+ ".csv": "csv",
1558
+ // Markdown
1559
+ ".md": "markdown",
1560
+ ".mdx": "markdown"
1561
+ };
1562
+ var EXTENSION_TO_LANGUAGE_ALIASES = {
1563
+ // TypeScript/JavaScript
1564
+ ".ts": ["typescript", "ts"],
1565
+ ".tsx": ["typescript", "tsx", "ts"],
1566
+ ".js": ["javascript", "js"],
1567
+ ".jsx": ["javascript", "jsx", "js"],
1568
+ ".mjs": ["javascript", "js"],
1569
+ ".cjs": ["javascript", "js"],
1570
+ // Python
1571
+ ".py": ["python", "py"],
1572
+ ".pyw": ["python", "py"],
1573
+ // Go
1574
+ ".go": ["go", "golang"],
1575
+ // Rust
1576
+ ".rs": ["rust", "rs"],
1577
+ // Java/Kotlin
1578
+ ".java": ["java"],
1579
+ ".kt": ["kotlin", "kt"],
1580
+ ".kts": ["kotlin", "kt"],
1581
+ // C/C++
1582
+ ".c": ["c"],
1583
+ ".h": ["c", "h"],
1584
+ ".cpp": ["cpp", "c++", "cxx"],
1585
+ ".hpp": ["cpp", "c++", "hpp"],
1586
+ ".cc": ["cpp", "c++"],
1587
+ // C#
1588
+ ".cs": ["csharp", "cs", "c#"],
1589
+ // Ruby
1590
+ ".rb": ["ruby", "rb"],
1591
+ // PHP
1592
+ ".php": ["php"],
1593
+ // Swift
1594
+ ".swift": ["swift"],
1595
+ // Shell
1596
+ ".sh": ["bash", "sh", "shell"],
1597
+ ".bash": ["bash", "sh", "shell"],
1598
+ ".zsh": ["zsh", "sh", "shell"],
1599
+ ".fish": ["fish"],
1600
+ // SQL
1601
+ ".sql": ["sql"],
1602
+ // Web
1603
+ ".html": ["html", "htm"],
1604
+ ".htm": ["html", "htm"],
1605
+ ".css": ["css"],
1606
+ ".scss": ["scss", "sass"],
1607
+ ".sass": ["sass", "scss"],
1608
+ ".less": ["less"],
1609
+ // Data formats
1610
+ ".json": ["json"],
1611
+ ".yaml": ["yaml", "yml"],
1612
+ ".yml": ["yaml", "yml"],
1613
+ ".xml": ["xml"],
1614
+ ".svg": ["xml", "svg"],
1615
+ ".toml": ["toml"],
1616
+ ".csv": ["csv"],
1617
+ // Markdown
1618
+ ".md": ["markdown", "md"],
1619
+ ".mdx": ["mdx", "markdown", "md"]
1620
+ };
1621
+ function getLanguageForExtension(ext) {
1622
+ return EXTENSION_TO_LANGUAGE[ext.toLowerCase()];
1623
+ }
1624
+ function getLanguageAliasesForExtension(ext) {
1625
+ return EXTENSION_TO_LANGUAGE_ALIASES[ext.toLowerCase()] || [];
1626
+ }
1627
+ var globalIncludeStacks = /* @__PURE__ */ new Map();
1628
+ var compilationCounter = 0;
1629
+ function generateCompilationId() {
1630
+ return `compile-${++compilationCounter}-${Date.now()}`;
1631
+ }
1632
+ var PrompdLoader = class _PrompdLoader extends nunjucks.Loader {
1633
+ constructor(options) {
1634
+ super();
1635
+ // Required by Nunjucks loader interface - we use synchronous loading
1636
+ this.async = false;
1637
+ this.fileSystem = options.fileSystem;
1638
+ this.baseDir = options.baseDir;
1639
+ this.verbose = options.verbose || false;
1640
+ this.maxDepth = options.maxDepth || 10;
1641
+ this.parser = new PrompdParser();
1642
+ this.compilationId = options.compilationId || generateCompilationId();
1643
+ if (!globalIncludeStacks.has(this.compilationId)) {
1644
+ globalIncludeStacks.set(this.compilationId, /* @__PURE__ */ new Set());
1645
+ }
1646
+ }
1647
+ /**
1648
+ * Get the include stack for this compilation.
1649
+ */
1650
+ getIncludeStack() {
1651
+ return globalIncludeStacks.get(this.compilationId) || /* @__PURE__ */ new Set();
1652
+ }
1653
+ /**
1654
+ * Clean up the global stack after compilation is complete.
1655
+ * Should be called when the top-level compilation finishes.
1656
+ */
1657
+ static cleanupCompilation(compilationId) {
1658
+ globalIncludeStacks.delete(compilationId);
1659
+ }
1660
+ /**
1661
+ * Get the source content for a template.
1662
+ * This is the main entry point called by Nunjucks.
1663
+ * Throws an error if the file is not found (required by Nunjucks interface).
1664
+ */
1665
+ getSource(name) {
1666
+ const resolvedPath = this.resolvePath(name);
1667
+ const includeStack = this.getIncludeStack();
1668
+ if (this.verbose) {
1669
+ console.log(`[include] Resolving: ${name} -> ${resolvedPath}`);
1670
+ }
1671
+ if (includeStack.has(resolvedPath)) {
1672
+ const stack = Array.from(includeStack).join(" -> ");
1673
+ throw new Error(
1674
+ `Circular include detected: ${stack} -> ${resolvedPath}`
1675
+ );
1676
+ }
1677
+ if (includeStack.size >= this.maxDepth) {
1678
+ throw new Error(
1679
+ `Maximum include depth (${this.maxDepth}) exceeded. Check for deep nesting or circular includes.`
1680
+ );
1681
+ }
1682
+ const exists = this.fileSystem.exists(resolvedPath);
1683
+ if (typeof exists === "object" && "then" in exists) {
1684
+ throw new Error(
1685
+ "PrompdLoader requires synchronous file system operations. Use NodeFileSystem or ensure MemoryFileSystem is pre-populated."
1686
+ );
1687
+ }
1688
+ if (!exists) {
1689
+ if (!isPrompdFile(resolvedPath)) {
1690
+ for (const ext of PROMPD_EXTENSIONS) {
1691
+ const withExt = resolvedPath + ext;
1692
+ if (this.fileSystem.exists(withExt) === true) {
1693
+ return this.loadFile(withExt);
1694
+ }
1695
+ }
1696
+ }
1697
+ throw new Error(`Template not found: ${name} (resolved to ${resolvedPath})`);
1698
+ }
1699
+ return this.loadFile(resolvedPath);
1700
+ }
1701
+ /**
1702
+ * Load and process a file.
1703
+ * Note: We do NOT remove from stack after loading because Nunjucks will process
1704
+ * the returned content which may contain more includes. The stack tracks the
1705
+ * entire include chain during a single compilation.
1706
+ */
1707
+ loadFile(filePath) {
1708
+ const includeStack = this.getIncludeStack();
1709
+ includeStack.add(filePath);
1710
+ const content = this.fileSystem.readFile(filePath);
1711
+ if (typeof content === "object" && "then" in content) {
1712
+ throw new Error(
1713
+ "PrompdLoader requires synchronous file system operations."
1714
+ );
1715
+ }
1716
+ const ext = extname(filePath).toLowerCase();
1717
+ let processedContent;
1718
+ if (isPrompdFile(filePath)) {
1719
+ processedContent = this.processPrompdFile(content, filePath);
1720
+ } else {
1721
+ processedContent = content;
1722
+ }
1723
+ if (this.verbose) {
1724
+ const preview = processedContent.substring(0, 50).replace(/\n/g, "\\n");
1725
+ console.log(`[include] Loaded ${ext} file: ${filePath} (${preview}...)`);
1726
+ }
1727
+ return {
1728
+ src: processedContent,
1729
+ path: filePath,
1730
+ noCache: true
1731
+ // Don't cache - files may change
1732
+ };
1733
+ }
1734
+ /**
1735
+ * Process a .prmd file: parse frontmatter and return only the body.
1736
+ */
1737
+ processPrompdFile(content, filePath) {
1738
+ try {
1739
+ const parsed = this.parser.parseContent(content);
1740
+ if (this.verbose) {
1741
+ const paramCount = parsed.metadata?.parameters?.length || 0;
1742
+ console.log(`[include] Parsed .prmd: ${filePath} (${paramCount} params)`);
1743
+ }
1744
+ return parsed.content || "";
1745
+ } catch (error) {
1746
+ const errorMessage = error instanceof Error ? error.message : String(error);
1747
+ if (this.verbose) {
1748
+ console.log(`[include] Warning: Failed to parse ${filePath}: ${errorMessage}`);
1749
+ }
1750
+ const withoutFrontmatter = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
1751
+ return withoutFrontmatter;
1752
+ }
1753
+ }
1754
+ /**
1755
+ * Resolve a path relative to the base directory.
1756
+ */
1757
+ resolvePath(name) {
1758
+ if (isAbsolutePosix(name)) {
1759
+ return name;
1760
+ }
1761
+ return this.fileSystem.join(this.baseDir, name);
1762
+ }
1763
+ /**
1764
+ * Create a child loader for nested includes.
1765
+ * The child loader shares the compilation ID to use the same include stack.
1766
+ */
1767
+ createChildLoader(newBaseDir) {
1768
+ return new _PrompdLoader({
1769
+ fileSystem: this.fileSystem,
1770
+ baseDir: newBaseDir,
1771
+ verbose: this.verbose,
1772
+ maxDepth: this.maxDepth,
1773
+ compilationId: this.compilationId
1774
+ // Share the compilation ID for cycle detection
1775
+ });
1776
+ }
1777
+ /**
1778
+ * Get the compilation ID for this loader.
1779
+ * Useful for cleanup after compilation.
1780
+ */
1781
+ getCompilationId() {
1782
+ return this.compilationId;
1783
+ }
1784
+ };
1785
+ function createPrompdEnvironment(options) {
1786
+ const loader = new PrompdLoader(options);
1787
+ const env = new nunjucks.Environment(loader, {
1788
+ autoescape: false,
1789
+ throwOnUndefined: false,
1790
+ trimBlocks: true,
1791
+ lstripBlocks: true,
1792
+ tags: {
1793
+ blockStart: "{%",
1794
+ blockEnd: "%}",
1795
+ variableStart: "{{",
1796
+ variableEnd: "}}",
1797
+ commentStart: "{#",
1798
+ commentEnd: "#}"
1799
+ }
1800
+ });
1801
+ return env;
1802
+ }
1803
+
1804
+ // src/lib/compiler/stages/template.ts
1805
+ var TemplateProcessingStage = class {
1806
+ constructor() {
1807
+ this.sectionProcessor = new SectionOverrideProcessor();
1808
+ this.nunjucksEnv = new nunjucks.Environment(null, {
1809
+ autoescape: false,
1810
+ // Don't escape HTML - we're doing markdown
1811
+ throwOnUndefined: false,
1812
+ // Gracefully handle missing variables
1813
+ trimBlocks: true,
1814
+ lstripBlocks: true,
1815
+ tags: {
1816
+ blockStart: "{%",
1817
+ blockEnd: "%}",
1818
+ variableStart: "{{",
1819
+ variableEnd: "}}",
1820
+ commentStart: "{#",
1821
+ commentEnd: "#}"
1822
+ }
1823
+ });
1824
+ this.registerFilters();
1825
+ }
1826
+ /**
1827
+ * Register custom Jinja2/Nunjucks filters for data transformation.
1828
+ */
1829
+ registerFilters() {
1830
+ this.nunjucksEnv.addFilter("fromcsv", (csvString) => {
1831
+ if (!csvString || typeof csvString !== "string") {
1832
+ return [];
1833
+ }
1834
+ return this.parseCsv(csvString);
1835
+ });
1836
+ this.nunjucksEnv.addFilter("fromjson", (value) => {
1837
+ if (value === null || value === void 0) return null;
1838
+ if (typeof value !== "string") return value;
1839
+ try {
1840
+ return JSON.parse(value);
1841
+ } catch {
1842
+ return null;
1843
+ }
1844
+ });
1845
+ this.nunjucksEnv.addFilter("tojson", (obj, indent) => {
1846
+ try {
1847
+ return JSON.stringify(obj, null, indent);
1848
+ } catch {
1849
+ return "{}";
1850
+ }
1851
+ });
1852
+ this.nunjucksEnv.addFilter("lines", (str) => {
1853
+ if (!str || typeof str !== "string") {
1854
+ return [];
1855
+ }
1856
+ return str.split(/\r?\n/);
1857
+ });
1858
+ this.nunjucksEnv.addFilter("dedent", (str) => {
1859
+ if (!str || typeof str !== "string") {
1860
+ return "";
1861
+ }
1862
+ const lines = str.split(/\r?\n/);
1863
+ let minIndent = Infinity;
1864
+ for (const line of lines) {
1865
+ if (line.trim().length === 0) continue;
1866
+ const match = line.match(/^(\s*)/);
1867
+ if (match && match[1].length < minIndent) {
1868
+ minIndent = match[1].length;
1869
+ }
1870
+ }
1871
+ if (minIndent === Infinity) minIndent = 0;
1872
+ return lines.map((line) => line.slice(minIndent)).join("\n");
1873
+ });
1874
+ this.nunjucksEnv.addFilter("truncate", (str, length = 80, suffix = "...") => {
1875
+ if (!str || typeof str !== "string") {
1876
+ return "";
1877
+ }
1878
+ if (str.length <= length) {
1879
+ return str;
1880
+ }
1881
+ return str.slice(0, length - suffix.length) + suffix;
1882
+ });
1883
+ this.nunjucksEnv.addFilter("codeblock", (str, language = "") => {
1884
+ if (!str || typeof str !== "string") {
1885
+ return "";
1886
+ }
1887
+ return "```" + language + "\n" + str + "\n```";
1888
+ });
1889
+ this.nunjucksEnv.addFilter("unique", (arr) => {
1890
+ if (!Array.isArray(arr)) {
1891
+ return [];
1892
+ }
1893
+ return [...new Set(arr)];
1894
+ });
1895
+ this.nunjucksEnv.addFilter("pluck", (arr, field) => {
1896
+ if (!Array.isArray(arr)) {
1897
+ return [];
1898
+ }
1899
+ return arr.map((item) => item?.[field]).filter((v) => v !== void 0);
1900
+ });
1901
+ this.nunjucksEnv.addFilter("where", (arr, field, value) => {
1902
+ if (!Array.isArray(arr)) {
1903
+ return [];
1904
+ }
1905
+ return arr.filter((item) => item?.[field] === value);
1906
+ });
1907
+ this.nunjucksEnv.addFilter("groupby", (arr, field) => {
1908
+ if (!Array.isArray(arr)) {
1909
+ return {};
1910
+ }
1911
+ const result = {};
1912
+ for (const item of arr) {
1913
+ const key = String(item?.[field] ?? "undefined");
1914
+ if (!result[key]) {
1915
+ result[key] = [];
1916
+ }
1917
+ result[key].push(item);
1918
+ }
1919
+ return result;
1920
+ });
1921
+ this.nunjucksEnv.addFilter("shuffle", (arr) => {
1922
+ if (!Array.isArray(arr)) {
1923
+ return [];
1924
+ }
1925
+ const shuffled = [...arr];
1926
+ for (let i = shuffled.length - 1; i > 0; i--) {
1927
+ const j = Math.floor(Math.random() * (i + 1));
1928
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
1929
+ }
1930
+ return shuffled;
1931
+ });
1932
+ this.nunjucksEnv.addFilter("sample", (arr, count = 1) => {
1933
+ if (!Array.isArray(arr)) {
1934
+ return [];
1935
+ }
1936
+ const shuffled = [...arr];
1937
+ for (let i = shuffled.length - 1; i > 0; i--) {
1938
+ const j = Math.floor(Math.random() * (i + 1));
1939
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
1940
+ }
1941
+ return shuffled.slice(0, Math.min(count, shuffled.length));
1942
+ });
1943
+ this.nunjucksEnv.addFilter("wordwrap", (str, width = 80) => {
1944
+ if (!str || typeof str !== "string") {
1945
+ return "";
1946
+ }
1947
+ const words = str.split(/\s+/);
1948
+ const lines = [];
1949
+ let currentLine = "";
1950
+ for (const word of words) {
1951
+ if (currentLine.length + word.length + 1 <= width) {
1952
+ currentLine += (currentLine ? " " : "") + word;
1953
+ } else {
1954
+ if (currentLine) lines.push(currentLine);
1955
+ currentLine = word;
1956
+ }
1957
+ }
1958
+ if (currentLine) lines.push(currentLine);
1959
+ return lines.join("\n");
1960
+ });
1961
+ this.nunjucksEnv.addFilter("bulletlist", (input) => {
1962
+ const items = Array.isArray(input) ? input : String(input).split(/\r?\n/);
1963
+ return items.filter((item) => String(item).trim()).map((item) => `- ${item}`).join("\n");
1964
+ });
1965
+ this.nunjucksEnv.addFilter("numberedlist", (input) => {
1966
+ const items = Array.isArray(input) ? input : String(input).split(/\r?\n/);
1967
+ return items.filter((item) => String(item).trim()).map((item, i) => `${i + 1}. ${item}`).join("\n");
1968
+ });
1969
+ }
1970
+ /**
1971
+ * Parse CSV string into array of record objects.
1972
+ */
1973
+ parseCsv(csvString) {
1974
+ const lines = csvString.trim().split(/\r?\n/);
1975
+ if (lines.length === 0) {
1976
+ return [];
1977
+ }
1978
+ const headers = this.parseCsvLine(lines[0]);
1979
+ const records = [];
1980
+ for (let i = 1; i < lines.length; i++) {
1981
+ const line = lines[i].trim();
1982
+ if (!line) continue;
1983
+ const values = this.parseCsvLine(line);
1984
+ const record = {};
1985
+ for (let j = 0; j < headers.length; j++) {
1986
+ const header = headers[j].trim();
1987
+ const value = values[j]?.trim() ?? "";
1988
+ record[header] = value;
1989
+ }
1990
+ records.push(record);
1991
+ }
1992
+ return records;
1993
+ }
1994
+ /**
1995
+ * Parse a single CSV line, handling quoted values.
1996
+ */
1997
+ parseCsvLine(line) {
1998
+ const result = [];
1999
+ let current = "";
2000
+ let inQuotes = false;
2001
+ for (let i = 0; i < line.length; i++) {
2002
+ const char = line[i];
2003
+ if (char === '"') {
2004
+ if (inQuotes && line[i + 1] === '"') {
2005
+ current += '"';
2006
+ i++;
2007
+ } else {
2008
+ inQuotes = !inQuotes;
2009
+ }
2010
+ } else if (char === "," && !inQuotes) {
2011
+ result.push(current);
2012
+ current = "";
2013
+ } else {
2014
+ current += char;
2015
+ }
2016
+ }
2017
+ result.push(current);
2018
+ return result;
2019
+ }
2020
+ async process(context) {
2021
+ if (!context.content) {
2022
+ return;
2023
+ }
2024
+ let content = context.content;
2025
+ if (context.dependencies.imports) {
2026
+ content = await this.processPackageReferences(context, content);
2027
+ }
2028
+ if (context.dependencies.inherits) {
2029
+ content = await this.processInheritance(context, content);
2030
+ }
2031
+ if (context.metadata?.override && !context.dependencies.inherits) {
2032
+ content = await this.processStandaloneOverrides(context, content);
2033
+ }
2034
+ if (context.metadata?.context) {
2035
+ content = this.filterCodeBlocksByContext(context, content);
2036
+ }
2037
+ content = await this.processTemplate(context, content);
2038
+ context.content = content;
2039
+ }
2040
+ /**
2041
+ * Process package references (e.g., @prefix/path/to/file).
2042
+ */
2043
+ async processPackageReferences(context, content) {
2044
+ if (!context.dependencies.imports) {
2045
+ return content;
2046
+ }
2047
+ for (const [packageRef, packageInfo] of Object.entries(context.dependencies.imports)) {
2048
+ if (!packageInfo.prefix || !packageInfo.path) {
2049
+ continue;
2050
+ }
2051
+ const prefix = packageInfo.prefix;
2052
+ const packagePath = packageInfo.path;
2053
+ const pattern = new RegExp(`@${this.escapeRegex(prefix)}/([^\\s]+)`, "g");
2054
+ const matches = [];
2055
+ let match;
2056
+ while ((match = pattern.exec(content)) !== null) {
2057
+ matches.push({
2058
+ match: match[0],
2059
+ resourcePath: match[1],
2060
+ index: match.index
2061
+ });
2062
+ }
2063
+ for (let i = matches.length - 1; i >= 0; i--) {
2064
+ const m = matches[i];
2065
+ const replacement = await this.loadPackageResource(context, packagePath, m.resourcePath, prefix);
2066
+ content = content.substring(0, m.index) + replacement + content.substring(m.index + m.match.length);
2067
+ }
2068
+ }
2069
+ return content;
2070
+ }
2071
+ /**
2072
+ * Load a resource from a package.
2073
+ */
2074
+ async loadPackageResource(context, packagePath, resourcePath, prefix) {
2075
+ try {
2076
+ const fs = context.fileSystem;
2077
+ const possibleExtensions = ["", ".prmd", ".md", ".txt"];
2078
+ for (const ext of possibleExtensions) {
2079
+ const filePath = resolvePackageFile(packagePath, resourcePath + ext);
2080
+ if (await fs.exists(filePath)) {
2081
+ const contentData = await fs.readFile(filePath);
2082
+ if (isPrompdFile(filePath)) {
2083
+ const parser = new PrompdParser();
2084
+ try {
2085
+ const parsed = parser.parseContent(contentData);
2086
+ return parsed.content || `# Content from @${prefix}/${resourcePath}`;
2087
+ } catch {
2088
+ return contentData;
2089
+ }
2090
+ } else {
2091
+ return contentData;
2092
+ }
2093
+ }
2094
+ }
2095
+ if (context.verbose) {
2096
+ console.log(`Warning: Could not find @${prefix}/${resourcePath} in ${packagePath}`);
2097
+ }
2098
+ return `[Not found: @${prefix}/${resourcePath}]`;
2099
+ } catch (error) {
2100
+ if (context.verbose) {
2101
+ console.log(`Warning: Failed to load @${prefix}/${resourcePath}: ${error}`);
2102
+ }
2103
+ return `[Error loading @${prefix}/${resourcePath}]`;
2104
+ }
2105
+ }
2106
+ /**
2107
+ * Process inheritance with section-aware merging.
2108
+ */
2109
+ async processInheritance(context, content) {
2110
+ const parentPath = context.dependencies.inherits;
2111
+ if (!parentPath) {
2112
+ return content;
2113
+ }
2114
+ try {
2115
+ const fs = context.fileSystem;
2116
+ const parser = new PrompdParser();
2117
+ let parentFile = null;
2118
+ if (isPrompdFile(parentPath) && await fs.exists(parentPath)) {
2119
+ parentFile = parentPath;
2120
+ } else if (await fs.exists(parentPath) && await fs.isDirectory(parentPath)) {
2121
+ const files = (await fs.readdir(parentPath)).filter((f) => isPrompdFile(f));
2122
+ if (files.length > 0) {
2123
+ const mainFile = files.find((f) => f === "main.prmd" || f === "main.md");
2124
+ parentFile = fs.join(parentPath, mainFile || files[0]);
2125
+ }
2126
+ }
2127
+ if (!parentFile) {
2128
+ const location = context.findLocation(/inherits:/);
2129
+ context.addDiagnostic({
2130
+ message: `Could not find inherited file: ${parentPath}`,
2131
+ severity: "error",
2132
+ source: "template",
2133
+ code: "INHERITED_FILE_NOT_FOUND",
2134
+ ...location
2135
+ });
2136
+ return content;
2137
+ }
2138
+ const parentFileContent = await fs.readFile(parentFile);
2139
+ const parentData = parser.parseContent(parentFileContent);
2140
+ await this.validateParentFileReferences(context, parentData.metadata, fs, parentFile);
2141
+ if (context.hasErrors()) {
2142
+ return content;
2143
+ }
2144
+ if (parentData.content) {
2145
+ const parentDir = fs.dirname(parentFile);
2146
+ parentData.content = this.resolveIncludePaths(parentData.content, parentDir);
2147
+ }
2148
+ const overrides = context.metadata?.override || {};
2149
+ if (parentData.content || content) {
2150
+ if (Object.keys(overrides).length > 0) {
2151
+ try {
2152
+ const parentSections = parentData.content ? this.sectionProcessor.extractSections(parentData.content) : /* @__PURE__ */ new Map();
2153
+ const childSections = content ? this.sectionProcessor.extractSections(content) : /* @__PURE__ */ new Map();
2154
+ const baseDir = context.fileSystem.dirname(context.sourceFile);
2155
+ const mergedContent = await this.sectionProcessor.applyOverrides(
2156
+ parentSections,
2157
+ childSections,
2158
+ overrides,
2159
+ baseDir,
2160
+ context.verbose,
2161
+ context.fileSystem
2162
+ );
2163
+ content = mergedContent;
2164
+ if (context.verbose) {
2165
+ console.log(`\u2713 Applied ${Object.keys(overrides).length} section overrides from parent: ${parentPath}`);
2166
+ }
2167
+ } catch (error) {
2168
+ const errorMessage = error instanceof Error ? error.message : String(error);
2169
+ context.addWarning(`Section override processing failed, using simple inheritance: ${errorMessage}`);
2170
+ if (parentData.content) {
2171
+ content = content ? `${parentData.content}
2172
+
2173
+ ${content}` : parentData.content;
2174
+ }
2175
+ }
2176
+ } else {
2177
+ if (parentData.content) {
2178
+ content = content ? `${parentData.content}
2179
+
2180
+ ${content}` : parentData.content;
2181
+ }
2182
+ }
2183
+ }
2184
+ if (parentData.metadata?.parameters && context.metadata) {
2185
+ for (const parentParam of parentData.metadata.parameters) {
2186
+ const paramExists = context.metadata.parameters?.some(
2187
+ (childParam) => childParam.name === parentParam.name
2188
+ );
2189
+ if (!paramExists) {
2190
+ if (!context.metadata.parameters) {
2191
+ context.metadata.parameters = [];
2192
+ }
2193
+ context.metadata.parameters.push(parentParam);
2194
+ if (!(parentParam.name in context.parameters) && parentParam.default !== void 0) {
2195
+ let value = parentParam.default;
2196
+ if (typeof value === "string") {
2197
+ const t = parentParam.type;
2198
+ if (t === "json" || t === "object" || t === "array") {
2199
+ try {
2200
+ value = JSON.parse(value);
2201
+ } catch {
2202
+ }
2203
+ } else if (t === "boolean") {
2204
+ if (value.toLowerCase() === "true") value = true;
2205
+ else if (value.toLowerCase() === "false") value = false;
2206
+ } else if (t === "integer" || t === "number" || t === "float") {
2207
+ const n = Number(value);
2208
+ if (!isNaN(n)) value = n;
2209
+ } else if (t === "date" || t === "datetime") {
2210
+ value = resolveDateValue(value, t === "datetime");
2211
+ }
2212
+ }
2213
+ context.parameters[parentParam.name] = value;
2214
+ }
2215
+ }
2216
+ }
2217
+ }
2218
+ if (context.verbose) {
2219
+ console.log(`\u2713 Template inherits from: ${parentPath}`);
2220
+ }
2221
+ } catch (error) {
2222
+ const errorMessage = error instanceof Error ? error.message : String(error);
2223
+ context.addWarning(`Failed to process inheritance from ${parentPath}: ${errorMessage}`);
2224
+ }
2225
+ return content;
2226
+ }
2227
+ /**
2228
+ * Process standalone overrides (without inheritance).
2229
+ */
2230
+ async processStandaloneOverrides(context, content) {
2231
+ const overrides = context.metadata?.override;
2232
+ if (!overrides) {
2233
+ return content;
2234
+ }
2235
+ if (context.verbose) {
2236
+ console.log(`Processing ${Object.keys(overrides).length} standalone section overrides...`);
2237
+ }
2238
+ try {
2239
+ const currentSections = this.sectionProcessor.extractSections(content);
2240
+ const baseDir = context.fileSystem.dirname(context.sourceFile);
2241
+ for (const [sectionId, overridePath] of Object.entries(overrides)) {
2242
+ if (currentSections.has(sectionId)) {
2243
+ if (overridePath === null) {
2244
+ currentSections.delete(sectionId);
2245
+ if (context.verbose) {
2246
+ console.log(` - Removing section '${sectionId}'`);
2247
+ }
2248
+ } else if (typeof overridePath === "string") {
2249
+ try {
2250
+ const overrideContent = await this.sectionProcessor.loadOverrideContent(
2251
+ overridePath,
2252
+ baseDir,
2253
+ context.fileSystem
2254
+ );
2255
+ const originalSection = currentSections.get(sectionId);
2256
+ currentSections.set(sectionId, {
2257
+ ...originalSection,
2258
+ content: overrideContent
2259
+ });
2260
+ if (context.verbose) {
2261
+ console.log(` - Replacing section '${sectionId}' with content from ${overridePath}`);
2262
+ }
2263
+ } catch (error) {
2264
+ const errorMessage = error instanceof Error ? error.message : String(error);
2265
+ context.addWarning(`Failed to apply override for section '${sectionId}': ${errorMessage}`);
2266
+ }
2267
+ }
2268
+ } else {
2269
+ context.addWarning(`Override section '${sectionId}' not found in current content`);
2270
+ if (context.verbose) {
2271
+ const available = Array.from(currentSections.keys()).sort().join(", ");
2272
+ console.log(`Warning: Override section '${sectionId}' not found. Available: ${available}`);
2273
+ }
2274
+ }
2275
+ }
2276
+ const contentParts = [];
2277
+ for (const section of currentSections.values()) {
2278
+ contentParts.push(section.content);
2279
+ }
2280
+ content = contentParts.join("\n\n");
2281
+ if (context.verbose) {
2282
+ console.log(`\u2713 Applied standalone overrides, final content has ${currentSections.size} sections`);
2283
+ }
2284
+ } catch (error) {
2285
+ const errorMessage = error instanceof Error ? error.message : String(error);
2286
+ context.addWarning(`Standalone override processing failed: ${errorMessage}`);
2287
+ }
2288
+ return content;
2289
+ }
2290
+ /**
2291
+ * Filter code blocks in content based on context file types.
2292
+ *
2293
+ * When context files are attached (e.g., .ts files), this method filters
2294
+ * the prompt content to only keep code blocks that match those file types.
2295
+ *
2296
+ * Example:
2297
+ * - If context contains "typescript-examples.ts", keep only ```typescript blocks
2298
+ * - Non-code content and unmatched code blocks are preserved as plain text
2299
+ *
2300
+ * @param context - The compilation context with extracted contexts
2301
+ * @param content - The markdown content to filter
2302
+ * @returns Filtered content with only matching code blocks
2303
+ */
2304
+ filterCodeBlocksByContext(context, content) {
2305
+ const contextExtensions = this.extractContextExtensions(context.metadata?.context);
2306
+ if (contextExtensions.size === 0) {
2307
+ return content;
2308
+ }
2309
+ const allowedLanguages = /* @__PURE__ */ new Set();
2310
+ for (const ext of contextExtensions) {
2311
+ const languages = getLanguageAliasesForExtension(ext);
2312
+ languages.forEach((lang) => allowedLanguages.add(lang.toLowerCase()));
2313
+ }
2314
+ if (allowedLanguages.size === 0) {
2315
+ return content;
2316
+ }
2317
+ if (context.verbose) {
2318
+ console.log(`Filtering code blocks for languages: ${Array.from(allowedLanguages).join(", ")}`);
2319
+ }
2320
+ const result = this.filterCodeBlocks(content, allowedLanguages, context.verbose);
2321
+ if (context.verbose && result.removedCount > 0) {
2322
+ console.log(`Removed ${result.removedCount} non-matching code block(s)`);
2323
+ }
2324
+ return result.content;
2325
+ }
2326
+ /**
2327
+ * Extract file extensions from context metadata.
2328
+ * Context can be a single file path string or an array of file paths.
2329
+ */
2330
+ extractContextExtensions(contextValue) {
2331
+ const extensions = /* @__PURE__ */ new Set();
2332
+ if (!contextValue) {
2333
+ return extensions;
2334
+ }
2335
+ const contextFiles = Array.isArray(contextValue) ? contextValue : [contextValue];
2336
+ for (const filePath of contextFiles) {
2337
+ if (typeof filePath === "string") {
2338
+ const ext = extname(filePath).toLowerCase();
2339
+ if (ext) {
2340
+ extensions.add(ext);
2341
+ }
2342
+ }
2343
+ }
2344
+ return extensions;
2345
+ }
2346
+ /**
2347
+ * Filter code blocks in markdown content, keeping only those with matching languages.
2348
+ *
2349
+ * @param content - Markdown content with code blocks
2350
+ * @param allowedLanguages - Set of allowed language identifiers (lowercase)
2351
+ * @param verbose - Whether to log filtering actions
2352
+ * @returns Object with filtered content and count of removed blocks
2353
+ */
2354
+ filterCodeBlocks(content, allowedLanguages, verbose) {
2355
+ const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
2356
+ let removedCount = 0;
2357
+ const filteredContent = content.replace(codeBlockRegex, (match, language, blockContent) => {
2358
+ const lang = (language || "").toLowerCase().trim();
2359
+ if (!lang) {
2360
+ return match;
2361
+ }
2362
+ if (allowedLanguages.has(lang)) {
2363
+ return match;
2364
+ }
2365
+ removedCount++;
2366
+ if (verbose) {
2367
+ const preview = blockContent.substring(0, 50).replace(/\n/g, "\\n");
2368
+ console.log(` - Removing \`\`\`${lang} block: "${preview}..."`);
2369
+ }
2370
+ return "";
2371
+ });
2372
+ const cleanedContent = filteredContent.replace(/\n{3,}/g, "\n\n");
2373
+ return { content: cleanedContent, removedCount };
2374
+ }
2375
+ /**
2376
+ * Process template with Jinja2/Nunjucks.
2377
+ * Uses a context-aware environment with PrompdLoader to support {% include %}.
2378
+ */
2379
+ async processTemplate(context, content) {
2380
+ if (!content) {
2381
+ return content;
2382
+ }
2383
+ try {
2384
+ const baseDir = context.fileSystem.dirname(context.sourceFile);
2385
+ const loader = new PrompdLoader({
2386
+ fileSystem: context.fileSystem,
2387
+ baseDir,
2388
+ verbose: context.verbose,
2389
+ maxDepth: 10
2390
+ });
2391
+ const env = new nunjucks.Environment(loader, {
2392
+ autoescape: false,
2393
+ throwOnUndefined: false,
2394
+ trimBlocks: true,
2395
+ lstripBlocks: true,
2396
+ tags: {
2397
+ blockStart: "{%",
2398
+ blockEnd: "%}",
2399
+ variableStart: "{{",
2400
+ variableEnd: "}}",
2401
+ commentStart: "{#",
2402
+ commentEnd: "#}"
2403
+ }
2404
+ });
2405
+ this.registerFiltersOnEnv(env);
2406
+ const renderTimeout = 5e3;
2407
+ const renderPromise = new Promise((resolve, reject) => {
2408
+ try {
2409
+ const result = env.renderString(content, context.parameters);
2410
+ resolve(result);
2411
+ } catch (error) {
2412
+ reject(error);
2413
+ }
2414
+ });
2415
+ let timeoutId;
2416
+ const timeoutPromise = new Promise((_, reject) => {
2417
+ timeoutId = setTimeout(() => reject(new Error("Template rendering timeout")), renderTimeout);
2418
+ });
2419
+ try {
2420
+ content = await Promise.race([renderPromise, timeoutPromise]);
2421
+ } finally {
2422
+ clearTimeout(timeoutId);
2423
+ }
2424
+ } catch (error) {
2425
+ const errorMessage = error instanceof Error ? error.message : String(error);
2426
+ const lineMatch = errorMessage.match(/\[Line (\d+), Column (\d+)\]/i) || errorMessage.match(/line (\d+)/i);
2427
+ const line = lineMatch ? parseInt(lineMatch[1], 10) : void 0;
2428
+ const column = lineMatch && lineMatch[2] ? parseInt(lineMatch[2], 10) : void 0;
2429
+ context.addDiagnostic({
2430
+ message: `Template syntax error: ${errorMessage}`,
2431
+ severity: "error",
2432
+ source: "template",
2433
+ code: "TEMPLATE_SYNTAX_ERROR",
2434
+ line,
2435
+ column
2436
+ });
2437
+ content = this.enhancedSimpleSubstitution(content, context.parameters);
2438
+ }
2439
+ return content;
2440
+ }
2441
+ /**
2442
+ * Resolve relative {% include %} paths in content to absolute paths.
2443
+ * This ensures that when parent content is merged into a child file,
2444
+ * the includes still resolve correctly relative to the parent's directory.
2445
+ */
2446
+ resolveIncludePaths(content, baseDir) {
2447
+ return content.replace(
2448
+ /(\{%[-\s]*include\s+)(["'])(\.[^"']+)\2(\s*[-]?%\})/g,
2449
+ (_match, prefix, quote, relativePath, suffix) => {
2450
+ const absolutePath = resolvePosix(baseDir, relativePath);
2451
+ const normalizedPath = absolutePath.replace(/\\/g, "/");
2452
+ return `${prefix}${quote}${normalizedPath}${quote}${suffix}`;
2453
+ }
2454
+ );
2455
+ }
2456
+ /**
2457
+ * Register custom filters on a Nunjucks environment.
2458
+ */
2459
+ registerFiltersOnEnv(env) {
2460
+ env.addFilter("trim", (str) => {
2461
+ if (str === void 0 || str === null) return "";
2462
+ return String(str).trim();
2463
+ });
2464
+ env.addFilter("lower", (str) => {
2465
+ if (str === void 0 || str === null) return "";
2466
+ return String(str).toLowerCase();
2467
+ });
2468
+ env.addFilter("upper", (str) => {
2469
+ if (str === void 0 || str === null) return "";
2470
+ return String(str).toUpperCase();
2471
+ });
2472
+ env.addFilter("capitalize", (str) => {
2473
+ if (str === void 0 || str === null) return "";
2474
+ const s = String(str);
2475
+ return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
2476
+ });
2477
+ env.addFilter("title", (str) => {
2478
+ if (str === void 0 || str === null) return "";
2479
+ return String(str).replace(/\b\w/g, (c) => c.toUpperCase());
2480
+ });
2481
+ env.addFilter("replace", (str, old, newStr, count) => {
2482
+ if (str === void 0 || str === null) return "";
2483
+ const s = String(str);
2484
+ if (count !== void 0) {
2485
+ let result = s;
2486
+ let replaced = 0;
2487
+ while (replaced < count) {
2488
+ const idx = result.indexOf(old);
2489
+ if (idx === -1) break;
2490
+ result = result.slice(0, idx) + newStr + result.slice(idx + old.length);
2491
+ replaced++;
2492
+ }
2493
+ return result;
2494
+ }
2495
+ return s.split(old).join(newStr);
2496
+ });
2497
+ env.addFilter("striptags", (str) => {
2498
+ if (str === void 0 || str === null) return "";
2499
+ return String(str).replace(/<[^>]*>/g, "");
2500
+ });
2501
+ env.addFilter("urlencode", (str) => {
2502
+ if (str === void 0 || str === null) return "";
2503
+ return encodeURIComponent(String(str));
2504
+ });
2505
+ env.addFilter("fromcsv", (csvString) => {
2506
+ if (!csvString || typeof csvString !== "string") {
2507
+ return [];
2508
+ }
2509
+ return this.parseCsv(csvString);
2510
+ });
2511
+ env.addFilter("fromjson", (value) => {
2512
+ if (value === null || value === void 0) return null;
2513
+ if (typeof value !== "string") return value;
2514
+ try {
2515
+ return JSON.parse(value);
2516
+ } catch {
2517
+ return null;
2518
+ }
2519
+ });
2520
+ env.addFilter("tojson", (obj, indent) => {
2521
+ try {
2522
+ return JSON.stringify(obj, null, indent);
2523
+ } catch {
2524
+ return "{}";
2525
+ }
2526
+ });
2527
+ env.addFilter("lines", (str) => {
2528
+ if (!str || typeof str !== "string") {
2529
+ return [];
2530
+ }
2531
+ return str.split(/\r?\n/);
2532
+ });
2533
+ env.addFilter("dedent", (str) => {
2534
+ if (!str || typeof str !== "string") {
2535
+ return "";
2536
+ }
2537
+ const lines = str.split(/\r?\n/);
2538
+ let minIndent = Infinity;
2539
+ for (const line of lines) {
2540
+ if (line.trim().length === 0) continue;
2541
+ const match = line.match(/^(\s*)/);
2542
+ if (match && match[1].length < minIndent) {
2543
+ minIndent = match[1].length;
2544
+ }
2545
+ }
2546
+ if (minIndent === Infinity) minIndent = 0;
2547
+ return lines.map((line) => line.slice(minIndent)).join("\n");
2548
+ });
2549
+ env.addFilter("truncate", (str, length = 80, suffix = "...") => {
2550
+ if (!str || typeof str !== "string") {
2551
+ return "";
2552
+ }
2553
+ if (str.length <= length) {
2554
+ return str;
2555
+ }
2556
+ return str.slice(0, length - suffix.length) + suffix;
2557
+ });
2558
+ env.addFilter("codeblock", (str, language = "") => {
2559
+ if (!str || typeof str !== "string") {
2560
+ return "";
2561
+ }
2562
+ return "```" + language + "\n" + str + "\n```";
2563
+ });
2564
+ env.addFilter("unique", (arr) => {
2565
+ if (!Array.isArray(arr)) {
2566
+ return [];
2567
+ }
2568
+ return [...new Set(arr)];
2569
+ });
2570
+ env.addFilter("pluck", (arr, field) => {
2571
+ if (!Array.isArray(arr)) {
2572
+ return [];
2573
+ }
2574
+ return arr.map((item) => item?.[field]).filter((v) => v !== void 0);
2575
+ });
2576
+ env.addFilter("where", (arr, field, value) => {
2577
+ if (!Array.isArray(arr)) {
2578
+ return [];
2579
+ }
2580
+ return arr.filter((item) => item?.[field] === value);
2581
+ });
2582
+ env.addFilter("groupby", (arr, field) => {
2583
+ if (!Array.isArray(arr)) {
2584
+ return {};
2585
+ }
2586
+ const result = {};
2587
+ for (const item of arr) {
2588
+ const key = String(item?.[field] ?? "undefined");
2589
+ if (!result[key]) {
2590
+ result[key] = [];
2591
+ }
2592
+ result[key].push(item);
2593
+ }
2594
+ return result;
2595
+ });
2596
+ env.addFilter("shuffle", (arr) => {
2597
+ if (!Array.isArray(arr)) {
2598
+ return [];
2599
+ }
2600
+ const shuffled = [...arr];
2601
+ for (let i = shuffled.length - 1; i > 0; i--) {
2602
+ const j = Math.floor(Math.random() * (i + 1));
2603
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
2604
+ }
2605
+ return shuffled;
2606
+ });
2607
+ env.addFilter("sample", (arr, count = 1) => {
2608
+ if (!Array.isArray(arr)) {
2609
+ return [];
2610
+ }
2611
+ const shuffled = [...arr];
2612
+ for (let i = shuffled.length - 1; i > 0; i--) {
2613
+ const j = Math.floor(Math.random() * (i + 1));
2614
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
2615
+ }
2616
+ return shuffled.slice(0, Math.min(count, shuffled.length));
2617
+ });
2618
+ env.addFilter("wordwrap", (str, width = 80) => {
2619
+ if (!str || typeof str !== "string") {
2620
+ return "";
2621
+ }
2622
+ const words = str.split(/\s+/);
2623
+ const lines = [];
2624
+ let currentLine = "";
2625
+ for (const word of words) {
2626
+ if (currentLine.length + word.length + 1 <= width) {
2627
+ currentLine += (currentLine ? " " : "") + word;
2628
+ } else {
2629
+ if (currentLine) lines.push(currentLine);
2630
+ currentLine = word;
2631
+ }
2632
+ }
2633
+ if (currentLine) lines.push(currentLine);
2634
+ return lines.join("\n");
2635
+ });
2636
+ env.addFilter("bulletlist", (input) => {
2637
+ const items = Array.isArray(input) ? input : String(input).split(/\r?\n/);
2638
+ return items.filter((item) => String(item).trim()).map((item) => `- ${item}`).join("\n");
2639
+ });
2640
+ env.addFilter("numberedlist", (input) => {
2641
+ const items = Array.isArray(input) ? input : String(input).split(/\r?\n/);
2642
+ return items.filter((item) => String(item).trim()).map((item, i) => `${i + 1}. ${item}`).join("\n");
2643
+ });
2644
+ }
2645
+ /**
2646
+ * Enhanced simple substitution with nested property access.
2647
+ * Supports both single brace {var} and double brace {{var}} syntax for backward compatibility.
2648
+ */
2649
+ enhancedSimpleSubstitution(content, parameters) {
2650
+ if (content === void 0 || content === null) {
2651
+ return "";
2652
+ }
2653
+ const resolveValue = (fullPath) => {
2654
+ const parts = fullPath.split(".");
2655
+ let value = parameters[parts[0]];
2656
+ if (value === void 0 || value === null) {
2657
+ return null;
2658
+ }
2659
+ try {
2660
+ for (let i = 1; i < parts.length; i++) {
2661
+ if (typeof value === "object" && value !== null) {
2662
+ value = value[parts[i]];
2663
+ } else {
2664
+ return null;
2665
+ }
2666
+ if (value === void 0 || value === null) {
2667
+ return null;
2668
+ }
2669
+ }
2670
+ return String(value);
2671
+ } catch {
2672
+ return null;
2673
+ }
2674
+ };
2675
+ content = content.replace(/\{\{[-~]?\s*([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*[-~]?\}\}/g, (_match, fullPath) => {
2676
+ const resolved = resolveValue(fullPath);
2677
+ return resolved !== null ? resolved : `[Missing: ${fullPath}]`;
2678
+ });
2679
+ content = content.replace(/(?<!\{)\{([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\}(?!\})/g, (_match, fullPath) => {
2680
+ const resolved = resolveValue(fullPath);
2681
+ return resolved !== null ? resolved : `[Missing: ${fullPath}]`;
2682
+ });
2683
+ return content;
2684
+ }
2685
+ /**
2686
+ * Escape special regex characters.
2687
+ */
2688
+ escapeRegex(str) {
2689
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2690
+ }
2691
+ /**
2692
+ * Validate that all file references in a parent .prmd's metadata actually exist.
2693
+ * Called during inheritance processing because AssetExtractionStage only runs on
2694
+ * the child file's metadata — the parent is only parsed, never fully compiled.
2695
+ */
2696
+ async validateParentFileReferences(context, metadata, fs, parentFile) {
2697
+ if (!metadata) return;
2698
+ const metadataAsRecord = metadata;
2699
+ const parentDir = fs.dirname(parentFile);
2700
+ const fileFields = ["system", "task", "user", "assistant", "response", "output", "context"];
2701
+ for (const field of fileFields) {
2702
+ const fieldValue = metadataAsRecord[field];
2703
+ if (!fieldValue) continue;
2704
+ const refs = Array.isArray(fieldValue) ? fieldValue.filter((v) => typeof v === "string") : typeof fieldValue === "string" ? [fieldValue] : [];
2705
+ for (const ref of refs) {
2706
+ if (!ref.startsWith("./") && !ref.startsWith("../")) continue;
2707
+ const resolvedPath = fs.resolve(parentDir, ref);
2708
+ const exists = await Promise.resolve(fs.exists(resolvedPath));
2709
+ if (!exists) {
2710
+ const location = context.findLocation(/inherits:/);
2711
+ context.addDiagnostic({
2712
+ message: `Inherited file "${basenamePosix(parentFile)}" references missing ${field} file: "${ref}" (resolved to: ${resolvedPath})`,
2713
+ severity: "error",
2714
+ source: "template",
2715
+ code: "INHERITED_METADATA_FILE_NOT_FOUND",
2716
+ ...location || {}
2717
+ });
2718
+ }
2719
+ }
2720
+ }
2721
+ }
2722
+ getName() {
2723
+ return "Template Processing";
2724
+ }
2725
+ };
2726
+ var MarkdownFormatter = class {
2727
+ constructor() {
2728
+ this.name = "markdown";
2729
+ this.fileExtension = ".md";
2730
+ this.mimeType = "text/markdown";
2731
+ }
2732
+ async format(compiled) {
2733
+ const output = [];
2734
+ if (compiled.verbose && compiled.metadata) {
2735
+ output.push("<!-- PROMPD METADATA");
2736
+ const cleanMetadata = this.cleanMetadataForDisplay(compiled.metadata);
2737
+ output.push(yaml.stringify(cleanMetadata));
2738
+ output.push("-->");
2739
+ output.push("");
2740
+ }
2741
+ if (compiled.contexts && compiled.contexts.length > 0) {
2742
+ output.push("# Extracted Context Files");
2743
+ output.push("");
2744
+ for (const ctx of compiled.contexts) {
2745
+ output.push(ctx);
2746
+ output.push("");
2747
+ }
2748
+ }
2749
+ if (compiled.content) {
2750
+ if (compiled.verbose) {
2751
+ output.push("# Main Prompt Content");
2752
+ output.push("");
2753
+ }
2754
+ output.push(compiled.content);
2755
+ }
2756
+ return output.join("\n");
2757
+ }
2758
+ /**
2759
+ * Clean metadata dictionary for YAML display, converting complex objects to strings.
2760
+ */
2761
+ cleanMetadataForDisplay(metadata) {
2762
+ if (typeof metadata !== "object" || metadata === null) {
2763
+ return metadata;
2764
+ }
2765
+ if (Array.isArray(metadata)) {
2766
+ return metadata.map((item) => this.cleanMetadataForDisplay(item));
2767
+ }
2768
+ const cleaned = {};
2769
+ for (const [key, value] of Object.entries(metadata)) {
2770
+ if (value === void 0) {
2771
+ continue;
2772
+ }
2773
+ if (typeof value === "object" && value !== null) {
2774
+ if ("value" in value) {
2775
+ cleaned[key] = value.value;
2776
+ } else if (Array.isArray(value)) {
2777
+ cleaned[key] = value.map((item) => this.cleanMetadataForDisplay(item));
2778
+ } else {
2779
+ cleaned[key] = this.cleanMetadataForDisplay(value);
2780
+ }
2781
+ } else {
2782
+ cleaned[key] = value;
2783
+ }
2784
+ }
2785
+ return cleaned;
2786
+ }
2787
+ };
2788
+
2789
+ // src/lib/compiler/formatters/openai.ts
2790
+ var OpenAIFormatter = class {
2791
+ constructor() {
2792
+ this.name = "provider-json:openai";
2793
+ this.fileExtension = ".json";
2794
+ this.mimeType = "application/json";
2795
+ }
2796
+ async format(compiled) {
2797
+ const messages = [];
2798
+ if (!compiled.content) {
2799
+ return JSON.stringify({
2800
+ model: "gpt-4",
2801
+ messages: [],
2802
+ temperature: 0.1
2803
+ }, null, 2);
2804
+ }
2805
+ const sections = this.parseSections(compiled.content);
2806
+ let systemMessage = "";
2807
+ const systemParts = [];
2808
+ if (sections.System) {
2809
+ systemParts.push(sections.System);
2810
+ }
2811
+ if (sections.Context) {
2812
+ systemParts.push(sections.Context);
2813
+ }
2814
+ systemMessage = systemParts.join("\n\n");
2815
+ if (systemMessage) {
2816
+ messages.push({
2817
+ role: "system",
2818
+ content: systemMessage
2819
+ });
2820
+ }
2821
+ let examplesContent = "";
2822
+ if (sections.Examples) {
2823
+ examplesContent = sections.Examples;
2824
+ }
2825
+ for (const [sectionName, content] of Object.entries(sections)) {
2826
+ if (sectionName === "System" || sectionName === "Context" || sectionName === "Examples") continue;
2827
+ const role = this.sectionToRole(sectionName);
2828
+ if (role) {
2829
+ let messageContent = content.trim();
2830
+ if (role === "user" && examplesContent && messages.filter((m) => m.role === "user").length === 0) {
2831
+ messageContent = `${messageContent}
2832
+
2833
+ ${examplesContent}`;
2834
+ examplesContent = "";
2835
+ }
2836
+ messages.push({ role, content: messageContent });
2837
+ }
2838
+ }
2839
+ if (compiled.contexts && compiled.contexts.length > 0 && messages.length > 0) {
2840
+ const firstUserMsg = messages.find((m) => m.role === "user");
2841
+ if (firstUserMsg) {
2842
+ const contextContent = compiled.contexts.join("\n\n");
2843
+ firstUserMsg.content = `${contextContent}
2844
+
2845
+ ${firstUserMsg.content}`;
2846
+ }
2847
+ }
2848
+ const apiRequest = {
2849
+ model: "gpt-4",
2850
+ messages,
2851
+ temperature: 0.1
2852
+ };
2853
+ return JSON.stringify(apiRequest, null, 2);
2854
+ }
2855
+ /**
2856
+ * Parse all sections from markdown content.
2857
+ */
2858
+ parseSections(content) {
2859
+ const sections = {};
2860
+ const lines = content.split("\n");
2861
+ let currentSection = null;
2862
+ let currentContent = [];
2863
+ let sectionOrder = [];
2864
+ for (const line of lines) {
2865
+ const trimmed = line.trim();
2866
+ const match = trimmed.match(/^# (.+)$/);
2867
+ if (match) {
2868
+ if (currentSection) {
2869
+ const key = sectionOrder.filter((s) => s === currentSection).length > 0 ? `${currentSection}_${sectionOrder.filter((s) => s === currentSection).length}` : currentSection;
2870
+ sections[key] = currentContent.join("\n").trim();
2871
+ sectionOrder.push(currentSection);
2872
+ }
2873
+ currentSection = match[1];
2874
+ currentContent = [];
2875
+ } else if (currentSection) {
2876
+ currentContent.push(line);
2877
+ }
2878
+ }
2879
+ if (currentSection) {
2880
+ const key = sectionOrder.filter((s) => s === currentSection).length > 0 ? `${currentSection}_${sectionOrder.filter((s) => s === currentSection).length}` : currentSection;
2881
+ sections[key] = currentContent.join("\n").trim();
2882
+ }
2883
+ return sections;
2884
+ }
2885
+ /**
2886
+ * Convert section name to message role.
2887
+ */
2888
+ sectionToRole(sectionName) {
2889
+ const normalized = sectionName.replace(/_\d+$/, "").toLowerCase();
2890
+ if (normalized === "user") return "user";
2891
+ if (normalized === "assistant") return "assistant";
2892
+ return null;
2893
+ }
2894
+ };
2895
+
2896
+ // src/lib/compiler/formatters/anthropic.ts
2897
+ var AnthropicFormatter = class {
2898
+ constructor() {
2899
+ this.name = "provider-json:anthropic";
2900
+ this.fileExtension = ".json";
2901
+ this.mimeType = "application/json";
2902
+ }
2903
+ async format(compiled) {
2904
+ let systemMessage = "";
2905
+ const messages = [];
2906
+ if (!compiled.content) {
2907
+ return JSON.stringify({
2908
+ model: "claude-3-sonnet-20240229",
2909
+ system: "",
2910
+ messages: []
2911
+ }, null, 2);
2912
+ }
2913
+ const sections = this.parseSections(compiled.content);
2914
+ const systemParts = [];
2915
+ if (sections.System) {
2916
+ systemParts.push(sections.System);
2917
+ }
2918
+ if (sections.Context) {
2919
+ systemParts.push(sections.Context);
2920
+ }
2921
+ systemMessage = systemParts.join("\n\n");
2922
+ let examplesContent = "";
2923
+ if (sections.Examples) {
2924
+ examplesContent = sections.Examples;
2925
+ }
2926
+ for (const [sectionName, content] of Object.entries(sections)) {
2927
+ if (sectionName === "System" || sectionName === "Context" || sectionName === "Examples") continue;
2928
+ const role = this.sectionToRole(sectionName);
2929
+ if (role) {
2930
+ let messageContent = content.trim();
2931
+ if (role === "user" && examplesContent && messages.filter((m) => m.role === "user").length === 0) {
2932
+ messageContent = `${messageContent}
2933
+
2934
+ ${examplesContent}`;
2935
+ examplesContent = "";
2936
+ }
2937
+ messages.push({ role, content: messageContent });
2938
+ }
2939
+ }
2940
+ if (compiled.contexts && compiled.contexts.length > 0 && messages.length > 0) {
2941
+ const firstUserMsg = messages.find((m) => m.role === "user");
2942
+ if (firstUserMsg) {
2943
+ const contextContent = compiled.contexts.join("\n\n");
2944
+ firstUserMsg.content = `${contextContent}
2945
+
2946
+ ${firstUserMsg.content}`;
2947
+ }
2948
+ }
2949
+ const apiRequest = {
2950
+ model: "claude-3-sonnet-20240229",
2951
+ system: systemMessage,
2952
+ messages
2953
+ };
2954
+ return JSON.stringify(apiRequest, null, 2);
2955
+ }
2956
+ /**
2957
+ * Parse all sections from markdown content.
2958
+ */
2959
+ parseSections(content) {
2960
+ const sections = {};
2961
+ const lines = content.split("\n");
2962
+ let currentSection = null;
2963
+ let currentContent = [];
2964
+ let sectionOrder = [];
2965
+ for (const line of lines) {
2966
+ const trimmed = line.trim();
2967
+ const match = trimmed.match(/^# (.+)$/);
2968
+ if (match) {
2969
+ if (currentSection) {
2970
+ const key = sectionOrder.filter((s) => s === currentSection).length > 0 ? `${currentSection}_${sectionOrder.filter((s) => s === currentSection).length}` : currentSection;
2971
+ sections[key] = currentContent.join("\n").trim();
2972
+ sectionOrder.push(currentSection);
2973
+ }
2974
+ currentSection = match[1];
2975
+ currentContent = [];
2976
+ } else if (currentSection) {
2977
+ currentContent.push(line);
2978
+ }
2979
+ }
2980
+ if (currentSection) {
2981
+ const key = sectionOrder.filter((s) => s === currentSection).length > 0 ? `${currentSection}_${sectionOrder.filter((s) => s === currentSection).length}` : currentSection;
2982
+ sections[key] = currentContent.join("\n").trim();
2983
+ }
2984
+ return sections;
2985
+ }
2986
+ /**
2987
+ * Convert section name to message role.
2988
+ */
2989
+ sectionToRole(sectionName) {
2990
+ const normalized = sectionName.replace(/_\d+$/, "").toLowerCase();
2991
+ if (normalized === "user") return "user";
2992
+ if (normalized === "assistant") return "assistant";
2993
+ if (normalized === "context" || normalized === "examples") {
2994
+ return "user";
2995
+ }
2996
+ return null;
2997
+ }
2998
+ };
2999
+
3000
+ // src/lib/compiler/stages/codegen.ts
3001
+ var CodeGenerationStage = class {
3002
+ constructor(formatters) {
3003
+ this.formatters = formatters || /* @__PURE__ */ new Map();
3004
+ this.registerFormatter(new MarkdownFormatter());
3005
+ this.registerFormatter(new OpenAIFormatter());
3006
+ this.registerFormatter(new AnthropicFormatter());
3007
+ }
3008
+ /**
3009
+ * Register a new output formatter.
3010
+ */
3011
+ registerFormatter(formatter) {
3012
+ this.formatters.set(formatter.name, formatter);
3013
+ }
3014
+ async process(context) {
3015
+ let formatName = context.outputFormat;
3016
+ if (formatName.startsWith("to-")) {
3017
+ formatName = formatName.substring(3);
3018
+ }
3019
+ if (formatName === "provider-json") {
3020
+ formatName = "provider-json:openai";
3021
+ }
3022
+ const formatter = this.formatters.get(formatName);
3023
+ if (!formatter) {
3024
+ context.addError(`Unknown output format: ${formatName}`);
3025
+ return;
3026
+ }
3027
+ try {
3028
+ const compiled = {
3029
+ metadata: context.metadata,
3030
+ content: context.content,
3031
+ contexts: context.contexts,
3032
+ parameters: context.parameters,
3033
+ verbose: context.verbose
3034
+ };
3035
+ context.compiledResult = await formatter.format(compiled);
3036
+ if (context.verbose) {
3037
+ console.log(`\u2713 Generated output in format: ${formatName}`);
3038
+ }
3039
+ } catch (error) {
3040
+ const errorMessage = error instanceof Error ? error.message : String(error);
3041
+ context.addError(`Code generation failed: ${errorMessage}`);
3042
+ }
3043
+ }
3044
+ getName() {
3045
+ return "Code Generation";
3046
+ }
3047
+ };
3048
+
3049
+ // src/lib/compiler/file-system.ts
3050
+ function normalizePosix2(p) {
3051
+ const isAbs = p.startsWith("/");
3052
+ const out = [];
3053
+ for (const seg of p.split("/")) {
3054
+ if (seg === "" || seg === ".") continue;
3055
+ if (seg === "..") {
3056
+ if (out.length && out[out.length - 1] !== "..") out.pop();
3057
+ else if (!isAbs) out.push("..");
3058
+ } else {
3059
+ out.push(seg);
3060
+ }
3061
+ }
3062
+ const joined = out.join("/");
3063
+ return isAbs ? "/" + joined : joined || ".";
3064
+ }
3065
+ function joinPosix2(...segments) {
3066
+ const filtered = segments.filter((s) => s && s.length > 0);
3067
+ if (filtered.length === 0) return ".";
3068
+ return normalizePosix2(filtered.join("/"));
3069
+ }
3070
+ function dirnamePosix2(p) {
3071
+ const norm = p.replace(/\/+$/, "");
3072
+ const idx = norm.lastIndexOf("/");
3073
+ if (idx === -1) return ".";
3074
+ if (idx === 0) return "/";
3075
+ return norm.slice(0, idx);
3076
+ }
3077
+ function utf8ByteLength(s) {
3078
+ return new TextEncoder().encode(s).length;
3079
+ }
3080
+ function toRelKey(filePath) {
3081
+ let n = filePath.replace(/\\/g, "/");
3082
+ if (n.startsWith("./")) n = n.substring(2);
3083
+ if (n.startsWith("/")) n = n.substring(1);
3084
+ if (n.endsWith("/") && n.length > 1) n = n.substring(0, n.length - 1);
3085
+ return n;
3086
+ }
3087
+ var MemoryFileSystem = class {
3088
+ constructor(files = {}) {
3089
+ this.files = /* @__PURE__ */ new Map();
3090
+ for (const [filePath, content] of Object.entries(files)) {
3091
+ this.addFile(filePath, content);
3092
+ }
3093
+ }
3094
+ /** Add or update a file in the in-memory file system. */
3095
+ addFile(filePath, content) {
3096
+ this.files.set(this.normalizePath(filePath), content);
3097
+ }
3098
+ /** Add multiple files at once. */
3099
+ addFiles(files) {
3100
+ for (const [filePath, content] of Object.entries(files)) {
3101
+ this.addFile(filePath, content);
3102
+ }
3103
+ }
3104
+ exists(filePath) {
3105
+ return this.files.has(this.normalizePath(filePath));
3106
+ }
3107
+ readFile(filePath) {
3108
+ const normalizedPath = this.normalizePath(filePath);
3109
+ const content = this.files.get(normalizedPath);
3110
+ if (content === void 0) {
3111
+ throw new Error(`File not found: ${filePath}`);
3112
+ }
3113
+ return content;
3114
+ }
3115
+ isDirectory(filePath) {
3116
+ const normalizedPath = this.normalizePath(filePath);
3117
+ const dirPrefix = normalizedPath.endsWith("/") ? normalizedPath : normalizedPath + "/";
3118
+ for (const file of this.files.keys()) {
3119
+ if (file.startsWith(dirPrefix)) {
3120
+ return true;
3121
+ }
3122
+ }
3123
+ return false;
3124
+ }
3125
+ readdir(dirPath) {
3126
+ const normalizedDir = this.normalizePath(dirPath);
3127
+ const dirPrefix = normalizedDir.endsWith("/") ? normalizedDir : normalizedDir + "/";
3128
+ const files = /* @__PURE__ */ new Set();
3129
+ for (const file of this.files.keys()) {
3130
+ if (file.startsWith(dirPrefix)) {
3131
+ const relativePath = file.substring(dirPrefix.length);
3132
+ const slashIndex = relativePath.indexOf("/");
3133
+ const fileName = slashIndex === -1 ? relativePath : relativePath.substring(0, slashIndex);
3134
+ if (fileName) {
3135
+ files.add(fileName);
3136
+ }
3137
+ }
3138
+ }
3139
+ return Array.from(files);
3140
+ }
3141
+ resolve(...pathSegments) {
3142
+ return joinPosix2("/", ...pathSegments);
3143
+ }
3144
+ dirname(filePath) {
3145
+ return dirnamePosix2(filePath);
3146
+ }
3147
+ join(...pathSegments) {
3148
+ return joinPosix2(...pathSegments);
3149
+ }
3150
+ /** Get the virtual file system path for a package. */
3151
+ getPackagePath(packageName, version) {
3152
+ return `/packages/${packageName}@${version}`;
3153
+ }
3154
+ /** Get all files under an optional base path. */
3155
+ getAllFiles(basePath) {
3156
+ if (!basePath) {
3157
+ return new Map(this.files);
3158
+ }
3159
+ const normalizedBase = this.normalizePath(basePath);
3160
+ const prefix = normalizedBase.endsWith("/") ? normalizedBase : normalizedBase + "/";
3161
+ const filtered = /* @__PURE__ */ new Map();
3162
+ for (const [filePath, content] of this.files.entries()) {
3163
+ if (filePath.startsWith(prefix) || filePath === normalizedBase) {
3164
+ filtered.set(filePath, content);
3165
+ }
3166
+ }
3167
+ return filtered;
3168
+ }
3169
+ /** Calculate total size (bytes) and file count under a base path. */
3170
+ getTotalSize(basePath) {
3171
+ const files = this.getAllFiles(basePath);
3172
+ let totalSize = 0;
3173
+ for (const content of files.values()) {
3174
+ totalSize += utf8ByteLength(content);
3175
+ }
3176
+ return { size: totalSize, files: files.size };
3177
+ }
3178
+ /** Normalize path to forward slashes, drop leading ./ and /, no trailing slash. */
3179
+ normalizePath(filePath) {
3180
+ return toRelKey(filePath);
3181
+ }
3182
+ };
3183
+ var HybridFileSystem = class {
3184
+ constructor(files = {}, backend) {
3185
+ this.mem = new MemoryFileSystem(files);
3186
+ this.backend = backend;
3187
+ }
3188
+ /** Add or update an in-memory (synchronously-served) file. */
3189
+ addFile(filePath, content) {
3190
+ this.mem.addFile(filePath, content);
3191
+ }
3192
+ /** Add multiple in-memory files at once. */
3193
+ addFiles(files) {
3194
+ this.mem.addFiles(files);
3195
+ }
3196
+ exists(filePath) {
3197
+ if (this.mem.exists(filePath)) return true;
3198
+ return this.backend.readFile(this.normalize(filePath)).then((content) => content !== null).catch(() => false);
3199
+ }
3200
+ readFile(filePath) {
3201
+ if (this.mem.exists(filePath)) return this.mem.readFile(filePath);
3202
+ return this.backend.readFile(this.normalize(filePath)).then((content) => {
3203
+ if (content === null) throw new Error(`File not found: ${filePath}`);
3204
+ return content;
3205
+ });
3206
+ }
3207
+ isDirectory(filePath) {
3208
+ if (this.mem.isDirectory(filePath)) return true;
3209
+ if (!this.backend.isDirectory) return false;
3210
+ return this.backend.isDirectory(this.normalize(filePath)).catch(() => false);
3211
+ }
3212
+ readdir(dirPath) {
3213
+ const local = this.mem.readdir(dirPath);
3214
+ if (!this.backend.readdir) return local;
3215
+ return this.backend.readdir(this.normalize(dirPath)).then((remote) => Array.from(/* @__PURE__ */ new Set([...local, ...remote]))).catch(() => local);
3216
+ }
3217
+ resolve(...pathSegments) {
3218
+ return this.mem.resolve(...pathSegments);
3219
+ }
3220
+ dirname(filePath) {
3221
+ return this.mem.dirname(filePath);
3222
+ }
3223
+ join(...pathSegments) {
3224
+ return this.mem.join(...pathSegments);
3225
+ }
3226
+ /** Normalize to the same relative keys MemoryFileSystem uses, for backend lookups. */
3227
+ normalize(filePath) {
3228
+ return toRelKey(filePath);
3229
+ }
3230
+ };
3231
+
3232
+ // src/lib/compiler/index.ts
3233
+ function createCoreStages() {
3234
+ return [
3235
+ new LexicalAnalysisStage(),
3236
+ new DependencyResolutionStage(),
3237
+ new SemanticAnalysisStage(),
3238
+ new TemplateProcessingStage(),
3239
+ new CodeGenerationStage()
3240
+ ];
3241
+ }
3242
+ var PrompdCompiler = class {
3243
+ constructor(options = {}) {
3244
+ this.securityConfig = options.securityConfig || DEFAULT_SECURITY_CONFIG;
3245
+ this.pipeline = new CompilerPipeline(options.stages || createCoreStages(), this.securityConfig);
3246
+ }
3247
+ /**
3248
+ * Compile a .prmd source to a string. Throws CompilationError on errors.
3249
+ */
3250
+ async compile(source, options = {}) {
3251
+ const context = await this.pipeline.execute(source, options);
3252
+ if (context.hasErrors()) {
3253
+ const errorMessages = context.errors.join("\n - ");
3254
+ throw new CompilationError(`Compilation failed:
3255
+ - ${errorMessages}`);
3256
+ }
3257
+ const result = context.compiledResult || "";
3258
+ return typeof result === "string" ? result : new TextDecoder().decode(result);
3259
+ }
3260
+ /**
3261
+ * Compile and return the full context (output + diagnostics, no throw).
3262
+ */
3263
+ async compileWithContext(source, options = {}) {
3264
+ return this.pipeline.execute(source, options);
3265
+ }
3266
+ /** Access the underlying pipeline (advanced customization). */
3267
+ getPipeline() {
3268
+ return this.pipeline;
3269
+ }
3270
+ };
3271
+ async function compile(source, outputFormat = "markdown", parameters = {}, options = {}) {
3272
+ const compiler = new PrompdCompiler();
3273
+ return compiler.compile(source, { outputFormat, parameters, ...options });
3274
+ }
3275
+
3276
+ // src/lib/workflowTypes.ts
3277
+ var DOCKABLE_NODE_TYPES = [
3278
+ "tool-call-router",
3279
+ "tool",
3280
+ "callback",
3281
+ "checkpoint",
3282
+ "memory",
3283
+ "error-handler"
3284
+ ];
3285
+ var DOCKABLE_HANDLES = [
3286
+ // Main agent/guardrail nodes - accept callback, checkpoint, error-handler on rejected handle
3287
+ { nodeType: "chat-agent", handleId: "rejected", position: { side: "left", topPercent: 80 }, acceptsTypes: ["callback", "checkpoint", "error-handler"] },
3288
+ { nodeType: "chat-agent", handleId: "memory", position: { side: "right", topPercent: 70 }, acceptsTypes: ["memory"] },
3289
+ { nodeType: "chat-agent", handleId: "onCheckpoint", position: { side: "bottom", topPercent: 100 }, acceptsTypes: ["callback", "checkpoint"] },
3290
+ { nodeType: "guardrail", handleId: "rejected", position: { side: "left", topPercent: 70 }, acceptsTypes: ["callback", "checkpoint", "error-handler"] },
3291
+ { nodeType: "guardrail", handleId: "onCheckpoint", position: { side: "bottom", topPercent: 100 }, acceptsTypes: ["callback", "checkpoint"] },
3292
+ { nodeType: "agent", handleId: "onCheckpoint", position: { side: "bottom", topPercent: 100 }, acceptsTypes: ["callback", "checkpoint"] },
3293
+ { nodeType: "agent", handleId: "ai-output", position: { side: "right", topPercent: 65 }, acceptsTypes: ["tool-call-router"] },
3294
+ { nodeType: "agent", handleId: "toolResult", position: { side: "right", topPercent: 80 }, acceptsTypes: ["tool-call-router"] },
3295
+ { nodeType: "agent", handleId: "memory", position: { side: "left", topPercent: 70 }, acceptsTypes: ["memory"] },
3296
+ // Prompt node - accepts memory for output caching, callback for logging
3297
+ { nodeType: "prompt", handleId: "output", position: { side: "right", topPercent: 50 }, acceptsTypes: ["memory", "callback"] },
3298
+ { nodeType: "prompt", handleId: "onCheckpoint", position: { side: "bottom", topPercent: 100 }, acceptsTypes: ["callback", "checkpoint"] },
3299
+ // Loop node - accepts memory for iteration state, callback for iteration events
3300
+ { nodeType: "loop", handleId: "onIteration", position: { side: "bottom", topPercent: 100 }, acceptsTypes: ["memory", "callback"] },
3301
+ // Condition node - accepts callback for branch logging
3302
+ { nodeType: "condition", handleId: "onEvaluate", position: { side: "bottom", topPercent: 100 }, acceptsTypes: ["callback"] },
3303
+ // Code node - accepts memory for state, callback for logging
3304
+ { nodeType: "code", handleId: "onExecute", position: { side: "bottom", topPercent: 100 }, acceptsTypes: ["memory", "callback"] },
3305
+ // Transformer node - accepts callback for transform logging
3306
+ { nodeType: "transformer", handleId: "onTransform", position: { side: "bottom", topPercent: 100 }, acceptsTypes: ["callback"] },
3307
+ // Tool Call Router node - accepts tool nodes for docking
3308
+ { nodeType: "tool-call-router", handleId: "ai-input", position: { side: "left", topPercent: 35 }, acceptsTypes: ["tool"] },
3309
+ // Tool Call Router node - accepts tool nodes for docking
3310
+ { nodeType: "tool-call-router", handleId: "toolResult", position: { side: "left", topPercent: 65 }, acceptsTypes: ["tool"] },
3311
+ // Loop node - accepts tool-call-router nodes for container-to-container docking
3312
+ { nodeType: "loop", handleId: "ai-output", position: { side: "left", topPercent: 50 }, acceptsTypes: ["tool-call-router"] },
3313
+ // Loop node - accepts tool-call-router nodes for container-to-container docking
3314
+ { nodeType: "loop", handleId: "toolResult", position: { side: "left", topPercent: 50 }, acceptsTypes: ["tool-call-router"] }
3315
+ ];
3316
+ var MEMORY_OPERATIONS_BY_MODE = {
3317
+ kv: ["get", "set", "delete", "list", "clear"],
3318
+ conversation: ["get", "append", "clear"],
3319
+ cache: ["get", "set", "delete", "clear"]
3320
+ };
3321
+ var BUILTIN_COMMAND_EXECUTABLES = [
3322
+ { executable: "npm", description: "Node.js package manager", actions: ["run", "install", "test", "build", "start"] },
3323
+ { executable: "npx", description: "Execute npm packages", actions: [] },
3324
+ { executable: "node", description: "Node.js runtime", actions: [] },
3325
+ { executable: "yarn", description: "Yarn package manager", actions: ["run", "install", "test", "build", "start"] },
3326
+ { executable: "pnpm", description: "PNPM package manager", actions: ["run", "install", "test", "build", "start"] },
3327
+ { executable: "git", description: "Version control", actions: ["status", "add", "commit", "push", "pull", "log", "diff", "branch"] },
3328
+ { executable: "python", description: "Python interpreter", actions: [] },
3329
+ { executable: "python3", description: "Python 3 interpreter", actions: [] },
3330
+ { executable: "pip", description: "Python package manager", actions: ["install", "list", "show"] },
3331
+ { executable: "prompd", description: "Prompd CLI", actions: ["compile", "run", "validate", "package"] },
3332
+ { executable: "dotnet", description: ".NET CLI", actions: ["build", "run", "test", "publish"] },
3333
+ { executable: "tsc", description: "TypeScript compiler", actions: [] },
3334
+ { executable: "eslint", description: "JavaScript linter", actions: [] },
3335
+ { executable: "prettier", description: "Code formatter", actions: [] },
3336
+ { executable: "echo", description: "Print text", actions: [] }
3337
+ ];
3338
+
3339
+ // src/lib/workflowValidator.ts
3340
+ function validateWorkflow(workflow) {
3341
+ const errors = [];
3342
+ const warnings = [];
3343
+ if (!workflow.nodes || workflow.nodes.length === 0) {
3344
+ errors.push({
3345
+ message: "Workflow is empty. Drag a node from the Node Palette on the left to get started.",
3346
+ code: "EMPTY_WORKFLOW"
3347
+ });
3348
+ return { errors, warnings, isValid: false };
3349
+ }
3350
+ for (const node of workflow.nodes) {
3351
+ const nodeErrors = validateNode(node, workflow);
3352
+ errors.push(...nodeErrors);
3353
+ }
3354
+ if (workflow.edges) {
3355
+ for (const connection of workflow.edges) {
3356
+ const connectionErrors = validateConnection(connection, workflow);
3357
+ errors.push(...connectionErrors);
3358
+ }
3359
+ }
3360
+ const disconnectedNodes = findDisconnectedNodes(workflow);
3361
+ for (const nodeId of disconnectedNodes) {
3362
+ const node = workflow.nodes.find((n) => n.id === nodeId);
3363
+ const nodeLabel = node?.data?.label || nodeId;
3364
+ warnings.push({
3365
+ nodeId,
3366
+ message: `Node '${nodeLabel}' is not connected. Connect it to other nodes to include it in the workflow.`,
3367
+ code: "DISCONNECTED_NODE"
3368
+ });
3369
+ }
3370
+ const cycles = detectCircularDependencies(workflow);
3371
+ for (const cycle of cycles) {
3372
+ errors.push({
3373
+ message: `Circular dependency detected: ${cycle.join(" \u2192 ")}`,
3374
+ code: "CIRCULAR_DEPENDENCY"
3375
+ });
3376
+ }
3377
+ const triggerCount = workflow.nodes.filter((n) => n.type === "trigger").length;
3378
+ if (triggerCount === 0) {
3379
+ warnings.push({
3380
+ message: "Workflow has no trigger node. Add a trigger to define when the workflow should run.",
3381
+ code: "NO_TRIGGER"
3382
+ });
3383
+ }
3384
+ const outputCount = workflow.nodes.filter((n) => n.type === "output").length;
3385
+ if (outputCount === 0) {
3386
+ warnings.push({
3387
+ message: "Workflow has no output node. Add an output node to capture workflow results.",
3388
+ code: "NO_OUTPUT"
3389
+ });
3390
+ }
3391
+ return {
3392
+ errors,
3393
+ warnings,
3394
+ isValid: errors.length === 0
3395
+ };
3396
+ }
3397
+ function validateNode(node, workflow) {
3398
+ const errors = [];
3399
+ if (!node.id) {
3400
+ errors.push({
3401
+ nodeId: node.id,
3402
+ message: "Node is missing required field: id",
3403
+ code: "MISSING_NODE_ID"
3404
+ });
3405
+ }
3406
+ if (!node.type) {
3407
+ errors.push({
3408
+ nodeId: node.id,
3409
+ message: "Node is missing required field: type",
3410
+ code: "MISSING_NODE_TYPE"
3411
+ });
3412
+ }
3413
+ switch (node.type) {
3414
+ case "trigger":
3415
+ errors.push(...validateTriggerNode(node));
3416
+ break;
3417
+ case "prompt":
3418
+ errors.push(...validatePromptNode(node));
3419
+ break;
3420
+ case "condition":
3421
+ errors.push(...validateConditionNode(node));
3422
+ break;
3423
+ case "loop":
3424
+ errors.push(...validateLoopNode(node));
3425
+ break;
3426
+ case "agent":
3427
+ errors.push(...validateAgentNode(node));
3428
+ break;
3429
+ }
3430
+ if (["loop", "parallel", "tool-call-router", "chat-agent"].includes(node.type)) {
3431
+ const children = workflow.nodes.filter((n) => n.parentId === node.id);
3432
+ if (children.length === 0 && node.type !== "chat-agent") {
3433
+ const containerTypeHelp = {
3434
+ "loop": "Drag nodes inside this loop container to define the loop body.",
3435
+ "parallel": "Drag nodes inside this parallel container to run them concurrently.",
3436
+ "tool-call-router": "Drag tool nodes inside this router to handle different tool calls."
3437
+ };
3438
+ errors.push({
3439
+ nodeId: node.id,
3440
+ message: `Container '${node.type}' (${node.data?.label || node.id}) is empty. ${containerTypeHelp[node.type] || "Drag nodes inside this container."}`,
3441
+ code: "EMPTY_CONTAINER"
3442
+ });
3443
+ }
3444
+ }
3445
+ return errors;
3446
+ }
3447
+ function validateTriggerNode(node) {
3448
+ const errors = [];
3449
+ const data = node.data;
3450
+ if (!data.triggerType) {
3451
+ errors.push({
3452
+ nodeId: node.id,
3453
+ field: "triggerType",
3454
+ message: `Trigger '${data.label || node.id}' is missing a trigger type. Select Manual, Schedule, Webhook, or Event in the properties panel.`,
3455
+ code: "MISSING_TRIGGER_TYPE"
3456
+ });
3457
+ }
3458
+ if (data.triggerType === "schedule" && !data.schedule && !data.scheduleCron) {
3459
+ errors.push({
3460
+ nodeId: node.id,
3461
+ field: "schedule",
3462
+ message: `Schedule trigger '${data.label || node.id}' needs a schedule. Enter a cron expression in the properties panel.`,
3463
+ code: "MISSING_SCHEDULE"
3464
+ });
3465
+ }
3466
+ if (data.triggerType === "webhook" && !data.webhookPath) {
3467
+ errors.push({
3468
+ nodeId: node.id,
3469
+ field: "webhookPath",
3470
+ message: `Webhook trigger '${data.label || node.id}' needs a webhook path. Enter a URL path (e.g., /api/trigger) in the properties panel.`,
3471
+ code: "MISSING_WEBHOOK_PATH"
3472
+ });
3473
+ }
3474
+ return errors;
3475
+ }
3476
+ function validatePromptNode(node) {
3477
+ const errors = [];
3478
+ const data = node.data;
3479
+ if (!data.source && !data.content && !data.rawPrompt) {
3480
+ errors.push({
3481
+ nodeId: node.id,
3482
+ message: `Prompt '${data.label || node.id}' has no content. Either select a .prmd file or enter prompt text in the properties panel.`,
3483
+ code: "MISSING_PROMPT_CONTENT"
3484
+ });
3485
+ }
3486
+ return errors;
3487
+ }
3488
+ function validateConditionNode(node) {
3489
+ const errors = [];
3490
+ const data = node.data;
3491
+ if (!data.conditions || data.conditions.length === 0) {
3492
+ errors.push({
3493
+ nodeId: node.id,
3494
+ field: "conditions",
3495
+ message: `Condition '${data.label || node.id}' has no conditions. Add at least one condition in the properties panel.`,
3496
+ code: "MISSING_CONDITIONS"
3497
+ });
3498
+ }
3499
+ return errors;
3500
+ }
3501
+ function validateLoopNode(node) {
3502
+ const errors = [];
3503
+ const data = node.data;
3504
+ if (!data.loopType) {
3505
+ errors.push({
3506
+ nodeId: node.id,
3507
+ field: "loopType",
3508
+ message: `Loop '${data.label || node.id}' is missing a loop type. Select While, For-Each, or Count in the properties panel.`,
3509
+ code: "MISSING_LOOP_TYPE"
3510
+ });
3511
+ }
3512
+ if (data.loopType === "while" && !data.condition) {
3513
+ errors.push({
3514
+ nodeId: node.id,
3515
+ field: "condition",
3516
+ message: `While loop '${data.label || node.id}' needs a condition. Enter a condition expression in the properties panel.`,
3517
+ code: "MISSING_LOOP_CONDITION"
3518
+ });
3519
+ }
3520
+ if (data.loopType === "for-each" && !data.arraySource) {
3521
+ errors.push({
3522
+ nodeId: node.id,
3523
+ field: "arraySource",
3524
+ message: `For-Each loop '${data.label || node.id}' needs an array source. Specify the array variable in the properties panel.`,
3525
+ code: "MISSING_ARRAY_SOURCE"
3526
+ });
3527
+ }
3528
+ if (data.loopType === "count" && !data.count) {
3529
+ errors.push({
3530
+ nodeId: node.id,
3531
+ field: "count",
3532
+ message: `Count loop '${data.label || node.id}' needs a count value. Enter the number of iterations in the properties panel.`,
3533
+ code: "MISSING_COUNT"
3534
+ });
3535
+ }
3536
+ return errors;
3537
+ }
3538
+ function validateAgentNode(node) {
3539
+ const errors = [];
3540
+ const data = node.data;
3541
+ if (!data.model && !data.provider && !data.providerNodeId) {
3542
+ errors.push({
3543
+ nodeId: node.id,
3544
+ message: `Agent '${data.label || node.id}' has no model configured. Select an LLM provider and model in the properties panel, or connect to a Provider node.`,
3545
+ code: "MISSING_AGENT_MODEL"
3546
+ });
3547
+ }
3548
+ return errors;
3549
+ }
3550
+ function validateConnection(connection, workflow) {
3551
+ const errors = [];
3552
+ const sourceNode = workflow.nodes.find((n) => n.id === connection.source);
3553
+ if (!sourceNode) {
3554
+ errors.push({
3555
+ connectionId: `${connection.source}-${connection.target}`,
3556
+ message: `Invalid connection: source node '${connection.source}' no longer exists. Delete this connection.`,
3557
+ code: "INVALID_SOURCE_NODE"
3558
+ });
3559
+ }
3560
+ const targetNode = workflow.nodes.find((n) => n.id === connection.target);
3561
+ if (!targetNode) {
3562
+ errors.push({
3563
+ connectionId: `${connection.source}-${connection.target}`,
3564
+ message: `Invalid connection: target node '${connection.target}' no longer exists. Delete this connection.`,
3565
+ code: "INVALID_TARGET_NODE"
3566
+ });
3567
+ }
3568
+ if (connection.source === connection.target) {
3569
+ const selfNode = workflow.nodes.find((n) => n.id === connection.source);
3570
+ const nodeLabel = selfNode?.data?.label || connection.source;
3571
+ errors.push({
3572
+ connectionId: `${connection.source}-${connection.target}`,
3573
+ message: `Node '${nodeLabel}' cannot connect to itself. Remove this connection.`,
3574
+ code: "SELF_CONNECTION"
3575
+ });
3576
+ }
3577
+ return errors;
3578
+ }
3579
+ function findDisconnectedNodes(workflow) {
3580
+ const connectedNodes = /* @__PURE__ */ new Set();
3581
+ if (workflow.edges) {
3582
+ for (const connection of workflow.edges) {
3583
+ connectedNodes.add(connection.source);
3584
+ connectedNodes.add(connection.target);
3585
+ }
3586
+ }
3587
+ return workflow.nodes.filter((node) => {
3588
+ if (node.type === "trigger" || node.type === "output") {
3589
+ return false;
3590
+ }
3591
+ return !connectedNodes.has(node.id);
3592
+ }).map((node) => node.id);
3593
+ }
3594
+ function detectCircularDependencies(workflow) {
3595
+ const cycles = [];
3596
+ const visited = /* @__PURE__ */ new Set();
3597
+ const recursionStack = /* @__PURE__ */ new Set();
3598
+ const graph = /* @__PURE__ */ new Map();
3599
+ for (const node of workflow.nodes) {
3600
+ graph.set(node.id, []);
3601
+ }
3602
+ if (workflow.edges) {
3603
+ for (const connection of workflow.edges) {
3604
+ const isDocked = connection.id?.startsWith("docked-") || connection.sourceHandle === "ai-output" || connection.targetHandle === "ai-input" || connection.targetHandle === "toolResult";
3605
+ const sourceNode = workflow.nodes.find((n) => n.id === connection.source);
3606
+ const isLoopEdge = sourceNode?.type === "loop" && (connection.sourceHandle === "loop-start" || connection.sourceHandle === "loop-end");
3607
+ if (isDocked || isLoopEdge) {
3608
+ continue;
3609
+ }
3610
+ const targets = graph.get(connection.source) || [];
3611
+ targets.push(connection.target);
3612
+ graph.set(connection.source, targets);
3613
+ }
3614
+ }
3615
+ function dfs(nodeId, path) {
3616
+ visited.add(nodeId);
3617
+ recursionStack.add(nodeId);
3618
+ path.push(nodeId);
3619
+ const neighbors = graph.get(nodeId) || [];
3620
+ for (const neighbor of neighbors) {
3621
+ if (!visited.has(neighbor)) {
3622
+ dfs(neighbor, [...path]);
3623
+ } else if (recursionStack.has(neighbor)) {
3624
+ const cycleStart = path.indexOf(neighbor);
3625
+ if (cycleStart !== -1) {
3626
+ cycles.push([...path.slice(cycleStart), neighbor]);
3627
+ }
3628
+ }
3629
+ }
3630
+ recursionStack.delete(nodeId);
3631
+ }
3632
+ for (const node of workflow.nodes) {
3633
+ if (!visited.has(node.id)) {
3634
+ dfs(node.id, []);
3635
+ }
3636
+ }
3637
+ return cycles;
3638
+ }
3639
+ function validateWorkflowQuick(workflow) {
3640
+ if (!workflow.nodes || workflow.nodes.length === 0) {
3641
+ return { isValid: false };
3642
+ }
3643
+ for (const node of workflow.nodes) {
3644
+ if (!node.id || !node.type) {
3645
+ return { isValid: false };
3646
+ }
3647
+ }
3648
+ return { isValid: true };
3649
+ }
3650
+
3651
+ // src/lib/workflowParser.ts
3652
+ var VALID_NODE_TYPES = [
3653
+ "trigger",
3654
+ "prompt",
3655
+ "provider",
3656
+ "condition",
3657
+ "loop",
3658
+ "parallel",
3659
+ "merge",
3660
+ "transformer",
3661
+ "api",
3662
+ "tool",
3663
+ "tool-call-parser",
3664
+ "tool-call-router",
3665
+ "agent",
3666
+ "chat-agent",
3667
+ // Composite chat agent with guardrail
3668
+ "guardrail",
3669
+ // Input validation node
3670
+ "callback",
3671
+ "checkpoint",
3672
+ // Alias for callback
3673
+ "user-input",
3674
+ "error-handler",
3675
+ "command",
3676
+ // Phase E: Shell command execution
3677
+ "code",
3678
+ // Phase E: Custom code execution
3679
+ "claude-code",
3680
+ // Phase E: Claude Code agent with SSH
3681
+ "workflow",
3682
+ // Phase E: Sub-workflow invocation
3683
+ "mcp-tool",
3684
+ // Phase E: External MCP tool execution
3685
+ "memory",
3686
+ // Memory/storage operations
3687
+ "output"
3688
+ ];
3689
+ var INTERNAL_HANDLES = ["loop-start", "loop-end", "parallel-start", "parallel-end"];
3690
+ function shouldEdgeBeAnimated(sourceHandle) {
3691
+ if (!sourceHandle) return false;
3692
+ if (INTERNAL_HANDLES.includes(sourceHandle)) return true;
3693
+ if (sourceHandle.startsWith("condition-") || sourceHandle === "default") return true;
3694
+ return false;
3695
+ }
3696
+ function parseWorkflow(json) {
3697
+ const errors = [];
3698
+ const warnings = [];
3699
+ let file;
3700
+ try {
3701
+ file = JSON.parse(json);
3702
+ } catch (e) {
3703
+ return {
3704
+ file: createEmptyWorkflow(),
3705
+ nodes: [],
3706
+ edges: [],
3707
+ errors: [{
3708
+ message: `Invalid JSON: ${e instanceof Error ? e.message : "Parse error"}`,
3709
+ code: "INVALID_JSON"
3710
+ }],
3711
+ warnings: []
3712
+ };
3713
+ }
3714
+ validateWorkflowStructure(file, errors, warnings);
3715
+ const nodes = convertNodesToReactFlow(file.nodes || []);
3716
+ const edges = normalizeEdges(file.edges || []);
3717
+ validateDataFlow(file, errors, warnings);
3718
+ const validationResult = validateWorkflow(file);
3719
+ errors.push(...validationResult.errors);
3720
+ warnings.push(...validationResult.warnings);
3721
+ return {
3722
+ file,
3723
+ nodes,
3724
+ edges,
3725
+ errors,
3726
+ warnings
3727
+ };
3728
+ }
3729
+ function serializeWorkflow(file, nodes, edges) {
3730
+ const updatedNodes = file.nodes.map((node) => {
3731
+ const rfNode = nodes.find((n) => n.id === node.id);
3732
+ if (rfNode) {
3733
+ const nodeData = rfNode.data;
3734
+ let position = rfNode.position;
3735
+ if (nodeData.dockedTo && nodeData._preDockPosition) {
3736
+ position = nodeData._preDockPosition;
3737
+ }
3738
+ const updated = {
3739
+ ...node,
3740
+ position,
3741
+ // Sync node data from React Flow (includes _savedWidth, _savedHeight, collapsed, etc.)
3742
+ data: nodeData
3743
+ };
3744
+ if (rfNode.parentId) {
3745
+ updated.parentId = rfNode.parentId;
3746
+ updated.extent = rfNode.extent || "parent";
3747
+ } else {
3748
+ delete updated.parentId;
3749
+ delete updated.extent;
3750
+ }
3751
+ if (rfNode.width && !nodeData.dockedTo) {
3752
+ updated.width = rfNode.width;
3753
+ } else if (nodeData.dockedTo && nodeData._preDockWidth) {
3754
+ updated.width = nodeData._preDockWidth;
3755
+ }
3756
+ if (rfNode.height && !nodeData.dockedTo) {
3757
+ updated.height = rfNode.height;
3758
+ } else if (nodeData.dockedTo && nodeData._preDockHeight) {
3759
+ updated.height = nodeData._preDockHeight;
3760
+ }
3761
+ return updated;
3762
+ }
3763
+ return node;
3764
+ });
3765
+ const workflowEdges = edges.map((edge) => ({
3766
+ id: edge.id,
3767
+ source: edge.source,
3768
+ target: edge.target,
3769
+ sourceHandle: edge.sourceHandle ?? void 0,
3770
+ targetHandle: edge.targetHandle ?? void 0,
3771
+ animated: edge.animated,
3772
+ label: typeof edge.label === "string" ? edge.label : void 0
3773
+ }));
3774
+ const updatedFile = {
3775
+ ...file,
3776
+ nodes: updatedNodes,
3777
+ edges: workflowEdges
3778
+ };
3779
+ return JSON.stringify(updatedFile, null, 2);
3780
+ }
3781
+ function createEmptyWorkflow() {
3782
+ return {
3783
+ version: "1.0",
3784
+ metadata: {
3785
+ id: `workflow-${Date.now()}`,
3786
+ name: "New Workflow",
3787
+ description: ""
3788
+ },
3789
+ parameters: [],
3790
+ nodes: [],
3791
+ edges: []
3792
+ };
3793
+ }
3794
+ function createWorkflowNode(type, position, id) {
3795
+ const nodeId = id || `${type}-${Date.now()}`;
3796
+ const baseData = {
3797
+ label: getDefaultLabel(type)
3798
+ };
3799
+ switch (type) {
3800
+ case "trigger":
3801
+ return {
3802
+ id: nodeId,
3803
+ type,
3804
+ position,
3805
+ data: {
3806
+ ...baseData,
3807
+ triggerType: "manual"
3808
+ }
3809
+ };
3810
+ case "prompt":
3811
+ return {
3812
+ id: nodeId,
3813
+ type,
3814
+ position,
3815
+ data: {
3816
+ ...baseData,
3817
+ source: "",
3818
+ provider: "openai",
3819
+ model: "gpt-4o",
3820
+ parameters: {}
3821
+ }
3822
+ };
3823
+ case "provider":
3824
+ return {
3825
+ id: nodeId,
3826
+ type,
3827
+ position,
3828
+ data: {
3829
+ ...baseData,
3830
+ providerId: "openai",
3831
+ model: "gpt-4o"
3832
+ }
3833
+ };
3834
+ case "condition":
3835
+ return {
3836
+ id: nodeId,
3837
+ type,
3838
+ position,
3839
+ data: {
3840
+ ...baseData,
3841
+ conditions: [],
3842
+ default: void 0
3843
+ }
3844
+ };
3845
+ case "loop":
3846
+ return {
3847
+ id: nodeId,
3848
+ type,
3849
+ position,
3850
+ width: 300,
3851
+ height: 200,
3852
+ data: {
3853
+ ...baseData,
3854
+ loopType: "while",
3855
+ condition: "",
3856
+ maxIterations: 10,
3857
+ body: []
3858
+ }
3859
+ };
3860
+ case "parallel":
3861
+ return {
3862
+ id: nodeId,
3863
+ type,
3864
+ position,
3865
+ width: 350,
3866
+ height: 200,
3867
+ data: {
3868
+ ...baseData,
3869
+ mode: "broadcast",
3870
+ forkCount: 2,
3871
+ branches: [],
3872
+ waitFor: "all",
3873
+ mergeStrategy: "object"
3874
+ }
3875
+ };
3876
+ case "merge":
3877
+ return {
3878
+ id: nodeId,
3879
+ type,
3880
+ position,
3881
+ data: {
3882
+ ...baseData,
3883
+ inputs: [],
3884
+ mergeAs: "object"
3885
+ }
3886
+ };
3887
+ case "api":
3888
+ return {
3889
+ id: nodeId,
3890
+ type,
3891
+ position,
3892
+ data: {
3893
+ ...baseData,
3894
+ method: "GET",
3895
+ url: ""
3896
+ }
3897
+ };
3898
+ case "callback":
3899
+ case "checkpoint":
3900
+ return {
3901
+ id: nodeId,
3902
+ type: "callback",
3903
+ position,
3904
+ data: {
3905
+ ...baseData,
3906
+ mode: "report",
3907
+ checkpointName: "",
3908
+ includePreviousOutput: true,
3909
+ includeNextNodeInfo: true,
3910
+ waitForAck: false
3911
+ }
3912
+ };
3913
+ case "user-input":
3914
+ return {
3915
+ id: nodeId,
3916
+ type,
3917
+ position,
3918
+ data: {
3919
+ ...baseData,
3920
+ prompt: "Enter your input:",
3921
+ inputType: "text",
3922
+ required: true,
3923
+ showContext: true
3924
+ }
3925
+ };
3926
+ case "tool":
3927
+ return {
3928
+ id: nodeId,
3929
+ type,
3930
+ position,
3931
+ data: {
3932
+ ...baseData,
3933
+ toolType: "function",
3934
+ toolName: "",
3935
+ parameters: {}
3936
+ }
3937
+ };
3938
+ case "tool-call-parser":
3939
+ return {
3940
+ id: nodeId,
3941
+ type,
3942
+ position,
3943
+ data: {
3944
+ ...baseData,
3945
+ format: "auto",
3946
+ noToolCallBehavior: "passthrough",
3947
+ allowedTools: []
3948
+ }
3949
+ };
3950
+ case "agent":
3951
+ return {
3952
+ id: nodeId,
3953
+ type,
3954
+ position,
3955
+ data: {
3956
+ ...baseData,
3957
+ systemPrompt: "You are a helpful AI assistant with access to tools. Use the available tools to complete the user's request.",
3958
+ userPrompt: "{{ input }}",
3959
+ tools: [],
3960
+ maxIterations: 10,
3961
+ toolCallFormat: "auto",
3962
+ outputMode: "final-response",
3963
+ includeHistory: true
3964
+ }
3965
+ };
3966
+ case "chat-agent":
3967
+ return {
3968
+ id: nodeId,
3969
+ type,
3970
+ position,
3971
+ width: 400,
3972
+ height: 320,
3973
+ data: {
3974
+ ...baseData,
3975
+ // Agent configuration
3976
+ agentSystemPrompt: "You are a helpful AI assistant.",
3977
+ agentUserPrompt: "{{ input }}",
3978
+ maxIterations: 10,
3979
+ toolCallFormat: "auto",
3980
+ outputMode: "final-response",
3981
+ // Guardrail configuration (disabled by default)
3982
+ guardrailEnabled: false,
3983
+ // User input configuration (enabled by default)
3984
+ userInputEnabled: true,
3985
+ userInputPrompt: "Enter your message:",
3986
+ userInputType: "textarea",
3987
+ // Container state
3988
+ collapsed: true,
3989
+ // Saved dimensions for when expanded
3990
+ _savedWidth: 400,
3991
+ _savedHeight: 320,
3992
+ tools: []
3993
+ }
3994
+ };
3995
+ case "guardrail":
3996
+ return {
3997
+ id: nodeId,
3998
+ type,
3999
+ position,
4000
+ data: {
4001
+ ...baseData,
4002
+ systemPrompt: 'Validate the input. Respond with "PASS" if the input is appropriate and safe, or "REJECT" with a reason if it violates guidelines.',
4003
+ scoreThreshold: 0.5
4004
+ }
4005
+ };
4006
+ case "tool-call-router":
4007
+ return {
4008
+ id: nodeId,
4009
+ type,
4010
+ position,
4011
+ width: 320,
4012
+ height: 180,
4013
+ data: {
4014
+ ...baseData,
4015
+ routingMode: "name-match",
4016
+ onNoMatch: "error",
4017
+ collapsed: false
4018
+ }
4019
+ };
4020
+ case "error-handler":
4021
+ return {
4022
+ id: nodeId,
4023
+ type,
4024
+ position,
4025
+ data: {
4026
+ ...baseData,
4027
+ strategy: "retry",
4028
+ retry: {
4029
+ maxAttempts: 3,
4030
+ backoffMs: 1e3,
4031
+ backoffMultiplier: 2
4032
+ }
4033
+ }
4034
+ };
4035
+ case "output":
4036
+ return {
4037
+ id: nodeId,
4038
+ type,
4039
+ position,
4040
+ data: {
4041
+ ...baseData
4042
+ }
4043
+ };
4044
+ case "command":
4045
+ return {
4046
+ id: nodeId,
4047
+ type,
4048
+ position,
4049
+ data: {
4050
+ ...baseData,
4051
+ command: "",
4052
+ args: [],
4053
+ cwd: "",
4054
+ env: {},
4055
+ timeoutMs: 3e4,
4056
+ outputFormat: "text",
4057
+ requiresApproval: true
4058
+ }
4059
+ };
4060
+ case "claude-code":
4061
+ return {
4062
+ id: nodeId,
4063
+ type,
4064
+ position,
4065
+ data: {
4066
+ ...baseData,
4067
+ connection: {
4068
+ type: "local"
4069
+ },
4070
+ task: {
4071
+ prompt: "",
4072
+ workingDirectory: ""
4073
+ },
4074
+ constraints: {
4075
+ maxTurns: 50,
4076
+ allowedTools: ["read", "write", "execute", "web"],
4077
+ requireApprovalForWrites: false
4078
+ },
4079
+ output: {
4080
+ format: "final-response",
4081
+ includeMetadata: false
4082
+ }
4083
+ }
4084
+ };
4085
+ case "workflow":
4086
+ return {
4087
+ id: nodeId,
4088
+ type,
4089
+ position,
4090
+ data: {
4091
+ ...baseData,
4092
+ source: "",
4093
+ parameters: {},
4094
+ outputMapping: {},
4095
+ inheritVariables: false
4096
+ }
4097
+ };
4098
+ case "mcp-tool":
4099
+ return {
4100
+ id: nodeId,
4101
+ type,
4102
+ position,
4103
+ data: {
4104
+ ...baseData,
4105
+ toolName: "",
4106
+ parameters: {},
4107
+ timeoutMs: 3e4,
4108
+ includeInContext: false
4109
+ }
4110
+ };
4111
+ case "code":
4112
+ return {
4113
+ id: nodeId,
4114
+ type,
4115
+ position,
4116
+ data: {
4117
+ ...baseData,
4118
+ language: "typescript",
4119
+ code: "",
4120
+ inputVariable: "input",
4121
+ executionContext: "isolated"
4122
+ }
4123
+ };
4124
+ case "transformer":
4125
+ return {
4126
+ id: nodeId,
4127
+ type,
4128
+ position,
4129
+ data: {
4130
+ ...baseData,
4131
+ mode: "template",
4132
+ template: "",
4133
+ inputVariable: "input",
4134
+ passthroughOnError: false
4135
+ }
4136
+ };
4137
+ case "memory":
4138
+ return {
4139
+ id: nodeId,
4140
+ type,
4141
+ position,
4142
+ data: {
4143
+ ...baseData,
4144
+ mode: "kv",
4145
+ operations: ["get"],
4146
+ // Multi-action array (replaces single 'operation')
4147
+ scope: "execution"
4148
+ }
4149
+ };
4150
+ default:
4151
+ console.error(
4152
+ `[createWorkflowNode] MISSING CASE for node type: "${type}". Add a case for '${type}' in workflowParser.ts createWorkflowNode() switch statement!`
4153
+ );
4154
+ return {
4155
+ id: nodeId,
4156
+ type: "tool",
4157
+ position,
4158
+ data: {
4159
+ ...baseData,
4160
+ label: `UNKNOWN: ${type}`,
4161
+ // Make it obvious this is a fallback
4162
+ toolType: "function",
4163
+ toolName: "",
4164
+ parameters: {}
4165
+ }
4166
+ };
4167
+ }
4168
+ }
4169
+ function getDefaultLabel(type) {
4170
+ const labels = {
4171
+ trigger: "Start",
4172
+ prompt: "Prompt",
4173
+ provider: "Provider",
4174
+ condition: "Condition",
4175
+ loop: "Loop",
4176
+ parallel: "Parallel",
4177
+ merge: "Merge",
4178
+ transformer: "Transform",
4179
+ api: "API Call",
4180
+ tool: "Tool",
4181
+ "tool-call-parser": "Tool Parser",
4182
+ "tool-call-router": "Tool Router",
4183
+ agent: "AI Agent",
4184
+ "chat-agent": "Chat Agent",
4185
+ guardrail: "Guardrail",
4186
+ callback: "Checkpoint",
4187
+ checkpoint: "Checkpoint",
4188
+ // Alias for callback
4189
+ "user-input": "User Input",
4190
+ "error-handler": "Error Handler",
4191
+ command: "Command",
4192
+ code: "Code",
4193
+ "claude-code": "Claude Code",
4194
+ workflow: "Sub-Workflow",
4195
+ "mcp-tool": "MCP Tool",
4196
+ memory: "Memory",
4197
+ output: "Output",
4198
+ "web-search": "Web Search",
4199
+ "database-query": "DB Query"
4200
+ // --- Add new node type labels here ---
4201
+ };
4202
+ return labels[type] || "Node";
4203
+ }
4204
+ function validateWorkflowStructure(file, errors, warnings) {
4205
+ if (!file.version) {
4206
+ warnings.push({
4207
+ message: "Missing version field, defaulting to 1.0",
4208
+ code: "MISSING_VERSION"
4209
+ });
4210
+ }
4211
+ if (!file.metadata) {
4212
+ errors.push({
4213
+ message: "Missing metadata section",
4214
+ code: "MISSING_METADATA"
4215
+ });
4216
+ } else {
4217
+ if (!file.metadata.id) {
4218
+ errors.push({
4219
+ field: "metadata.id",
4220
+ message: "Missing workflow ID",
4221
+ code: "MISSING_ID"
4222
+ });
4223
+ }
4224
+ if (!file.metadata.name) {
4225
+ warnings.push({
4226
+ message: "Missing workflow name",
4227
+ code: "MISSING_NAME"
4228
+ });
4229
+ }
4230
+ }
4231
+ if (!file.nodes || !Array.isArray(file.nodes)) {
4232
+ errors.push({
4233
+ message: "Missing or invalid nodes array",
4234
+ code: "INVALID_NODES"
4235
+ });
4236
+ } else {
4237
+ const nodeIds = /* @__PURE__ */ new Set();
4238
+ for (const node of file.nodes) {
4239
+ if (nodeIds.has(node.id)) {
4240
+ errors.push({
4241
+ nodeId: node.id,
4242
+ message: `Duplicate node ID: ${node.id}`,
4243
+ code: "DUPLICATE_NODE_ID"
4244
+ });
4245
+ }
4246
+ nodeIds.add(node.id);
4247
+ if (!VALID_NODE_TYPES.includes(node.type)) {
4248
+ errors.push({
4249
+ nodeId: node.id,
4250
+ message: `Invalid node type: ${node.type}`,
4251
+ code: "INVALID_NODE_TYPE"
4252
+ });
4253
+ }
4254
+ if (!node.position || typeof node.position.x !== "number" || typeof node.position.y !== "number") {
4255
+ errors.push({
4256
+ nodeId: node.id,
4257
+ message: "Invalid or missing position",
4258
+ code: "INVALID_POSITION"
4259
+ });
4260
+ }
4261
+ validateNodeData(node, errors, warnings);
4262
+ }
4263
+ }
4264
+ if (file.edges && !Array.isArray(file.edges)) {
4265
+ errors.push({
4266
+ message: "Invalid edges array",
4267
+ code: "INVALID_EDGES"
4268
+ });
4269
+ }
4270
+ }
4271
+ function validateNodeData(node, errors, warnings) {
4272
+ if (!node.data) {
4273
+ errors.push({
4274
+ nodeId: node.id,
4275
+ message: "Missing node data",
4276
+ code: "MISSING_NODE_DATA"
4277
+ });
4278
+ return;
4279
+ }
4280
+ switch (node.type) {
4281
+ case "prompt":
4282
+ if (!("source" in node.data)) {
4283
+ errors.push({
4284
+ nodeId: node.id,
4285
+ field: "source",
4286
+ message: "Prompt node missing source",
4287
+ code: "MISSING_PROMPT_SOURCE"
4288
+ });
4289
+ }
4290
+ break;
4291
+ case "condition":
4292
+ if (!("conditions" in node.data) || !Array.isArray(node.data.conditions)) {
4293
+ errors.push({
4294
+ nodeId: node.id,
4295
+ field: "conditions",
4296
+ message: "Condition node missing conditions array",
4297
+ code: "MISSING_CONDITIONS"
4298
+ });
4299
+ }
4300
+ break;
4301
+ case "loop":
4302
+ if (!("loopType" in node.data)) {
4303
+ errors.push({
4304
+ nodeId: node.id,
4305
+ field: "loopType",
4306
+ message: "Loop node missing loopType",
4307
+ code: "MISSING_LOOP_TYPE"
4308
+ });
4309
+ }
4310
+ if (!("maxIterations" in node.data) || typeof node.data.maxIterations !== "number") {
4311
+ warnings.push({
4312
+ nodeId: node.id,
4313
+ message: "Loop node missing maxIterations, defaulting to 10",
4314
+ code: "MISSING_MAX_ITERATIONS"
4315
+ });
4316
+ }
4317
+ break;
4318
+ case "parallel":
4319
+ if (!("branches" in node.data) || !Array.isArray(node.data.branches)) {
4320
+ errors.push({
4321
+ nodeId: node.id,
4322
+ field: "branches",
4323
+ message: "Parallel node missing branches array",
4324
+ code: "MISSING_BRANCHES"
4325
+ });
4326
+ }
4327
+ break;
4328
+ case "api":
4329
+ if (!("url" in node.data) || !node.data.url) {
4330
+ errors.push({
4331
+ nodeId: node.id,
4332
+ field: "url",
4333
+ message: "API node missing URL",
4334
+ code: "MISSING_API_URL"
4335
+ });
4336
+ }
4337
+ if (!("method" in node.data)) {
4338
+ warnings.push({
4339
+ nodeId: node.id,
4340
+ message: "API node missing method, defaulting to GET",
4341
+ code: "MISSING_API_METHOD"
4342
+ });
4343
+ }
4344
+ break;
4345
+ }
4346
+ }
4347
+ function validateDataFlow(file, errors, warnings) {
4348
+ if (!file.nodes || !file.edges) return;
4349
+ const nodeIds = new Set(file.nodes.map((n) => n.id));
4350
+ for (const edge of file.edges) {
4351
+ if (!nodeIds.has(edge.source)) {
4352
+ errors.push({
4353
+ connectionId: edge.id,
4354
+ message: `Edge references non-existent source node: ${edge.source}`,
4355
+ code: "INVALID_SOURCE_NODE"
4356
+ });
4357
+ }
4358
+ if (!nodeIds.has(edge.target)) {
4359
+ errors.push({
4360
+ connectionId: edge.id,
4361
+ message: `Edge references non-existent target node: ${edge.target}`,
4362
+ code: "INVALID_TARGET_NODE"
4363
+ });
4364
+ }
4365
+ }
4366
+ const hasCycle = detectCycles(file.nodes, file.edges);
4367
+ if (hasCycle) {
4368
+ errors.push({
4369
+ message: "Workflow contains cycles which may cause infinite loops",
4370
+ code: "CYCLE_DETECTED"
4371
+ });
4372
+ }
4373
+ const connectedNodes = /* @__PURE__ */ new Set();
4374
+ for (const edge of file.edges) {
4375
+ connectedNodes.add(edge.source);
4376
+ connectedNodes.add(edge.target);
4377
+ }
4378
+ for (const node of file.nodes) {
4379
+ if (!connectedNodes.has(node.id) && file.nodes.length > 1) {
4380
+ warnings.push({
4381
+ nodeId: node.id,
4382
+ message: `Node "${node.data.label || node.id}" is not connected to any other nodes`,
4383
+ code: "UNREACHABLE_NODE"
4384
+ });
4385
+ }
4386
+ }
4387
+ }
4388
+ function isInternalContainerEdge(edge) {
4389
+ const internalHandles = [
4390
+ "loop-start",
4391
+ "loop-end",
4392
+ "parallel-start",
4393
+ "parallel-end"
4394
+ ];
4395
+ if (edge.sourceHandle && internalHandles.includes(edge.sourceHandle)) {
4396
+ return true;
4397
+ }
4398
+ if (edge.targetHandle && internalHandles.includes(edge.targetHandle)) {
4399
+ return true;
4400
+ }
4401
+ if (edge.sourceHandle && edge.sourceHandle.startsWith("fork-")) {
4402
+ return true;
4403
+ }
4404
+ if (edge.targetHandle && edge.targetHandle.startsWith("fork-")) {
4405
+ return true;
4406
+ }
4407
+ if (edge.sourceHandle === "toolResult" && edge.targetHandle === "toolResult") {
4408
+ return true;
4409
+ }
4410
+ if (edge.sourceHandle === "onCheckpoint") {
4411
+ return true;
4412
+ }
4413
+ return false;
4414
+ }
4415
+ function detectCycles(nodes, edges) {
4416
+ const adjacencyList = /* @__PURE__ */ new Map();
4417
+ for (const node of nodes) {
4418
+ adjacencyList.set(node.id, []);
4419
+ }
4420
+ for (const edge of edges) {
4421
+ if (isInternalContainerEdge(edge)) {
4422
+ continue;
4423
+ }
4424
+ const targets = adjacencyList.get(edge.source) || [];
4425
+ targets.push(edge.target);
4426
+ adjacencyList.set(edge.source, targets);
4427
+ }
4428
+ const visited = /* @__PURE__ */ new Set();
4429
+ const recursionStack = /* @__PURE__ */ new Set();
4430
+ function dfs(nodeId) {
4431
+ visited.add(nodeId);
4432
+ recursionStack.add(nodeId);
4433
+ const neighbors = adjacencyList.get(nodeId) || [];
4434
+ for (const neighbor of neighbors) {
4435
+ if (!visited.has(neighbor)) {
4436
+ if (dfs(neighbor)) return true;
4437
+ } else if (recursionStack.has(neighbor)) {
4438
+ return true;
4439
+ }
4440
+ }
4441
+ recursionStack.delete(nodeId);
4442
+ return false;
4443
+ }
4444
+ for (const node of nodes) {
4445
+ if (!visited.has(node.id)) {
4446
+ if (dfs(node.id)) return true;
4447
+ }
4448
+ }
4449
+ return false;
4450
+ }
4451
+ function convertNodesToReactFlow(nodes, errors) {
4452
+ const rfNodes = nodes.map((node) => {
4453
+ if (node.type === "memory") {
4454
+ const memData = node.data;
4455
+ if (memData.operation && !memData.operations) {
4456
+ memData.operations = [memData.operation];
4457
+ delete memData.operation;
4458
+ }
4459
+ }
4460
+ const nodeData = node.data;
4461
+ const rfNode = {
4462
+ id: node.id,
4463
+ type: node.type,
4464
+ position: node.position || { x: 0, y: 0 },
4465
+ data: nodeData
4466
+ };
4467
+ if (nodeData.dockedTo) {
4468
+ rfNode.hidden = true;
4469
+ rfNode.width = 0;
4470
+ rfNode.height = 0;
4471
+ rfNode.position = { x: -9999, y: -9999 };
4472
+ }
4473
+ if (node.parentId) {
4474
+ rfNode.parentId = node.parentId;
4475
+ rfNode.extent = "parent";
4476
+ }
4477
+ if (node.width && !nodeData.dockedTo) {
4478
+ rfNode.width = node.width;
4479
+ }
4480
+ if (node.height && !nodeData.dockedTo) {
4481
+ rfNode.height = node.height;
4482
+ }
4483
+ return rfNode;
4484
+ });
4485
+ return sortNodesForReactFlow(rfNodes);
4486
+ }
4487
+ function sortNodesForReactFlow(nodes) {
4488
+ const nodeMap = /* @__PURE__ */ new Map();
4489
+ for (const node of nodes) {
4490
+ nodeMap.set(node.id, node);
4491
+ }
4492
+ const rootNodes = [];
4493
+ const childNodes = [];
4494
+ for (const node of nodes) {
4495
+ if (node.parentId) {
4496
+ childNodes.push(node);
4497
+ } else {
4498
+ rootNodes.push(node);
4499
+ }
4500
+ }
4501
+ const result = [...rootNodes];
4502
+ const remaining = [...childNodes];
4503
+ const placedIds = new Set(rootNodes.map((n) => n.id));
4504
+ let iterations = 0;
4505
+ const maxIterations = remaining.length * 2;
4506
+ while (remaining.length > 0 && iterations < maxIterations) {
4507
+ iterations++;
4508
+ for (let i = remaining.length - 1; i >= 0; i--) {
4509
+ const child = remaining[i];
4510
+ if (child.parentId && placedIds.has(child.parentId)) {
4511
+ result.push(child);
4512
+ placedIds.add(child.id);
4513
+ remaining.splice(i, 1);
4514
+ }
4515
+ }
4516
+ }
4517
+ if (remaining.length > 0) {
4518
+ console.warn("[workflowParser] Some nodes have missing parent references:", remaining.map((n) => n.id));
4519
+ result.push(...remaining);
4520
+ }
4521
+ return result;
4522
+ }
4523
+ function normalizeEdges(edges, errors) {
4524
+ return edges.map((edge) => {
4525
+ const sourceHandle = edge.sourceHandle || "output";
4526
+ const targetHandle = edge.targetHandle || "input";
4527
+ const animated = edge.animated ?? (shouldEdgeBeAnimated(sourceHandle) || shouldEdgeBeAnimated(targetHandle));
4528
+ return {
4529
+ id: edge.id,
4530
+ source: edge.source,
4531
+ target: edge.target,
4532
+ sourceHandle,
4533
+ targetHandle,
4534
+ animated,
4535
+ label: edge.label
4536
+ };
4537
+ });
4538
+ }
4539
+ function isExecutionFlowEdge(edge) {
4540
+ if (edge.sourceHandle === "loop-end" || edge.sourceHandle === "parallel-end") {
4541
+ return false;
4542
+ }
4543
+ if (edge.targetHandle && edge.targetHandle.startsWith("fork-")) {
4544
+ return false;
4545
+ }
4546
+ const eventBasedHandles = ["onError", "onCheckpoint", "onProgress", "toolResult"];
4547
+ if (edge.sourceHandle && eventBasedHandles.includes(edge.sourceHandle)) {
4548
+ return false;
4549
+ }
4550
+ return true;
4551
+ }
4552
+ function getExecutionOrder(workflow) {
4553
+ const { file } = workflow;
4554
+ if (!file.nodes || !file.edges) return [];
4555
+ const childNodeIds = /* @__PURE__ */ new Set();
4556
+ for (const node of file.nodes) {
4557
+ if (node.parentId) {
4558
+ childNodeIds.add(node.id);
4559
+ }
4560
+ }
4561
+ const rootNodes = file.nodes.filter((n) => !n.parentId);
4562
+ const inDegree = /* @__PURE__ */ new Map();
4563
+ const adjacencyList = /* @__PURE__ */ new Map();
4564
+ for (const node of rootNodes) {
4565
+ inDegree.set(node.id, 0);
4566
+ adjacencyList.set(node.id, []);
4567
+ }
4568
+ for (const edge of file.edges) {
4569
+ if (childNodeIds.has(edge.source) || childNodeIds.has(edge.target)) {
4570
+ continue;
4571
+ }
4572
+ if (!isExecutionFlowEdge(edge)) {
4573
+ continue;
4574
+ }
4575
+ const targets = adjacencyList.get(edge.source) || [];
4576
+ targets.push(edge.target);
4577
+ adjacencyList.set(edge.source, targets);
4578
+ inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
4579
+ }
4580
+ const globalNodeTypes = /* @__PURE__ */ new Set(["provider", "error-handler", "connection"]);
4581
+ const startNodeTypes = /* @__PURE__ */ new Set(["trigger"]);
4582
+ const eventDrivenNodeTypes = /* @__PURE__ */ new Set(["callback", "checkpoint"]);
4583
+ const globalQueue = [];
4584
+ const startQueue = [];
4585
+ const otherQueue = [];
4586
+ for (const [nodeId, degree] of inDegree) {
4587
+ if (degree === 0) {
4588
+ const node = rootNodes.find((n) => n.id === nodeId);
4589
+ const nodeType = node?.type || "";
4590
+ if (eventDrivenNodeTypes.has(nodeType)) {
4591
+ continue;
4592
+ }
4593
+ if (globalNodeTypes.has(nodeType)) {
4594
+ globalQueue.push(nodeId);
4595
+ } else if (startNodeTypes.has(nodeType)) {
4596
+ startQueue.push(nodeId);
4597
+ } else {
4598
+ otherQueue.push(nodeId);
4599
+ }
4600
+ }
4601
+ }
4602
+ const queue = [...globalQueue, ...startQueue, ...otherQueue];
4603
+ const order = [];
4604
+ while (queue.length > 0) {
4605
+ const current = queue.shift();
4606
+ order.push(current);
4607
+ const neighbors = adjacencyList.get(current) || [];
4608
+ for (const neighbor of neighbors) {
4609
+ const newDegree = (inDegree.get(neighbor) || 0) - 1;
4610
+ inDegree.set(neighbor, newDegree);
4611
+ if (newDegree === 0) queue.push(neighbor);
4612
+ }
4613
+ }
4614
+ const visitedSet = new Set(order);
4615
+ for (const node of rootNodes) {
4616
+ if (!visitedSet.has(node.id)) {
4617
+ if (!eventDrivenNodeTypes.has(node.type || "")) {
4618
+ console.log(`[getExecutionOrder] Node not in execution flow (disconnected or event-driven): ${node.id} (type: ${node.type})`);
4619
+ }
4620
+ }
4621
+ }
4622
+ return order;
4623
+ }
4624
+
4625
+ export { AnthropicFormatter, BUILTIN_COMMAND_EXECUTABLES, CODE_EXTENSIONS, CONTENT_TYPES, CodeGenerationStage, CompilationContext, CompilationError, CompilationStage, CompilerPipeline, DEFAULT_SECURITY_CONFIG, DOCKABLE_HANDLES, DOCKABLE_NODE_TYPES, DependencyResolutionStage, EXTENSION_TO_LANGUAGE, EXTENSION_TO_LANGUAGE_ALIASES, HybridFileSystem, LexicalAnalysisStage, MEMORY_OPERATIONS_BY_MODE, MarkdownFormatter, MemoryFileSystem, OpenAIFormatter, PACKAGE_TYPE_DIRS, PROMPD_EXTENSIONS, ParseError, PrompdCompiler, PrompdError, PrompdLoader, PrompdParser, SectionOverrideProcessor, SecurityError, SemanticAnalysisStage, TOOL_DEPLOY_DIRS, TemplateProcessingStage, VALID_PACKAGE_TYPES, ValidationError, basenamePosix, compile, createCoreStages, createEmptyWorkflow, createPrompdEnvironment, createWorkflowNode, dirnamePosix, extname, getContentType, getExecutionOrder, getInstallDirForType, getLanguageAliasesForExtension, getLanguageForExtension, isAbsolutePosix, isPrompdFile, isValidPackageReference, isValidPackageType, joinPosix, needsFrontmatterProtection, normalizePosix, parsePackageReference, parsePackageReferenceWithPath, parseWorkflow, resolvePackageFile, resolvePosix, serializeWorkflow, stripFilePath, validateWorkflow, validateWorkflowQuick };
4626
+ //# sourceMappingURL=index.js.map
4627
+ //# sourceMappingURL=index.js.map