@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/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
+ }