@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/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/index.cjs +4716 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2929 -0
- package/dist/index.d.ts +2929 -0
- package/dist/index.js +4627 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
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
|