@wundr.io/prompt-templates 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +206 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +506 -0
- package/dist/engine.js.map +1 -0
- package/dist/helpers.d.ts +201 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +566 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +129 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +415 -0
- package/dist/loader.js.map +1 -0
- package/dist/macros.d.ts +64 -0
- package/dist/macros.d.ts.map +1 -0
- package/dist/macros.js +620 -0
- package/dist/macros.js.map +1 -0
- package/dist/types.d.ts +443 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +62 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -0
- package/src/engine.ts +616 -0
- package/src/helpers.ts +650 -0
- package/src/index.ts +138 -0
- package/src/loader.ts +468 -0
- package/src/macros.ts +635 -0
- package/src/types.ts +380 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wundr/prompt-templates - Jinja2-style dynamic prompt templating using Handlebars
|
|
3
|
+
*
|
|
4
|
+
* This package provides a powerful templating engine for creating dynamic AI prompts
|
|
5
|
+
* with support for variables, helpers, macros, and context-aware rendering.
|
|
6
|
+
*
|
|
7
|
+
* @example Basic usage
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createEngine } from '@wundr.io/prompt-templates';
|
|
10
|
+
*
|
|
11
|
+
* const engine = createEngine();
|
|
12
|
+
*
|
|
13
|
+
* const result = engine.render(
|
|
14
|
+
* 'Hello, {{name}}! You are a {{role}}.',
|
|
15
|
+
* { variables: { name: 'Claude', role: 'helpful assistant' } }
|
|
16
|
+
* );
|
|
17
|
+
*
|
|
18
|
+
* console.log(result.output);
|
|
19
|
+
* // Output: "Hello, Claude! You are a helpful assistant."
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example Using macros
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { createEngine } from '@wundr.io/prompt-templates';
|
|
25
|
+
*
|
|
26
|
+
* const engine = createEngine();
|
|
27
|
+
*
|
|
28
|
+
* const result = engine.render(
|
|
29
|
+
* '{{> systemRole role="a coding assistant" expertise="TypeScript" }}',
|
|
30
|
+
* { variables: {} }
|
|
31
|
+
* );
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example Loading templates from files
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { createEngine } from '@wundr.io/prompt-templates';
|
|
37
|
+
*
|
|
38
|
+
* const engine = createEngine({}, '/path/to/templates');
|
|
39
|
+
* engine.loadTemplate('my-prompt');
|
|
40
|
+
*
|
|
41
|
+
* const result = engine.render('my-prompt', {
|
|
42
|
+
* variables: { user: 'John' }
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @packageDocumentation
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
// Types
|
|
50
|
+
export type {
|
|
51
|
+
// Core types
|
|
52
|
+
JsonPrimitive,
|
|
53
|
+
JsonValue,
|
|
54
|
+
JsonObject,
|
|
55
|
+
JsonArray,
|
|
56
|
+
// Context types
|
|
57
|
+
TemplateContext,
|
|
58
|
+
SystemContext,
|
|
59
|
+
MemoryContext,
|
|
60
|
+
ConversationMessage,
|
|
61
|
+
// Tool types
|
|
62
|
+
ToolDefinition,
|
|
63
|
+
ToolParameters,
|
|
64
|
+
ToolParameterProperty,
|
|
65
|
+
// Template configuration
|
|
66
|
+
PromptTemplateConfig,
|
|
67
|
+
MacroDefinition,
|
|
68
|
+
MacroParameter,
|
|
69
|
+
HelperDefinition,
|
|
70
|
+
HelperFunction,
|
|
71
|
+
SafeString,
|
|
72
|
+
// Render types
|
|
73
|
+
RenderOptions,
|
|
74
|
+
RenderResult,
|
|
75
|
+
RenderMetadata,
|
|
76
|
+
TemplateError,
|
|
77
|
+
// Loader types
|
|
78
|
+
LoaderOptions,
|
|
79
|
+
// Event types
|
|
80
|
+
EngineEvents,
|
|
81
|
+
EngineEventHandler,
|
|
82
|
+
} from './types.js';
|
|
83
|
+
|
|
84
|
+
// Zod schemas for validation
|
|
85
|
+
export {
|
|
86
|
+
PromptTemplateConfigSchema,
|
|
87
|
+
ToolDefinitionSchema,
|
|
88
|
+
MacroDefinitionSchema,
|
|
89
|
+
} from './types.js';
|
|
90
|
+
|
|
91
|
+
// Engine
|
|
92
|
+
export { PromptTemplateEngine, createEngine } from './engine.js';
|
|
93
|
+
|
|
94
|
+
// Loader
|
|
95
|
+
export { TemplateLoader, createLoader } from './loader.js';
|
|
96
|
+
|
|
97
|
+
// Helpers
|
|
98
|
+
export {
|
|
99
|
+
formatTools,
|
|
100
|
+
ifDefined,
|
|
101
|
+
codeBlock,
|
|
102
|
+
formatMemory,
|
|
103
|
+
repeat,
|
|
104
|
+
formatDate,
|
|
105
|
+
json,
|
|
106
|
+
truncate,
|
|
107
|
+
join,
|
|
108
|
+
compare,
|
|
109
|
+
capitalize,
|
|
110
|
+
uppercase,
|
|
111
|
+
lowercase,
|
|
112
|
+
indent,
|
|
113
|
+
wrap,
|
|
114
|
+
bulletList,
|
|
115
|
+
numberedList,
|
|
116
|
+
getBuiltinHelpers,
|
|
117
|
+
} from './helpers.js';
|
|
118
|
+
|
|
119
|
+
// Macros
|
|
120
|
+
export {
|
|
121
|
+
systemRoleMacro,
|
|
122
|
+
taskContextMacro,
|
|
123
|
+
outputFormatMacro,
|
|
124
|
+
conversationHistoryMacro,
|
|
125
|
+
toolsSectionMacro,
|
|
126
|
+
codeContextMacro,
|
|
127
|
+
chainOfThoughtMacro,
|
|
128
|
+
fewShotExamplesMacro,
|
|
129
|
+
safetyGuardrailsMacro,
|
|
130
|
+
personaMacro,
|
|
131
|
+
getBuiltinMacros,
|
|
132
|
+
getMacroByName,
|
|
133
|
+
getMacroNames,
|
|
134
|
+
} from './macros.js';
|
|
135
|
+
|
|
136
|
+
// Package info
|
|
137
|
+
export const version = '1.0.3';
|
|
138
|
+
export const name = '@wundr.io/prompt-templates';
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wundr/prompt-templates - Template file loader
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
import { PromptTemplateConfigSchema } from './types.js';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
PromptTemplateConfig,
|
|
12
|
+
LoaderOptions,
|
|
13
|
+
TemplateError,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default loader options
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_LOADER_OPTIONS: Required<LoaderOptions> = {
|
|
20
|
+
baseDir: process.cwd(),
|
|
21
|
+
extension: '.hbs',
|
|
22
|
+
cache: true,
|
|
23
|
+
watch: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Template cache for loaded templates
|
|
28
|
+
*/
|
|
29
|
+
interface TemplateCache {
|
|
30
|
+
readonly template: PromptTemplateConfig;
|
|
31
|
+
readonly loadedAt: Date;
|
|
32
|
+
readonly filePath: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* TemplateLoader handles loading templates from the filesystem
|
|
37
|
+
*/
|
|
38
|
+
export class TemplateLoader {
|
|
39
|
+
private readonly options: Required<LoaderOptions>;
|
|
40
|
+
private readonly cache: Map<string, TemplateCache> = new Map();
|
|
41
|
+
private readonly watchers: Map<string, fs.FSWatcher> = new Map();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a new TemplateLoader
|
|
45
|
+
*
|
|
46
|
+
* @param options - Loader configuration options
|
|
47
|
+
*/
|
|
48
|
+
constructor(options: LoaderOptions = {}) {
|
|
49
|
+
this.options = { ...DEFAULT_LOADER_OPTIONS, ...options };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load a template from a file
|
|
54
|
+
*
|
|
55
|
+
* @param templatePath - Path to the template file (relative or absolute)
|
|
56
|
+
* @returns Loaded template configuration
|
|
57
|
+
* @throws Error if template cannot be loaded or is invalid
|
|
58
|
+
*/
|
|
59
|
+
loadTemplate(templatePath: string): PromptTemplateConfig {
|
|
60
|
+
const absolutePath = this.resolveTemplatePath(templatePath);
|
|
61
|
+
|
|
62
|
+
// Check cache first
|
|
63
|
+
if (this.options.cache) {
|
|
64
|
+
const cached = this.cache.get(absolutePath);
|
|
65
|
+
if (cached) {
|
|
66
|
+
return cached.template;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Load from filesystem
|
|
71
|
+
const template = this.loadFromFile(absolutePath);
|
|
72
|
+
|
|
73
|
+
// Cache the template
|
|
74
|
+
if (this.options.cache) {
|
|
75
|
+
this.cache.set(absolutePath, {
|
|
76
|
+
template,
|
|
77
|
+
loadedAt: new Date(),
|
|
78
|
+
filePath: absolutePath,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Set up watcher if enabled
|
|
82
|
+
if (this.options.watch && !this.watchers.has(absolutePath)) {
|
|
83
|
+
this.setupWatcher(absolutePath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return template;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load a template asynchronously
|
|
92
|
+
*
|
|
93
|
+
* @param templatePath - Path to the template file
|
|
94
|
+
* @returns Promise resolving to loaded template configuration
|
|
95
|
+
*/
|
|
96
|
+
async loadTemplateAsync(templatePath: string): Promise<PromptTemplateConfig> {
|
|
97
|
+
const absolutePath = this.resolveTemplatePath(templatePath);
|
|
98
|
+
|
|
99
|
+
// Check cache first
|
|
100
|
+
if (this.options.cache) {
|
|
101
|
+
const cached = this.cache.get(absolutePath);
|
|
102
|
+
if (cached) {
|
|
103
|
+
return cached.template;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Load from filesystem asynchronously
|
|
108
|
+
const template = await this.loadFromFileAsync(absolutePath);
|
|
109
|
+
|
|
110
|
+
// Cache the template
|
|
111
|
+
if (this.options.cache) {
|
|
112
|
+
this.cache.set(absolutePath, {
|
|
113
|
+
template,
|
|
114
|
+
loadedAt: new Date(),
|
|
115
|
+
filePath: absolutePath,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Set up watcher if enabled
|
|
119
|
+
if (this.options.watch && !this.watchers.has(absolutePath)) {
|
|
120
|
+
this.setupWatcher(absolutePath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return template;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Load all templates from a directory
|
|
129
|
+
*
|
|
130
|
+
* @param dirPath - Directory path to scan
|
|
131
|
+
* @param recursive - Whether to scan subdirectories
|
|
132
|
+
* @returns Array of loaded templates
|
|
133
|
+
*/
|
|
134
|
+
loadTemplatesFromDirectory(
|
|
135
|
+
dirPath?: string,
|
|
136
|
+
recursive: boolean = false,
|
|
137
|
+
): PromptTemplateConfig[] {
|
|
138
|
+
const absolutePath = dirPath
|
|
139
|
+
? path.isAbsolute(dirPath)
|
|
140
|
+
? dirPath
|
|
141
|
+
: path.join(this.options.baseDir, dirPath)
|
|
142
|
+
: this.options.baseDir;
|
|
143
|
+
|
|
144
|
+
const templates: PromptTemplateConfig[] = [];
|
|
145
|
+
const files = this.getTemplateFiles(absolutePath, recursive);
|
|
146
|
+
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
try {
|
|
149
|
+
const template = this.loadTemplate(file);
|
|
150
|
+
templates.push(template);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
// Log warning but continue loading other templates
|
|
153
|
+
console.warn(`Failed to load template ${file}:`, error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return templates;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Load raw template content from a file (without metadata parsing)
|
|
162
|
+
*
|
|
163
|
+
* @param templatePath - Path to the template file
|
|
164
|
+
* @returns Raw template string
|
|
165
|
+
*/
|
|
166
|
+
loadRawTemplate(templatePath: string): string {
|
|
167
|
+
const absolutePath = this.resolveTemplatePath(templatePath);
|
|
168
|
+
|
|
169
|
+
if (!fs.existsSync(absolutePath)) {
|
|
170
|
+
throw this.createError(
|
|
171
|
+
'TEMPLATE_NOT_FOUND',
|
|
172
|
+
`Template file not found: ${absolutePath}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return fs.readFileSync(absolutePath, 'utf-8');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Load raw template content asynchronously
|
|
181
|
+
*
|
|
182
|
+
* @param templatePath - Path to the template file
|
|
183
|
+
* @returns Promise resolving to raw template string
|
|
184
|
+
*/
|
|
185
|
+
async loadRawTemplateAsync(templatePath: string): Promise<string> {
|
|
186
|
+
const absolutePath = this.resolveTemplatePath(templatePath);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await fs.promises.access(absolutePath, fs.constants.R_OK);
|
|
190
|
+
} catch {
|
|
191
|
+
throw this.createError(
|
|
192
|
+
'TEMPLATE_NOT_FOUND',
|
|
193
|
+
`Template file not found: ${absolutePath}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return fs.promises.readFile(absolutePath, 'utf-8');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if a template exists
|
|
202
|
+
*
|
|
203
|
+
* @param templatePath - Path to the template file
|
|
204
|
+
* @returns True if template exists
|
|
205
|
+
*/
|
|
206
|
+
templateExists(templatePath: string): boolean {
|
|
207
|
+
const absolutePath = this.resolveTemplatePath(templatePath);
|
|
208
|
+
return fs.existsSync(absolutePath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Clear the template cache
|
|
213
|
+
*
|
|
214
|
+
* @param templatePath - Optional specific template to clear, or all if not provided
|
|
215
|
+
*/
|
|
216
|
+
clearCache(templatePath?: string): void {
|
|
217
|
+
if (templatePath) {
|
|
218
|
+
const absolutePath = this.resolveTemplatePath(templatePath);
|
|
219
|
+
this.cache.delete(absolutePath);
|
|
220
|
+
} else {
|
|
221
|
+
this.cache.clear();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get cache statistics
|
|
227
|
+
*
|
|
228
|
+
* @returns Cache statistics object
|
|
229
|
+
*/
|
|
230
|
+
getCacheStats(): { size: number; entries: string[] } {
|
|
231
|
+
return {
|
|
232
|
+
size: this.cache.size,
|
|
233
|
+
entries: Array.from(this.cache.keys()),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Stop all file watchers
|
|
239
|
+
*/
|
|
240
|
+
stopWatching(): void {
|
|
241
|
+
for (const watcher of this.watchers.values()) {
|
|
242
|
+
watcher.close();
|
|
243
|
+
}
|
|
244
|
+
this.watchers.clear();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Resolve template path to absolute path
|
|
249
|
+
*/
|
|
250
|
+
private resolveTemplatePath(templatePath: string): string {
|
|
251
|
+
// If already absolute, use as-is
|
|
252
|
+
if (path.isAbsolute(templatePath)) {
|
|
253
|
+
return this.ensureExtension(templatePath);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Resolve relative to base directory
|
|
257
|
+
return this.ensureExtension(path.join(this.options.baseDir, templatePath));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Ensure template path has the correct extension
|
|
262
|
+
*/
|
|
263
|
+
private ensureExtension(filePath: string): string {
|
|
264
|
+
if (!path.extname(filePath)) {
|
|
265
|
+
return filePath + this.options.extension;
|
|
266
|
+
}
|
|
267
|
+
return filePath;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Load template configuration from file
|
|
272
|
+
*/
|
|
273
|
+
private loadFromFile(filePath: string): PromptTemplateConfig {
|
|
274
|
+
if (!fs.existsSync(filePath)) {
|
|
275
|
+
throw this.createError(
|
|
276
|
+
'TEMPLATE_NOT_FOUND',
|
|
277
|
+
`Template file not found: ${filePath}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
282
|
+
return this.parseTemplateContent(content, filePath);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Load template configuration from file asynchronously
|
|
287
|
+
*/
|
|
288
|
+
private async loadFromFileAsync(
|
|
289
|
+
filePath: string,
|
|
290
|
+
): Promise<PromptTemplateConfig> {
|
|
291
|
+
try {
|
|
292
|
+
await fs.promises.access(filePath, fs.constants.R_OK);
|
|
293
|
+
} catch {
|
|
294
|
+
throw this.createError(
|
|
295
|
+
'TEMPLATE_NOT_FOUND',
|
|
296
|
+
`Template file not found: ${filePath}`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
301
|
+
return this.parseTemplateContent(content, filePath);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Parse template content with optional frontmatter
|
|
306
|
+
*/
|
|
307
|
+
private parseTemplateContent(
|
|
308
|
+
content: string,
|
|
309
|
+
filePath: string,
|
|
310
|
+
): PromptTemplateConfig {
|
|
311
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
312
|
+
|
|
313
|
+
if (frontmatterMatch) {
|
|
314
|
+
// Has frontmatter - parse YAML/JSON metadata
|
|
315
|
+
const metadataStr = frontmatterMatch[1];
|
|
316
|
+
const templateStr = frontmatterMatch[2];
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const metadata = JSON.parse(metadataStr);
|
|
320
|
+
const config = {
|
|
321
|
+
...metadata,
|
|
322
|
+
template: templateStr?.trim() || '',
|
|
323
|
+
id: metadata.id || path.basename(filePath, this.options.extension),
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return this.validateConfig(config);
|
|
327
|
+
} catch {
|
|
328
|
+
// If JSON parse fails, treat as simple key: value pairs
|
|
329
|
+
const metadata = this.parseSimpleFrontmatter(metadataStr);
|
|
330
|
+
const config = {
|
|
331
|
+
...metadata,
|
|
332
|
+
template: templateStr?.trim() || '',
|
|
333
|
+
id: metadata.id || path.basename(filePath, this.options.extension),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
return this.validateConfig(config);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// No frontmatter - create minimal config
|
|
341
|
+
const id = path.basename(filePath, this.options.extension);
|
|
342
|
+
return this.validateConfig({
|
|
343
|
+
id,
|
|
344
|
+
name: id,
|
|
345
|
+
version: '1.0.0',
|
|
346
|
+
template: content.trim(),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Parse simple frontmatter format (key: value)
|
|
352
|
+
*/
|
|
353
|
+
private parseSimpleFrontmatter(content: string): Record<string, unknown> {
|
|
354
|
+
const result: Record<string, unknown> = {};
|
|
355
|
+
const lines = content.split('\n');
|
|
356
|
+
|
|
357
|
+
for (const line of lines) {
|
|
358
|
+
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
359
|
+
if (match) {
|
|
360
|
+
const key = match[1];
|
|
361
|
+
let value: unknown = match[2]?.trim() || '';
|
|
362
|
+
|
|
363
|
+
// Try to parse as JSON for complex values
|
|
364
|
+
if (value && typeof value === 'string') {
|
|
365
|
+
if (value.startsWith('[') || value.startsWith('{')) {
|
|
366
|
+
try {
|
|
367
|
+
value = JSON.parse(value);
|
|
368
|
+
} catch {
|
|
369
|
+
// Keep as string
|
|
370
|
+
}
|
|
371
|
+
} else if (value === 'true') {
|
|
372
|
+
value = true;
|
|
373
|
+
} else if (value === 'false') {
|
|
374
|
+
value = false;
|
|
375
|
+
} else if (/^\d+$/.test(value)) {
|
|
376
|
+
value = parseInt(value, 10);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (key) {
|
|
381
|
+
result[key] = value;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Validate template configuration
|
|
391
|
+
*/
|
|
392
|
+
private validateConfig(config: unknown): PromptTemplateConfig {
|
|
393
|
+
const result = PromptTemplateConfigSchema.safeParse(config);
|
|
394
|
+
|
|
395
|
+
if (!result.success) {
|
|
396
|
+
const errors = result.error.errors
|
|
397
|
+
.map(e => `${e.path.join('.')}: ${e.message}`)
|
|
398
|
+
.join(', ');
|
|
399
|
+
throw this.createError(
|
|
400
|
+
'INVALID_TEMPLATE_CONFIG',
|
|
401
|
+
`Invalid template configuration: ${errors}`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result.data as PromptTemplateConfig;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get all template files in a directory
|
|
410
|
+
*/
|
|
411
|
+
private getTemplateFiles(dirPath: string, recursive: boolean): string[] {
|
|
412
|
+
const files: string[] = [];
|
|
413
|
+
|
|
414
|
+
if (!fs.existsSync(dirPath)) {
|
|
415
|
+
return files;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
419
|
+
|
|
420
|
+
for (const entry of entries) {
|
|
421
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
422
|
+
|
|
423
|
+
if (entry.isDirectory() && recursive) {
|
|
424
|
+
files.push(...this.getTemplateFiles(fullPath, recursive));
|
|
425
|
+
} else if (
|
|
426
|
+
entry.isFile() &&
|
|
427
|
+
entry.name.endsWith(this.options.extension)
|
|
428
|
+
) {
|
|
429
|
+
files.push(fullPath);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return files;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Set up file watcher for template changes
|
|
438
|
+
*/
|
|
439
|
+
private setupWatcher(filePath: string): void {
|
|
440
|
+
const watcher = fs.watch(filePath, eventType => {
|
|
441
|
+
if (eventType === 'change') {
|
|
442
|
+
// Invalidate cache on change
|
|
443
|
+
this.cache.delete(filePath);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
this.watchers.set(filePath, watcher);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Create a template error
|
|
452
|
+
*/
|
|
453
|
+
private createError(code: string, message: string): TemplateError & Error {
|
|
454
|
+
const error = new Error(message) as TemplateError & Error;
|
|
455
|
+
(error as unknown as { code: string }).code = code;
|
|
456
|
+
return error;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Create a template loader with default options
|
|
462
|
+
*
|
|
463
|
+
* @param options - Loader options
|
|
464
|
+
* @returns Configured template loader
|
|
465
|
+
*/
|
|
466
|
+
export function createLoader(options?: LoaderOptions): TemplateLoader {
|
|
467
|
+
return new TemplateLoader(options);
|
|
468
|
+
}
|