@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/helpers.ts ADDED
@@ -0,0 +1,650 @@
1
+ /**
2
+ * @wundr/prompt-templates - Built-in Handlebars helpers for prompt templating
3
+ */
4
+
5
+ import Handlebars from 'handlebars';
6
+
7
+ import type {
8
+ ToolDefinition,
9
+ ConversationMessage,
10
+ HelperDefinition,
11
+ } from './types.js';
12
+
13
+ /**
14
+ * Format tools array into a structured prompt format
15
+ *
16
+ * @param tools - Array of tool definitions
17
+ * @param options - Handlebars helper options
18
+ * @returns Formatted tools string
19
+ *
20
+ * @example
21
+ * {{formatTools tools}}
22
+ */
23
+ export function formatTools(
24
+ tools: ToolDefinition[] | undefined,
25
+ options?: Handlebars.HelperOptions,
26
+ ): string {
27
+ if (!tools || tools.length === 0) {
28
+ return '';
29
+ }
30
+
31
+ const format = options?.hash?.['format'] as string | undefined;
32
+
33
+ if (format === 'json') {
34
+ return JSON.stringify(tools, null, 2);
35
+ }
36
+
37
+ if (format === 'compact') {
38
+ return tools.map(t => `- ${t.name}: ${t.description}`).join('\n');
39
+ }
40
+
41
+ // Default detailed format
42
+ const formatted = tools
43
+ .map(tool => {
44
+ let output = `### ${tool.name}\n${tool.description}`;
45
+
46
+ if (tool.parameters?.properties) {
47
+ output += '\n\n**Parameters:**';
48
+ for (const [name, prop] of Object.entries(tool.parameters.properties)) {
49
+ const required = tool.parameters.required?.includes(name)
50
+ ? ' (required)'
51
+ : '';
52
+ const paramProp = prop as { type?: string; description?: string };
53
+ output += `\n- \`${name}\`${required}: ${paramProp.type || 'any'} - ${paramProp.description || 'No description'}`;
54
+ }
55
+ }
56
+
57
+ if (tool.examples && tool.examples.length > 0) {
58
+ output += '\n\n**Examples:**';
59
+ for (const example of tool.examples) {
60
+ output += `\n\`\`\`\n${example}\n\`\`\``;
61
+ }
62
+ }
63
+
64
+ return output;
65
+ })
66
+ .join('\n\n---\n\n');
67
+
68
+ return formatted;
69
+ }
70
+
71
+ /**
72
+ * Conditionally render block if value is defined and not null/undefined
73
+ *
74
+ * @param value - Value to check
75
+ * @param options - Handlebars helper options
76
+ * @returns Rendered block or empty string
77
+ *
78
+ * @example
79
+ * {{#ifDefined user.name}}Hello, {{user.name}}{{/ifDefined}}
80
+ */
81
+ export function ifDefined(
82
+ this: unknown,
83
+ value: unknown,
84
+ options: Handlebars.HelperOptions,
85
+ ): string {
86
+ if (value !== undefined && value !== null) {
87
+ return options.fn(this);
88
+ }
89
+ return options.inverse ? options.inverse(this) : '';
90
+ }
91
+
92
+ /**
93
+ * Wrap content in a code block with optional language
94
+ *
95
+ * @param language - Programming language for syntax highlighting
96
+ * @param options - Handlebars helper options
97
+ * @returns Code block formatted string
98
+ *
99
+ * @example
100
+ * {{#codeBlock "javascript"}}
101
+ * const x = 1;
102
+ * {{/codeBlock}}
103
+ */
104
+ export function codeBlock(
105
+ this: unknown,
106
+ language: string | Handlebars.HelperOptions,
107
+ options?: Handlebars.HelperOptions,
108
+ ): string {
109
+ // Handle case where language is not provided
110
+ if (typeof language === 'object' && !options) {
111
+ options = language as Handlebars.HelperOptions;
112
+ language = '';
113
+ }
114
+
115
+ const content = options?.fn(this) || '';
116
+ const lang = typeof language === 'string' ? language : '';
117
+ return `\`\`\`${lang}\n${content.trim()}\n\`\`\``;
118
+ }
119
+
120
+ /**
121
+ * Format memory/conversation history into a readable format
122
+ *
123
+ * @param messages - Array of conversation messages
124
+ * @param options - Handlebars helper options
125
+ * @returns Formatted memory string
126
+ *
127
+ * @example
128
+ * {{formatMemory memory.messages}}
129
+ */
130
+ export function formatMemory(
131
+ messages: ConversationMessage[] | undefined,
132
+ options?: Handlebars.HelperOptions,
133
+ ): string {
134
+ if (!messages || messages.length === 0) {
135
+ return '';
136
+ }
137
+
138
+ const maxMessages = (options?.hash?.['max'] as number) ?? messages.length;
139
+ const format = options?.hash?.['format'] as string | undefined;
140
+ const slicedMessages = messages.slice(-maxMessages);
141
+
142
+ if (format === 'compact') {
143
+ return slicedMessages
144
+ .map(m => `[${m.role.toUpperCase()}]: ${m.content}`)
145
+ .join('\n');
146
+ }
147
+
148
+ if (format === 'xml') {
149
+ return slicedMessages
150
+ .map(
151
+ m =>
152
+ `<message role="${m.role}"${m.name ? ` name="${m.name}"` : ''}>\n${m.content}\n</message>`,
153
+ )
154
+ .join('\n\n');
155
+ }
156
+
157
+ // Default format
158
+ return slicedMessages
159
+ .map(m => {
160
+ const roleLabel =
161
+ m.role.charAt(0).toUpperCase() + m.role.slice(1).toLowerCase();
162
+ const nameLabel = m.name ? ` (${m.name})` : '';
163
+ return `**${roleLabel}${nameLabel}:**\n${m.content}`;
164
+ })
165
+ .join('\n\n');
166
+ }
167
+
168
+ /**
169
+ * Repeat content n times
170
+ *
171
+ * @param count - Number of times to repeat
172
+ * @param options - Handlebars helper options
173
+ * @returns Repeated content
174
+ *
175
+ * @example
176
+ * {{#repeat 3}}Item {{@index}}{{/repeat}}
177
+ */
178
+ export function repeat(
179
+ this: unknown,
180
+ count: number,
181
+ options: Handlebars.HelperOptions,
182
+ ): string {
183
+ const results: string[] = [];
184
+ for (let i = 0; i < count; i++) {
185
+ const data = Handlebars.createFrame(options.data || {});
186
+ data['index'] = i;
187
+ data['first'] = i === 0;
188
+ data['last'] = i === count - 1;
189
+ results.push(options.fn(this, { data }));
190
+ }
191
+ return results.join('');
192
+ }
193
+
194
+ /**
195
+ * Format a date value
196
+ *
197
+ * @param date - Date to format
198
+ * @param formatStr - Format string (iso, locale, relative, or custom)
199
+ * @returns Formatted date string
200
+ *
201
+ * @example
202
+ * {{formatDate timestamp "iso"}}
203
+ */
204
+ export function formatDate(
205
+ date: Date | string | number | undefined,
206
+ formatStr?: string,
207
+ ): string {
208
+ if (!date) {
209
+ return '';
210
+ }
211
+
212
+ const dateObj = date instanceof Date ? date : new Date(date);
213
+
214
+ if (isNaN(dateObj.getTime())) {
215
+ return '';
216
+ }
217
+
218
+ const format = typeof formatStr === 'string' ? formatStr : 'iso';
219
+
220
+ switch (format) {
221
+ case 'iso':
222
+ return dateObj.toISOString();
223
+ case 'locale':
224
+ return dateObj.toLocaleString();
225
+ case 'date':
226
+ return dateObj.toLocaleDateString();
227
+ case 'time':
228
+ return dateObj.toLocaleTimeString();
229
+ case 'relative': {
230
+ const now = Date.now();
231
+ const diff = now - dateObj.getTime();
232
+ const seconds = Math.floor(diff / 1000);
233
+ const minutes = Math.floor(seconds / 60);
234
+ const hours = Math.floor(minutes / 60);
235
+ const days = Math.floor(hours / 24);
236
+
237
+ if (days > 0) {
238
+ return `${days} day${days > 1 ? 's' : ''} ago`;
239
+ }
240
+ if (hours > 0) {
241
+ return `${hours} hour${hours > 1 ? 's' : ''} ago`;
242
+ }
243
+ if (minutes > 0) {
244
+ return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
245
+ }
246
+ return 'just now';
247
+ }
248
+ default:
249
+ return dateObj.toISOString();
250
+ }
251
+ }
252
+
253
+ /**
254
+ * JSON stringify with formatting options
255
+ *
256
+ * @param value - Value to stringify
257
+ * @param options - Handlebars helper options
258
+ * @returns JSON string
259
+ *
260
+ * @example
261
+ * {{json data indent=2}}
262
+ */
263
+ export function json(
264
+ value: unknown,
265
+ options?: Handlebars.HelperOptions,
266
+ ): string {
267
+ const indent = (options?.hash?.['indent'] as number) ?? 2;
268
+ try {
269
+ return JSON.stringify(value, null, indent);
270
+ } catch {
271
+ return String(value);
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Truncate text to a maximum length
277
+ *
278
+ * @param text - Text to truncate
279
+ * @param length - Maximum length
280
+ * @param suffix - Suffix to add when truncated (default: '...')
281
+ * @returns Truncated text
282
+ *
283
+ * @example
284
+ * {{truncate description 100 "..."}}
285
+ */
286
+ export function truncate(
287
+ text: string | undefined,
288
+ length?: number | Handlebars.HelperOptions,
289
+ suffix?: string | Handlebars.HelperOptions,
290
+ ): string {
291
+ if (!text) {
292
+ return '';
293
+ }
294
+
295
+ const maxLength = typeof length === 'number' ? length : 100;
296
+ const ellipsis = typeof suffix === 'string' ? suffix : '...';
297
+
298
+ if (text.length <= maxLength) {
299
+ return text;
300
+ }
301
+
302
+ return text.substring(0, maxLength - ellipsis.length) + ellipsis;
303
+ }
304
+
305
+ /**
306
+ * Join array items with a separator
307
+ *
308
+ * @param items - Array to join
309
+ * @param separator - Separator string (default: ', ')
310
+ * @returns Joined string
311
+ *
312
+ * @example
313
+ * {{join tags ", "}}
314
+ */
315
+ export function join(
316
+ items: unknown[] | undefined,
317
+ separator?: string | Handlebars.HelperOptions,
318
+ ): string {
319
+ if (!items || !Array.isArray(items)) {
320
+ return '';
321
+ }
322
+
323
+ const sep = typeof separator === 'string' ? separator : ', ';
324
+ return items.map(String).join(sep);
325
+ }
326
+
327
+ /**
328
+ * String comparison helper
329
+ *
330
+ * @param a - First value
331
+ * @param operator - Comparison operator
332
+ * @param b - Second value
333
+ * @param options - Handlebars helper options
334
+ * @returns Rendered block based on comparison result
335
+ *
336
+ * @example
337
+ * {{#compare role "eq" "admin"}}Admin content{{/compare}}
338
+ */
339
+ export function compare(
340
+ this: unknown,
341
+ a: unknown,
342
+ operator: string,
343
+ b: unknown,
344
+ options: Handlebars.HelperOptions,
345
+ ): string {
346
+ let result = false;
347
+
348
+ switch (operator) {
349
+ case 'eq':
350
+ case '==':
351
+ case '===':
352
+ result = a === b;
353
+ break;
354
+ case 'ne':
355
+ case '!=':
356
+ case '!==':
357
+ result = a !== b;
358
+ break;
359
+ case 'lt':
360
+ case '<':
361
+ result = Number(a) < Number(b);
362
+ break;
363
+ case 'lte':
364
+ case '<=':
365
+ result = Number(a) <= Number(b);
366
+ break;
367
+ case 'gt':
368
+ case '>':
369
+ result = Number(a) > Number(b);
370
+ break;
371
+ case 'gte':
372
+ case '>=':
373
+ result = Number(a) >= Number(b);
374
+ break;
375
+ case 'contains':
376
+ result = typeof a === 'string' && typeof b === 'string' && a.includes(b);
377
+ break;
378
+ case 'startsWith':
379
+ result =
380
+ typeof a === 'string' && typeof b === 'string' && a.startsWith(b);
381
+ break;
382
+ case 'endsWith':
383
+ result = typeof a === 'string' && typeof b === 'string' && a.endsWith(b);
384
+ break;
385
+ default:
386
+ result = false;
387
+ }
388
+
389
+ if (result) {
390
+ return options.fn(this);
391
+ }
392
+ return options.inverse ? options.inverse(this) : '';
393
+ }
394
+
395
+ /**
396
+ * Capitalize first letter of string
397
+ *
398
+ * @param text - Text to capitalize
399
+ * @returns Capitalized text
400
+ *
401
+ * @example
402
+ * {{capitalize name}}
403
+ */
404
+ export function capitalize(text: string | undefined): string {
405
+ if (!text) {
406
+ return '';
407
+ }
408
+ return text.charAt(0).toUpperCase() + text.slice(1);
409
+ }
410
+
411
+ /**
412
+ * Convert string to uppercase
413
+ *
414
+ * @param text - Text to convert
415
+ * @returns Uppercase text
416
+ *
417
+ * @example
418
+ * {{uppercase status}}
419
+ */
420
+ export function uppercase(text: string | undefined): string {
421
+ if (!text) {
422
+ return '';
423
+ }
424
+ return text.toUpperCase();
425
+ }
426
+
427
+ /**
428
+ * Convert string to lowercase
429
+ *
430
+ * @param text - Text to convert
431
+ * @returns Lowercase text
432
+ *
433
+ * @example
434
+ * {{lowercase status}}
435
+ */
436
+ export function lowercase(text: string | undefined): string {
437
+ if (!text) {
438
+ return '';
439
+ }
440
+ return text.toLowerCase();
441
+ }
442
+
443
+ /**
444
+ * Indent text by a number of spaces
445
+ *
446
+ * @param text - Text to indent
447
+ * @param spaces - Number of spaces (default: 2)
448
+ * @returns Indented text
449
+ *
450
+ * @example
451
+ * {{indent content 4}}
452
+ */
453
+ export function indent(
454
+ text: string | undefined,
455
+ spaces?: number | Handlebars.HelperOptions,
456
+ ): string {
457
+ if (!text) {
458
+ return '';
459
+ }
460
+
461
+ const numSpaces = typeof spaces === 'number' ? spaces : 2;
462
+ const indentStr = ' '.repeat(numSpaces);
463
+ return text
464
+ .split('\n')
465
+ .map(line => indentStr + line)
466
+ .join('\n');
467
+ }
468
+
469
+ /**
470
+ * Wrap text to a maximum line width
471
+ *
472
+ * @param text - Text to wrap
473
+ * @param width - Maximum line width (default: 80)
474
+ * @returns Wrapped text
475
+ *
476
+ * @example
477
+ * {{wrap longText 72}}
478
+ */
479
+ export function wrap(
480
+ text: string | undefined,
481
+ width?: number | Handlebars.HelperOptions,
482
+ ): string {
483
+ if (!text) {
484
+ return '';
485
+ }
486
+
487
+ const maxWidth = typeof width === 'number' ? width : 80;
488
+ const words = text.split(' ');
489
+ const lines: string[] = [];
490
+ let currentLine = '';
491
+
492
+ for (const word of words) {
493
+ if (currentLine.length + word.length + 1 > maxWidth) {
494
+ if (currentLine) {
495
+ lines.push(currentLine);
496
+ }
497
+ currentLine = word;
498
+ } else {
499
+ currentLine = currentLine ? `${currentLine} ${word}` : word;
500
+ }
501
+ }
502
+
503
+ if (currentLine) {
504
+ lines.push(currentLine);
505
+ }
506
+
507
+ return lines.join('\n');
508
+ }
509
+
510
+ /**
511
+ * Create a bulleted list from array
512
+ *
513
+ * @param items - Array of items
514
+ * @param options - Handlebars helper options
515
+ * @returns Bulleted list string
516
+ *
517
+ * @example
518
+ * {{bulletList items bullet="*"}}
519
+ */
520
+ export function bulletList(
521
+ items: unknown[] | undefined,
522
+ options?: Handlebars.HelperOptions,
523
+ ): string {
524
+ if (!items || !Array.isArray(items)) {
525
+ return '';
526
+ }
527
+
528
+ const bullet = (options?.hash?.['bullet'] as string) || '-';
529
+ return items.map(item => `${bullet} ${String(item)}`).join('\n');
530
+ }
531
+
532
+ /**
533
+ * Create a numbered list from array
534
+ *
535
+ * @param items - Array of items
536
+ * @param options - Handlebars helper options
537
+ * @returns Numbered list string
538
+ *
539
+ * @example
540
+ * {{numberedList steps start=1}}
541
+ */
542
+ export function numberedList(
543
+ items: unknown[] | undefined,
544
+ options?: Handlebars.HelperOptions,
545
+ ): string {
546
+ if (!items || !Array.isArray(items)) {
547
+ return '';
548
+ }
549
+
550
+ const start = (options?.hash?.['start'] as number) || 1;
551
+ return items
552
+ .map((item, index) => `${start + index}. ${String(item)}`)
553
+ .join('\n');
554
+ }
555
+
556
+ /**
557
+ * Get all built-in helper definitions
558
+ *
559
+ * @returns Array of helper definitions
560
+ */
561
+ export function getBuiltinHelpers(): HelperDefinition[] {
562
+ return [
563
+ {
564
+ name: 'formatTools',
565
+ description: 'Format tools array into a structured prompt format',
566
+ fn: formatTools as HelperDefinition['fn'],
567
+ },
568
+ {
569
+ name: 'ifDefined',
570
+ description:
571
+ 'Conditionally render block if value is defined and not null/undefined',
572
+ fn: ifDefined as HelperDefinition['fn'],
573
+ },
574
+ {
575
+ name: 'codeBlock',
576
+ description: 'Wrap content in a code block with optional language',
577
+ fn: codeBlock as HelperDefinition['fn'],
578
+ },
579
+ {
580
+ name: 'formatMemory',
581
+ description: 'Format memory/conversation history into a readable format',
582
+ fn: formatMemory as HelperDefinition['fn'],
583
+ },
584
+ {
585
+ name: 'repeat',
586
+ description: 'Repeat content n times',
587
+ fn: repeat as HelperDefinition['fn'],
588
+ },
589
+ {
590
+ name: 'formatDate',
591
+ description: 'Format a date value',
592
+ fn: formatDate as HelperDefinition['fn'],
593
+ },
594
+ {
595
+ name: 'json',
596
+ description: 'JSON stringify with formatting options',
597
+ fn: json as HelperDefinition['fn'],
598
+ },
599
+ {
600
+ name: 'truncate',
601
+ description: 'Truncate text to a maximum length',
602
+ fn: truncate as HelperDefinition['fn'],
603
+ },
604
+ {
605
+ name: 'join',
606
+ description: 'Join array items with a separator',
607
+ fn: join as HelperDefinition['fn'],
608
+ },
609
+ {
610
+ name: 'compare',
611
+ description: 'String comparison helper',
612
+ fn: compare as HelperDefinition['fn'],
613
+ },
614
+ {
615
+ name: 'capitalize',
616
+ description: 'Capitalize first letter of string',
617
+ fn: capitalize as HelperDefinition['fn'],
618
+ },
619
+ {
620
+ name: 'uppercase',
621
+ description: 'Convert string to uppercase',
622
+ fn: uppercase as HelperDefinition['fn'],
623
+ },
624
+ {
625
+ name: 'lowercase',
626
+ description: 'Convert string to lowercase',
627
+ fn: lowercase as HelperDefinition['fn'],
628
+ },
629
+ {
630
+ name: 'indent',
631
+ description: 'Indent text by a number of spaces',
632
+ fn: indent as HelperDefinition['fn'],
633
+ },
634
+ {
635
+ name: 'wrap',
636
+ description: 'Wrap text to a maximum line width',
637
+ fn: wrap as HelperDefinition['fn'],
638
+ },
639
+ {
640
+ name: 'bulletList',
641
+ description: 'Create a bulleted list from array',
642
+ fn: bulletList as HelperDefinition['fn'],
643
+ },
644
+ {
645
+ name: 'numberedList',
646
+ description: 'Create a numbered list from array',
647
+ fn: numberedList as HelperDefinition['fn'],
648
+ },
649
+ ];
650
+ }