@riotprompt/riotprompt 0.0.1

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.
Files changed (129) hide show
  1. package/.gitcarve/config.yaml +10 -0
  2. package/.gitcarve/context/content.md +11 -0
  3. package/.markdown-doctest-setup.mjs +23 -0
  4. package/.nvmrc +1 -0
  5. package/LICENSE +190 -0
  6. package/README.md +513 -0
  7. package/dist/builder.cjs +152 -0
  8. package/dist/builder.cjs.map +1 -0
  9. package/dist/builder.d.ts +37 -0
  10. package/dist/builder.js +148 -0
  11. package/dist/builder.js.map +1 -0
  12. package/dist/chat.cjs +26 -0
  13. package/dist/chat.cjs.map +1 -0
  14. package/dist/chat.d.ts +14 -0
  15. package/dist/chat.js +21 -0
  16. package/dist/chat.js.map +1 -0
  17. package/dist/constants.cjs +34 -0
  18. package/dist/constants.cjs.map +1 -0
  19. package/dist/constants.d.ts +13 -0
  20. package/dist/constants.js +23 -0
  21. package/dist/constants.js.map +1 -0
  22. package/dist/formatter.cjs +139 -0
  23. package/dist/formatter.cjs.map +1 -0
  24. package/dist/formatter.d.ts +88 -0
  25. package/dist/formatter.js +131 -0
  26. package/dist/formatter.js.map +1 -0
  27. package/dist/items/content.cjs +14 -0
  28. package/dist/items/content.cjs.map +1 -0
  29. package/dist/items/content.d.ts +3 -0
  30. package/dist/items/content.js +10 -0
  31. package/dist/items/content.js.map +1 -0
  32. package/dist/items/context.cjs +13 -0
  33. package/dist/items/context.cjs.map +1 -0
  34. package/dist/items/context.d.ts +3 -0
  35. package/dist/items/context.js +9 -0
  36. package/dist/items/context.js.map +1 -0
  37. package/dist/items/instruction.cjs +13 -0
  38. package/dist/items/instruction.cjs.map +1 -0
  39. package/dist/items/instruction.d.ts +3 -0
  40. package/dist/items/instruction.js +9 -0
  41. package/dist/items/instruction.js.map +1 -0
  42. package/dist/items/parameters.cjs +53 -0
  43. package/dist/items/parameters.cjs.map +1 -0
  44. package/dist/items/parameters.d.ts +5 -0
  45. package/dist/items/parameters.js +47 -0
  46. package/dist/items/parameters.js.map +1 -0
  47. package/dist/items/section.cjs +120 -0
  48. package/dist/items/section.cjs.map +1 -0
  49. package/dist/items/section.d.ts +33 -0
  50. package/dist/items/section.js +115 -0
  51. package/dist/items/section.js.map +1 -0
  52. package/dist/items/trait.cjs +13 -0
  53. package/dist/items/trait.cjs.map +1 -0
  54. package/dist/items/trait.d.ts +3 -0
  55. package/dist/items/trait.js +9 -0
  56. package/dist/items/trait.js.map +1 -0
  57. package/dist/items/weighted.cjs +27 -0
  58. package/dist/items/weighted.cjs.map +1 -0
  59. package/dist/items/weighted.d.ts +24 -0
  60. package/dist/items/weighted.js +22 -0
  61. package/dist/items/weighted.js.map +1 -0
  62. package/dist/loader.cjs +167 -0
  63. package/dist/loader.cjs.map +1 -0
  64. package/dist/loader.d.ts +35 -0
  65. package/dist/loader.js +161 -0
  66. package/dist/loader.js.map +1 -0
  67. package/dist/logger.cjs +51 -0
  68. package/dist/logger.cjs.map +1 -0
  69. package/dist/logger.d.ts +11 -0
  70. package/dist/logger.js +46 -0
  71. package/dist/logger.js.map +1 -0
  72. package/dist/override.cjs +109 -0
  73. package/dist/override.cjs.map +1 -0
  74. package/dist/override.d.ts +31 -0
  75. package/dist/override.js +105 -0
  76. package/dist/override.js.map +1 -0
  77. package/dist/parse/markdown.cjs +114 -0
  78. package/dist/parse/markdown.cjs.map +1 -0
  79. package/dist/parse/markdown.d.ts +3 -0
  80. package/dist/parse/markdown.js +110 -0
  81. package/dist/parse/markdown.js.map +1 -0
  82. package/dist/parse/text.cjs +33 -0
  83. package/dist/parse/text.cjs.map +1 -0
  84. package/dist/parse/text.d.ts +3 -0
  85. package/dist/parse/text.js +29 -0
  86. package/dist/parse/text.js.map +1 -0
  87. package/dist/parser.cjs +99 -0
  88. package/dist/parser.cjs.map +1 -0
  89. package/dist/parser.d.ts +21 -0
  90. package/dist/parser.js +75 -0
  91. package/dist/parser.js.map +1 -0
  92. package/dist/prompt.cjs +15 -0
  93. package/dist/prompt.cjs.map +1 -0
  94. package/dist/prompt.d.ts +16 -0
  95. package/dist/prompt.js +11 -0
  96. package/dist/prompt.js.map +1 -0
  97. package/dist/riotprompt.cjs +1359 -0
  98. package/dist/riotprompt.cjs.map +1 -0
  99. package/dist/riotprompt.d.ts +25 -0
  100. package/dist/riotprompt.js +21 -0
  101. package/dist/riotprompt.js.map +1 -0
  102. package/dist/util/general.cjs +52 -0
  103. package/dist/util/general.cjs.map +1 -0
  104. package/dist/util/general.d.ts +4 -0
  105. package/dist/util/general.js +47 -0
  106. package/dist/util/general.js.map +1 -0
  107. package/dist/util/markdown.cjs +115 -0
  108. package/dist/util/markdown.cjs.map +1 -0
  109. package/dist/util/markdown.d.ts +7 -0
  110. package/dist/util/markdown.js +111 -0
  111. package/dist/util/markdown.js.map +1 -0
  112. package/dist/util/storage.cjs +155 -0
  113. package/dist/util/storage.cjs.map +1 -0
  114. package/dist/util/storage.d.ts +32 -0
  115. package/dist/util/storage.js +132 -0
  116. package/dist/util/storage.js.map +1 -0
  117. package/dist/util/text.cjs +42 -0
  118. package/dist/util/text.cjs.map +1 -0
  119. package/dist/util/text.d.ts +1 -0
  120. package/dist/util/text.js +38 -0
  121. package/dist/util/text.js.map +1 -0
  122. package/docs/loader.md +237 -0
  123. package/docs/override.md +323 -0
  124. package/docs/parser.md +130 -0
  125. package/eslint.config.mjs +82 -0
  126. package/nodemon.json +14 -0
  127. package/package.json +72 -0
  128. package/vite.config.ts +114 -0
  129. package/vitest.config.ts +25 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sources":["../../src/util/text.ts"],"sourcesContent":["import { DEFAULT_CHARACTER_ENCODING } from \"../constants\";\n\n// Returns true if the input is likely text, false if likely binary\nexport function isText(input: string | Buffer): boolean {\n let buf: Buffer;\n if (typeof input === 'string') {\n buf = Buffer.from(input, DEFAULT_CHARACTER_ENCODING);\n } else {\n buf = input;\n }\n\n // Empty buffers are considered text\n if (buf.length === 0) {\n return true;\n }\n\n // If the buffer contains null bytes, it's likely binary\n if (buf.includes(0)) {\n return false;\n }\n\n // For UTF-8 encoded text (including emoji and international characters),\n // convert to string first and check if there are non-printable characters\n const str = buf.toString(DEFAULT_CHARACTER_ENCODING);\n\n // Count the number of non-printable ASCII characters (excluding common whitespace)\n let nonPrintable = 0;\n const len = Math.min(str.length, 512); // Only check the first 512 characters for performance\n\n for (let i = 0; i < len; i++) {\n const charCode = str.charCodeAt(i);\n // Allow: tab (9), line feed (10), carriage return (13), printable ASCII (32-126)\n // Also allow all non-ASCII Unicode characters (charCode > 127)\n if (\n charCode !== 9 && charCode !== 10 && charCode !== 13 &&\n (charCode < 32 || (charCode > 126 && charCode < 128))\n ) {\n nonPrintable++;\n }\n }\n\n // If more than 10% of the checked characters are non-printable, consider it binary\n return nonPrintable / len < 0.1;\n}\n"],"names":["isText","input","buf","Buffer","from","DEFAULT_CHARACTER_ENCODING","length","includes","str","toString","nonPrintable","len","Math","min","i","charCode","charCodeAt"],"mappings":";;AAEA;AACO,SAASA,OAAOC,KAAsB,EAAA;IACzC,IAAIC,GAAAA;IACJ,IAAI,OAAOD,UAAU,QAAU,EAAA;QAC3BC,GAAMC,GAAAA,MAAAA,CAAOC,IAAI,CAACH,KAAOI,EAAAA,0BAAAA,CAAAA;KACtB,MAAA;QACHH,GAAMD,GAAAA,KAAAA;AACV;;IAGA,IAAIC,GAAAA,CAAII,MAAM,KAAK,CAAG,EAAA;QAClB,OAAO,IAAA;AACX;;IAGA,IAAIJ,GAAAA,CAAIK,QAAQ,CAAC,CAAI,CAAA,EAAA;QACjB,OAAO,KAAA;AACX;;;IAIA,MAAMC,GAAAA,GAAMN,GAAIO,CAAAA,QAAQ,CAACJ,0BAAAA,CAAAA;;AAGzB,IAAA,IAAIK,YAAe,GAAA,CAAA;IACnB,MAAMC,GAAAA,GAAMC,KAAKC,GAAG,CAACL,IAAIF,MAAM,EAAE;AAEjC,IAAA,IAAK,IAAIQ,CAAAA,GAAI,CAAGA,EAAAA,CAAAA,GAAIH,KAAKG,CAAK,EAAA,CAAA;QAC1B,MAAMC,QAAAA,GAAWP,GAAIQ,CAAAA,UAAU,CAACF,CAAAA,CAAAA;;;AAGhC,QAAA,IACIC,QAAa,KAAA,CAAA,IAAKA,QAAa,KAAA,EAAA,IAAMA,QAAa,KAAA,EAAA,KACjDA,QAAAA,GAAW,EAAOA,IAAAA,QAAAA,GAAW,GAAOA,IAAAA,QAAAA,GAAW,GAAG,CACrD,EAAA;AACEL,YAAAA,YAAAA,EAAAA;AACJ;AACJ;;AAGA,IAAA,OAAOA,eAAeC,GAAM,GAAA,GAAA;AAChC;;;;"}
package/docs/loader.md ADDED
@@ -0,0 +1,237 @@
1
+ # riotprompt Loader Utility
2
+
3
+ ## Overview
4
+
5
+ The **Loader** utility in riotprompt is designed to import prompt content from the filesystem. It can take a directory (or multiple directories) of Markdown files and convert them into riotprompt sections or items. This is especially useful for managing large or modular prompt structures, where different parts of the prompt (personas, instructions, context, etc.) are kept in separate files for clarity and reusability.
6
+
7
+ By using the Loader, you can:
8
+
9
+ * **Organize Prompts as Files**: Keep complex prompts as separate markdown files (e.g., one file per persona, or per context topic).
10
+ * **Reuse Content**: Share and reuse prompt segments across projects by simply loading the files.
11
+ * **Keep Code and Prompts Separate**: Your application code can load prompt content at runtime, making it easier to update prompts without changing code.
12
+ * **Scale to Many Context Files**: Effortlessly include a large number of context snippets or knowledge base files as part of the prompt by placing them in a context directory and loading them all.
13
+
14
+ > **NOTE:** This was originally designed to support loading context from a directory of Markdown files. For example, if you are analyzing emails, you might want to have a directory named "./context/projects" that contains a series of Markdown files that give context about different projects.
15
+
16
+
17
+ ## How the Loader Transforms Files
18
+
19
+ The Loader works together with the Parser under the hood. When you point the Loader at a directory, it will:
20
+
21
+ 1. **Scan the Directory**: It finds all files in the specified directory (not including subdirectories). The Loader is agnostic about file extensions and processes all files in the directory.
22
+ 2. **Read each File**: For each file found, the Loader reads its content from disk.
23
+ 3. **Parse Content into Sections**: If any of the files are Markdown (ending in .md), the Loader uses the Parser to convert the Markdown content of the file into a Section (or items). Crucially, the Loader will strip out the first heading from the file from the content and use it as the section title if a heading is present. If a particular file does not contain Markdown, a section will be added for the file with the name of the file as the Section title.
24
+
25
+ * If the file content begins with a heading (e.g., `# Title`), that heading is used as the Section's title and is not duplicated in the Section's items.
26
+ * The rest of the file's content becomes the body (items) of that Section.
27
+ * If a file does not contain an explicit top-level heading, the Loader can optionally use the filename (or a provided default) as the section title, or simply treat all content as belonging to an unnamed section.
28
+ 4. **Aggregate or Return Sections**: The Loader then returns the structured content from all files. You can choose to combine these sections into a larger Section (for example, a single "Context" section containing multiple subsections), or handle them individually.
29
+
30
+ This approach means you can, for example, have a directory of background context files and load them all at once, without manually copying and pasting their content into one big prompt string.
31
+
32
+ ## Using the Loader
33
+
34
+ Typically, you'll create a Loader instance via its factory method and then call methods to load files or directories. For example:
35
+
36
+ ```ts
37
+ import { Loader, Section } from '@riotprompt';
38
+
39
+ // Create a Loader instance
40
+ const loader = Loader.create();
41
+
42
+ // Load all markdown files from a directory (e.g., the "context" folder)
43
+ const contextSections: Section<Context>[] = await loader.load<Context>(['./prompts/context']);
44
+
45
+ // contextSections is an array of Section objects, one per markdown file in the directory.
46
+ console.log(`Loaded ${contextSections.length} context sections.`);
47
+ ```
48
+
49
+ In this snippet, `loader.load<Context>(['./prompts/context'])` will find all markdown files under `./prompts/context`. Suppose the folder structure is:
50
+
51
+ ```
52
+ prompts/
53
+ context/
54
+ project-alpha.md
55
+ project-beta.md
56
+ ```
57
+
58
+ And the files contain:
59
+
60
+ * **project-alpha.md**:
61
+
62
+ ```
63
+ # Project Alpha
64
+
65
+ Details about Project Alpha...
66
+ ```
67
+
68
+ * **project-beta.md**:
69
+
70
+ ```
71
+ # Project Beta
72
+
73
+ Information on Project Beta...
74
+ ```
75
+
76
+ After running the loader:
77
+
78
+ * `contextSections[0]` will be a Section titled "Project Alpha", containing the text "Details about Project Alpha..." as its content (as an item in that section).
79
+ * `contextSections[1]` will be a Section titled "Project Beta", with "Information on Project Beta..." as its content.
80
+
81
+ Each file became its own Section, using the first line header as the title.
82
+
83
+ ### Loading Multiple Directories or Mixed Content
84
+
85
+ You can call `load` multiple times or even use Loader methods to load individual files. The riotprompt **Builder** provides higher-level methods (like `loadContext` or `loadContent`) which can take an array of directories or file paths and internally use the Loader to gather them.
86
+
87
+ For example, using the Builder (which internally leverages the Loader):
88
+
89
+ ```ts
90
+ import { Builder, Prompt } from '@riotprompt';
91
+
92
+ const builder = Builder.create({
93
+ basePath: './prompts'
94
+ });
95
+ const prompt: Prompt = await builder
96
+ .addInstructionPath('instructions/code-review.md')
97
+ .loadContext(['./prompts/context', './prompts/additional-context'])
98
+ .build();
99
+ ```
100
+
101
+ In this example, `builder.loadContext([...])` will use the Loader to load all Markdown files from the two specified directories (`./prompts/context` and `./prompts/additional-context`), combining them appropriately into the prompt's context section. You don't have to manually instantiate `Loader` here because `Builder` does it for you.
102
+
103
+ However, if you want fine-grained control, you can use `Loader` directly. For instance, you might want to load a directory of context files, filter or modify them, and then insert them into different parts of the prompt.
104
+
105
+ > NOTE: The Builder example above uses a single `basePath` for simplicity. In most real-world use cases, you'll likely want to include an `overridePath` as well, which allows for customization of prompts without modifying the original files.
106
+
107
+
108
+ **Example – Direct Loader usage:**
109
+
110
+ ```ts
111
+ const loader = Loader.create();
112
+ // Load persona definitions from a directory
113
+ const personaSections: Section<Instruction>[] = await loader.load<Instruction>(['./prompts/personas']);
114
+ // Load context files from multiple directories
115
+ const peopleContext: Section<Context>[] = await loader.load<Context>(['./prompts/context/people']);
116
+ const projectContext: Section<Context>[] = await loader.load<Context>(['./prompts/context/projects']);
117
+
118
+ // You can now, for example, combine these:
119
+ const combinedContext = createSection<Context>("Context");
120
+ for (const sec of [...peopleContext, ...projectContext]) {
121
+ combinedContext.add(sec);
122
+ }
123
+ ```
124
+
125
+ In this example, we manually loaded personas and context from different folders and then merged the context sections into a single Section called "Context". (Note: riotprompt might provide utility methods for combining sections, but doing it manually as shown is also straightforward.)
126
+
127
+ ### Example Folder Structure
128
+
129
+ To illustrate a larger setup, imagine your `./prompts` directory is organized as follows:
130
+
131
+ ```
132
+ prompts/
133
+ personas/
134
+ developer.md
135
+ manager.md
136
+ instructions/
137
+ code-review.md
138
+ summarize.md
139
+ content/
140
+ example-request.md
141
+ context/
142
+ project/
143
+ alpha.md
144
+ beta.md
145
+ team/
146
+ team-info.md
147
+ ```
148
+
149
+ * **Personas** directory contains persona profiles (e.g., "developer" persona, "manager" persona).
150
+ * **Instructions** directory contains different sets of instructions (for different tasks like code review, summarization).
151
+ * **Content** might contain user prompts or example content pieces.
152
+ * **Context** is further divided into subfolders (e.g., project-specific context files, team info files).
153
+
154
+ Using Loader (via Builder or directly), you can easily load all relevant pieces:
155
+
156
+ * `loader.load<Instruction>(['./prompts/personas'])` yields an array of persona Sections.
157
+ * `loader.load<Instruction>(['./prompts/instructions'])` yields Sections for each set of instructions.
158
+ * `loader.load<Context>(['./prompts/context/project'])` and `loader.load<Context>(['./prompts/context/team'])` yield context Sections which you might then combine.
159
+
160
+
161
+
162
+ ### Loader Options
163
+
164
+ The Loader's `create` method may allow some configuration as well. Common configurations include:
165
+
166
+ * **File Extensions**: The Loader is agnostic about file extensions and will include all files in the specified directory. There is one special file, `content.md`, which is parsed first and supplies instructions for the section that will contain sections for each of the included files. Additionally, any file with a `.md` extension will have its first header parsed to set the title of the resulting Section. You do not need to configure file extensions; the Loader handles all files appropriately based on their extension.
167
+ * **Non-Recursive Directory Scanning**: The `load()` method only processes files in the immediate directories specified in the array. It does not automatically traverse subdirectories. If you need to process files in subdirectories, you must explicitly include those subdirectory paths in the array passed to `load()`. For example, to load files from both `./prompts/context/project` and `./prompts/context/team`, you would need to call `load(['./prompts/context/project', './prompts/context/team'])`.
168
+ * **Ignore Patterns**: You can provide an array of regular expression strings via the `ignorePatterns` option in the `Options` interface. Files matching any of these patterns will be excluded from processing. This is useful for ignoring hidden files, temporary files, or specific file types you don't want to load. By default, the Loader uses the following patterns to ignore common non-content files:
169
+ * `^\\..*`: Ignores hidden files (e.g., `.git`, `.DS_Store`).
170
+ * `\\.(jpg|jpeg|png|gif|bmp|svg|webp|ico)$`: Ignores common image file extensions.
171
+ * `\\.(mp3|wav|ogg|aac|flac)$`: Ignores common audio file extensions.
172
+ * `\\.(mp4|mov|avi|mkv|webm)$`: Ignores common video file extensions.
173
+ * `\\.(pdf|doc|docx|xls|xlsx|ppt|pptx)$`: Ignores common document file formats that are typically binary.
174
+ * `\\.(zip|tar|gz|rar|7z)$`: Ignores common compressed file extensions.
175
+ You can override these defaults by passing your own array of regex strings to `ignorePatterns`.
176
+ * **Parameters**: Similar to the Parser, the Loader might accept a `Parameters` object if you want to substitute placeholders in all loaded files. Another approach is to pass `parameters` into the Builder (so that after loading, when formatting, those parameters are applied).
177
+
178
+ ### Parameterized Content Loading
179
+
180
+ The Loader supports parameterized content through the `parameters` option in the `Options` interface. This allows you to define variables in your markdown files and have them replaced with actual values when the content is loaded.
181
+
182
+ Here's how to use this feature:
183
+
184
+ ```ts
185
+ import { Loader, Section } from '@riotprompt';
186
+
187
+ // Create a Loader with parameters
188
+ const loader = Loader.create({
189
+ parameters: {
190
+ projectName: "Alpha Project",
191
+ clientName: "Acme Corporation",
192
+ deadline: "December 31, 2023"
193
+ }
194
+ });
195
+
196
+ // Load content that contains variables
197
+ const contextSections: Section<Context>[] = await loader.load<Context>(['./prompts/context']);
198
+ ```
199
+
200
+ In this example, any markdown file in the `./prompts/context` directory can include variables like `{{projectName}}`, `{{clientName}}`, or `{{deadline}}`, and they will be replaced with the corresponding values when loaded.
201
+
202
+ For instance, if one of your files contains:
203
+
204
+ ```markdown
205
+ # {{projectName}} Overview
206
+
207
+ This document provides key information about {{projectName}} for {{clientName}}.
208
+ The project has a delivery deadline of {{deadline}}.
209
+ ```
210
+
211
+ After loading, this content would be transformed to:
212
+
213
+ ```markdown
214
+ # Alpha Project Overview
215
+
216
+ This document provides key information about Alpha Project for Acme Corporation.
217
+ The project has a delivery deadline of December 31, 2023.
218
+ ```
219
+
220
+ This parameterization feature is extremely useful for:
221
+
222
+ * Creating reusable template prompts
223
+ * Injecting dynamic values into your prompts at runtime
224
+ * Maintaining a single source of truth for key information across multiple prompt files
225
+
226
+ You can change the parameters for each Loader instance, allowing you to generate different versions of the same prompt templates for different scenarios.
227
+
228
+ ### Purpose and Best Practices
229
+
230
+ Using Loader helps manage large prompts by breaking them into maintainable pieces:
231
+
232
+ * **Separation of Concerns**: Keep each logical part of the prompt in its own file (e.g., persona definitions, instructional prompts, background context). This makes editing and reviewing prompts easier.
233
+ * **Dynamic Inclusion**: You can decide at runtime which files or directories to load. For instance, load different instruction sets based on user input or context (by pointing Loader to different folders).
234
+ * **Reusability**: If multiple applications or multiple parts of your application need the same prompt content, having them in files that the Loader can pull in avoids duplication in code.
235
+
236
+ In summary, the Loader utility bridges the gap between your prompt files on disk and the in-memory structures that riotprompt uses. It handles the heavy lifting of reading files and parsing Markdown, so you can assemble your final prompt by simply organizing files and calling load functions. This results in a cleaner project structure and easier prompt maintenance.
237
+
@@ -0,0 +1,323 @@
1
+ # riotprompt Override Utility
2
+
3
+ ## Overview
4
+
5
+ The **Override** utility in riotprompt allows you to customize or replace parts of a prompt without altering the original prompt files. This is particularly useful in larger applications or frameworks (like Cortalyne) where you have default prompt templates but want to adjust certain sections for specific use cases, users, or environments. By using overrides, you can maintain a clean separation between **core prompt content** and **custom modifications**.
6
+
7
+ In essence, overrides let you **selectively replace or augment prompt sections**:
8
+
9
+ * You can completely **override** a section (replace it entirely with new content).
10
+ * You can **prepend** content (insert additional text before the original content).
11
+ * You can **append** content (insert additional text after the original content).
12
+
13
+ All of this is done without modifying the original prompt source file; instead, riotprompt will detect override files and merge or replace content accordingly when building the final prompt.
14
+
15
+ ## How Overrides Work
16
+
17
+ riotprompt's override system works by looking for specially-named files in an "override" directory that correspond to your prompt files. When you build a prompt (for example, using the Builder), you can specify an `overridePath` where your override files live and enable overrides.
18
+
19
+ For each prompt file loaded from the base path, riotprompt will check if there is a corresponding override file. The correspondence is determined by **filename and path**:
20
+
21
+ * If an override file with the *exact same name* exists in the override directory (mirroring the relative path of the original file), riotprompt will treat that as a **full override** for that prompt file.
22
+ * Additionally, riotprompt recognizes two suffix conventions for partial overrides:
23
+
24
+ * A file ending in **`-pre.md`** is treated as content to **prepend** (placed before the base content).
25
+ * A file ending in **`-post.md`** is treated as content to **append** (placed after the base content).
26
+
27
+ This naming scheme allows you to choose the override mode by how you name the file, without needing additional configuration in code for each file.
28
+
29
+ **Example:**
30
+
31
+ Suppose you have a base prompt file `prompts/instructions/email.md` that defines instructions for drafting an email. To modify this via overrides:
32
+
33
+ * Place a file at `overrides/instructions/email.md` – this will completely replace the content of `email.md` when overrides are applied.
34
+ * Place a file at `overrides/instructions/email-pre.md` – this will be inserted *before* the content of the base `email.md`.
35
+ * Place a file at `overrides/instructions/email-post.md` – this will be inserted *after* the content of the base `email.md`.
36
+
37
+ You can use none, one, or all of these in combination as needed. For instance, you might only have a `email-post.md` to add a few extra instructions at the end of the default email instructions, leaving the original content intact.
38
+
39
+ ## Note on Path Configuration
40
+
41
+ When implementing overrides in a real-world application, the paths are often configured strategically:
42
+
43
+ * **basePath** is frequently constructed to point to content from a package, often using variables like `__dirname` to reference the location of the installed package. For example:
44
+
45
+ ```ts
46
+ import path from 'path';
47
+ import { fileURLToPath } from 'url';
48
+
49
+ const __filename = fileURLToPath(import.meta.url);
50
+ const __dirname = path.dirname(__filename);
51
+
52
+ const basePath = path.join(__dirname, '../prompts');
53
+ ```
54
+
55
+ * **overridePath** is typically configured using a project's configuration directory, which might be in the user's workspace or a designated config location:
56
+
57
+ ```ts
58
+ const overridePath = path.join(process.cwd(), 'config/prompts');
59
+ ```
60
+
61
+ This setup creates a powerful pattern where library authors can ship applications with a set of default prompts (in the package's prompts directory), while allowing users to customize those prompts by:
62
+
63
+ 1. **Completely replacing** prompt content by providing override files with the same name
64
+ 2. **Extending** the default prompts by adding content before or after using the `-pre.md` and `-post.md` conventions
65
+
66
+ This approach maintains a clean separation between the library's default content and user customizations, making it easier to update the library without losing custom modifications. It's particularly valuable in frameworks and tools that rely heavily on prompt engineering but need to support user-specific adaptations.
67
+
68
+ **Directory Structure:**
69
+
70
+ The override directory should mirror the structure of the base prompt directory. For example:
71
+
72
+ ```
73
+ prompts/ (base prompt files)
74
+ instructions/
75
+ email.md
76
+ meeting.md
77
+
78
+ overrides/ (override files)
79
+ instructions/
80
+ email-pre.md (prepends content to email.md instructions)
81
+ email-post.md (appends content to email.md instructions)
82
+ meeting.md (completely overrides meeting.md instructions)
83
+ ```
84
+
85
+ When a prompt is configured, the Builder and Override utility will take the paths "instructions/email.md" and "instructions/meeting.md", and the library will change the extension from ".md" to "-pre.md" and "-post.md" when it is looking for override content.
86
+
87
+ In this scenario:
88
+
89
+ * For `email.md`, the builder will find an `email-pre.md` and `email-post.md`. It will prepend and append their content around `email.md`'s content. There is no `email.md` full override in the overrides directory, so the base content is still used (with the additions).
90
+ * For `meeting.md`, a full override file exists in overrides. That means the original `meeting.md` content will be entirely replaced by the content from the override file. (If there were also `meeting-pre.md` or `meeting-post.md`, those would be applied as well, but typically if you're fully overriding, you may not use pre/post for that file.)
91
+
92
+ ## Enabling Overrides in Builder
93
+
94
+ If you are using the `Builder` to assemble prompts, you need to tell it to use the overrides. This is done via the `overridePath` and `overrides` options in `Builder.create()`:
95
+
96
+ ```ts
97
+ const builder = Builder.create({
98
+ basePath: './prompts',
99
+ overridePath: './overrides',
100
+ overrides: true
101
+ });
102
+ ```
103
+
104
+ * `basePath` is where your base prompt files are located.
105
+ * `overridePath` is where your override files are located.
106
+ * `overrides: true` enables the override functionality. (By default, riotprompt might ignore override files unless this flag is set. This is a safety feature to prevent accidental override of content.)
107
+
108
+ When `overrides` is true, the builder will incorporate any found override files as it loads prompt files. If you set an override path but `overrides` is false (or omitted), riotprompt will likely skip applying full overrides. (Prepend/append might still be applied, or they might also be ignored – typically you enable the flag when you intend to use any overrides.)
109
+
110
+ In applications like **Cortalyne**, a command-line flag is used (e.g. `--overrides`) to toggle this behavior. This maps to the `overrides: true` setting in the riotprompt builder.
111
+
112
+ ## Using Override Utility Programmatically
113
+
114
+ While the common usage is via Builder configuration, riotprompt also provides lower-level capabilities to apply overrides if needed.
115
+
116
+ For example, there may be an `Override` class or methods that you can use on Section objects:
117
+
118
+ ```ts
119
+ import { Override, createSection } from '@riotprompt';
120
+
121
+ // Suppose we have a base section created or loaded:
122
+ const baseSection = createSection("Instructions");
123
+ baseSection.add("Follow the company style guide.");
124
+ baseSection.add("Keep the email concise.");
125
+
126
+ // Create an Override instance
127
+ const override = Override.create({
128
+ configDir: './overrides',
129
+ overrides: true
130
+ });
131
+
132
+ // Customize the section using an override file path
133
+ await override.customize('instructions/email.md', baseSection);
134
+
135
+ // Now baseSection might have content prepended, appended, or completely replaced
136
+ // depending on what override files exist in the './overrides' directory:
137
+ // - ./overrides/instructions/email.md (full override)
138
+ // - ./overrides/instructions/email-pre.md (prepend)
139
+ // - ./overrides/instructions/email-post.md (append)
140
+
141
+ console.log(baseSection.items[0].text);
142
+ // Output depends on what override files exist
143
+ ```
144
+
145
+ In this example, `override.customize` takes a file path string ('instructions/email.md'), a section object (baseSection), and optionally section options. Under the hood, this is what Builder would do when it finds override files.
146
+
147
+ Even if you don't call `Override` methods directly, understanding this helps – basically riotprompt will inject the override content at the appropriate place.
148
+
149
+ ## Guidance for Structuring Overrides
150
+
151
+ When using overrides in your project, consider the following best practices:
152
+
153
+ * **Mirror File Structure**: Keep the override files in a parallel structure to the base prompt files. This makes it clear which base file each override is targeting. For example, if the base has `personas/agent.md`, put the override in `overrides/personas/agent.md` (or `agent-pre.md`/`agent-post.md` as needed).
154
+ * **Minimize Full Overrides**: Whenever possible, use prepend (`-pre.md`) or append (`-post.md`) files to adjust content, rather than completely overriding. Prepending/appending is less likely to break core functionality because you are adding to the existing prompt rather than replacing it. Use a full override (`same-name.md`) only when you need to substantially change or replace the entire content.
155
+ * **Use Overrides for Environment-Specific Tweaks**: If you have multiple deployment environments or user customizations, you can maintain different override directories or files for each case. For example, one override file could tweak the tone of instructions for a specific client without affecting the base prompt shared by others.
156
+ * **Test Overrides Thoroughly**: Because overrides can change the behavior of prompts, test with and without overrides enabled (if your app allows). Tools like Cortalyne have a debug mode to show final prompts after overrides – use that to ensure your override content is merging correctly.
157
+ * **Document Your Overrides**: Within your team or project, document what each override file is intended to do. Since they change default behavior, it's good to have a note (even within the file as a comment) about why the override exists.
158
+
159
+ ## Example: Combining Base and Override Content
160
+
161
+ Let's walk through a concrete example. Imagine a base persona file `prompts/personas/assistant.md`:
162
+
163
+ ```markdown
164
+ # Assistant Persona
165
+ - You are a helpful assistant with expertise in marketing.
166
+ - Your tone is friendly and professional.
167
+ ```
168
+
169
+ Now suppose for a specific scenario we want the assistant to adopt a more formal tone, but we don't want to change the base file (since that is the default for other scenarios). We create an override file `overrides/personas/assistant-pre.md` with:
170
+
171
+ ```markdown
172
+ - (Formal Override) Your tone is formal and courteous.
173
+ ```
174
+
175
+ We also create `overrides/personas/assistant-post.md` with:
176
+
177
+ ```markdown
178
+ - Always adhere to corporate communication guidelines.
179
+ ```
180
+
181
+ We run our builder with overrides enabled. What happens?
182
+
183
+ * riotprompt loads the base `assistant.md` persona Section:
184
+
185
+ * Title: "Assistant Persona"
186
+ * Items:
187
+
188
+ 1. "You are a helpful assistant with expertise in marketing."
189
+ 2. "Your tone is friendly and professional."
190
+ * It finds `assistant-pre.md` and `assistant-post.md` in overrides for that path.
191
+ * The content from `assistant-pre.md` ("Your tone is formal and courteous.") is inserted at the **beginning** of the assistant persona's items (before the original items).
192
+ * The content from `assistant-post.md` ("Always adhere to corporate communication guidelines.") is inserted at the **end** of the items list.
193
+ * The resulting Section "Assistant Persona" now has:
194
+
195
+ 1. "(Formal Override) Your tone is formal and courteous."
196
+ 2. "You are a helpful assistant with expertise in marketing."
197
+ 3. "Your tone is friendly and professional."
198
+ 4. "Always adhere to corporate communication guidelines."
199
+
200
+ Notice how the original content was not removed – we only added to it in this case. If we had provided an `assistant.md` in the override directory (a full override), then none of the original items would be kept (they would be replaced entirely by whatever is in that override file).
201
+
202
+ This mechanism is powerful for tweaking behavior on the fly.
203
+
204
+ ## Using Parameters with Overrides
205
+
206
+ The Override utility supports dynamic content customization through parameters. Parameters allow you to define placeholders in your prompt text (e.g., `{{variable}}`) that get replaced with specific values when the prompt is processed. This creates even more flexibility in your override files.
207
+
208
+ ### Example: Dynamic Overrides with Parameters
209
+
210
+ Consider a scenario where you want to customize a chatbot persona for different clients while maintaining a consistent base structure. You can use parameters in your override files to achieve this:
211
+
212
+ ```ts
213
+ import { Override, createSection, createParameters } from '@riotprompt';
214
+
215
+ // Create a base assistant persona
216
+ const assistantPersona = createSection("Assistant Persona");
217
+ assistantPersona.add("You are a helpful customer service AI.");
218
+ assistantPersona.add("You provide clear, accurate information.");
219
+
220
+ // Create parameters for client-specific customization
221
+ const clientParameters = createParameters({
222
+ clientName: "Acme Corporation",
223
+ industry: "manufacturing",
224
+ supportEmail: "support@acme.com"
225
+ });
226
+
227
+ // Create an override instance with parameters
228
+ const override = Override.create({
229
+ configDir: './overrides',
230
+ overrides: true,
231
+ parameters: clientParameters
232
+ });
233
+
234
+ // Apply client-specific overrides with parameter substitution
235
+ await override.customize('personas/assistant.md', assistantPersona);
236
+ ```
237
+
238
+ Now in your override file `./overrides/personas/assistant-post.md`, you can use those parameters:
239
+
240
+ ```markdown
241
+ - You are assisting {{clientName}}, a company in the {{industry}} industry.
242
+ - For additional support, direct customers to {{supportEmail}}.
243
+ ```
244
+
245
+ When processed, the placeholders will be replaced with the actual values provided in the parameters, resulting in:
246
+
247
+ ```
248
+ # Assistant Persona
249
+ - You are a helpful customer service AI.
250
+ - You provide clear, accurate information.
251
+ - You are assisting Acme Corporation, a company in the manufacturing industry.
252
+ - For additional support, direct customers to support@acme.com.
253
+ ```
254
+
255
+ This approach allows you to:
256
+ 1. Maintain a clean separation between core prompt content and customizations
257
+ 2. Dynamically update client-specific information without editing the override files
258
+ 3. Reuse the same override files with different parameter sets for various clients or environments
259
+
260
+ When combined with environment variables or configuration files, this creates a powerful system for maintaining context-aware prompts that can be tailored for different scenarios without duplicating content.
261
+
262
+
263
+ ## Configuring Logging
264
+
265
+ The Override utility supports custom logging to help debug override operations. When creating an Override instance, you can supply a `logger` property in the options:
266
+
267
+ ```ts
268
+ import { Override, Logger } from '@riotprompt';
269
+
270
+ // Create a custom logger
271
+ const myLogger: Logger = {
272
+ debug: (message, ...args) => myCustomLogging.debug(message, args),
273
+ info: (message, ...args) => myCustomLogging.info(message, args),
274
+ warn: (message, ...args) => myCustomLogging.warn(message, args),
275
+ error: (message, ...args) => myCustomLogging.error(message, args),
276
+ verbose: (message, ...args) => myCustomLogging.verbose(message, args),
277
+ silly: (message, ...args) => myCustomLogging.silly(message, args)
278
+ };
279
+
280
+ // Use custom logger with Override
281
+ const override = Override.create({
282
+ configDir: './overrides',
283
+ overrides: true,
284
+ logger: myLogger
285
+ });
286
+ ```
287
+
288
+ > **Note:** You don't have to create your own logger. This interface matches the default Winston logger interface, so you can directly use Winston loggers with riotprompt.
289
+
290
+ The `Logger` interface requires six methods:
291
+
292
+ ```ts
293
+ interface Logger {
294
+ debug: (message: string, ...args: any[]) => void;
295
+ info: (message: string, ...args: any[]) => void;
296
+ warn: (message: string, ...args: any[]) => void;
297
+ error: (message: string, ...args: any[]) => void;
298
+ verbose: (message: string, ...args: any[]) => void;
299
+ silly: (message: string, ...args: any[]) => void;
300
+ }
301
+ ```
302
+
303
+ If you don't provide a logger, riotprompt uses a default logger that maps these methods to the corresponding `console` methods (`console.debug`, `console.info`, etc.), with `verbose` and `silly` both mapping to `console.log`.
304
+
305
+ The logger helps you track:
306
+ - When override files are found
307
+ - Which override files are being applied
308
+ - When content is being prepended, appended, or replaced
309
+ - The final content after all overrides are applied
310
+
311
+ This is particularly useful during development and troubleshooting to understand exactly how your overrides are being applied to your prompt content.
312
+
313
+
314
+ ## Conclusion
315
+
316
+ The Override utility makes riotprompt flexible and extensible:
317
+
318
+ * It enables on-the-fly customization of prompt content.
319
+ * It supports complex use cases like user-specific or context-specific prompt adjustments, all while keeping base prompts clean and general.
320
+ * When building applications (like Cortalyne) on top of riotprompt, consider exposing an override mechanism to your end users or for your configurations. This way, you can ship a set of robust default prompts and still allow modifications without editing those core files.
321
+
322
+ By following a consistent override pattern (filename matching and pre/post suffixes) and using the Builder's override support, you can manage prompt variations systematically and safely.
323
+