prompt_manager 1.0.1 → 1.1.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +130 -25
- data/README.md +127 -263
- data/lib/pm/configuration.rb +34 -4
- data/lib/pm/core_directives.rb +21 -20
- data/lib/pm/directive.rb +26 -34
- data/lib/pm/directives.rb +55 -16
- data/lib/pm/errors.rb +71 -0
- data/lib/pm/metadata.rb +110 -9
- data/lib/pm/parsed.rb +51 -46
- data/lib/pm/render_scope.rb +60 -0
- data/lib/pm/renderer.rb +59 -0
- data/lib/pm/shell.rb +95 -40
- data/lib/pm/version.rb +1 -1
- data/lib/pm.rb +116 -57
- data/lib/prompt_manager.rb +6 -0
- metadata +13 -113
- data/.envrc +0 -3
- data/.irbrc +0 -14
- data/COMMITS.md +0 -196
- data/Rakefile +0 -12
- data/docs/api/configuration.md +0 -59
- data/docs/api/constants.md +0 -60
- data/docs/api/index.md +0 -14
- data/docs/api/metadata.md +0 -99
- data/docs/api/parsed.md +0 -98
- data/docs/api/pm-module.md +0 -131
- data/docs/api/render-context.md +0 -51
- data/docs/architecture/design-decisions.md +0 -70
- data/docs/architecture/index.md +0 -6
- data/docs/architecture/processing-pipeline.md +0 -112
- data/docs/assets/css/custom.css +0 -1
- data/docs/assets/images/prompt_manager.gif +0 -0
- data/docs/assets/images/prompt_manager.mp4 +0 -0
- data/docs/examples/ai-agent-prompts.md +0 -173
- data/docs/examples/code-review-prompt.md +0 -107
- data/docs/examples/index.md +0 -7
- data/docs/examples/multi-file-composition.md +0 -123
- data/docs/getting-started/configuration.md +0 -106
- data/docs/getting-started/index.md +0 -7
- data/docs/getting-started/installation.md +0 -34
- data/docs/getting-started/quick-start.md +0 -81
- data/docs/guides/comment-stripping.md +0 -64
- data/docs/guides/custom-directives.md +0 -180
- data/docs/guides/erb-rendering.md +0 -102
- data/docs/guides/includes.md +0 -184
- data/docs/guides/index.md +0 -11
- data/docs/guides/parameters.md +0 -96
- data/docs/guides/parsing.md +0 -127
- data/docs/guides/shell-expansion.md +0 -108
- data/docs/index.md +0 -70
- data/mkdocs.yml +0 -181
data/README.md
CHANGED
|
@@ -2,180 +2,128 @@
|
|
|
2
2
|
|
|
3
3
|
<table>
|
|
4
4
|
<tr>
|
|
5
|
-
<td width="
|
|
6
|
-
<img src="docs/assets/images/prompt_manager.gif" alt="PromptManager" width="
|
|
5
|
+
<td width="40%" align="center" valign="top">
|
|
6
|
+
<img src="docs/assets/images/prompt_manager.gif" alt="PromptManager" width="100%"><br>
|
|
7
7
|
<em>"Prompts with superpowers"</em>
|
|
8
8
|
</td>
|
|
9
|
-
<td width="
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
<td width="60%" valign="top">
|
|
10
|
+
<strong>Parse YAML front matter from markdown, expand shell references, compose prompts from other files, and render ERB on demand.</strong><br><br>
|
|
11
|
+
PM treats a prompt file as a parameterized template: YAML front matter for metadata and parameters, <code>$VAR</code> / <code>$(command)</code> for environment context, <code><%= ... %></code> for rendering, and <code>include</code> / <code>insert</code> for composition.
|
|
12
|
+
|
|
13
|
+
<h3>Key Features</h3>
|
|
14
|
+
<ul>
|
|
15
|
+
<li><strong>YAML Metadata</strong> - Parse from markdown strings or files</li>
|
|
16
|
+
<li><strong>Shell Expansion</strong> - <code>$VAR</code>, <code>${VAR}</code>, <code>$(command)</code>; <code>\$</code> escapes; code fences are skipped</li>
|
|
17
|
+
<li><strong>ERB Templates</strong> - On-demand rendering with required and default parameters</li>
|
|
18
|
+
<li><strong>Includes</strong> - Compose prompts from other files, with a tracked include tree</li>
|
|
19
|
+
<li><strong>Raw Inserts</strong> - Drop any file in verbatim</li>
|
|
20
|
+
<li><strong>Custom Directives</strong> - Blocks or <code>PM::Directive</code> subclasses, callable from ERB</li>
|
|
21
|
+
<li><strong>Configurable Pipeline</strong> - Enable, disable, or lock stages globally or per file</li>
|
|
22
|
+
<li><strong>Typed Errors</strong> - A <code>PM::Error</code> hierarchy for everything PM raises</li>
|
|
23
|
+
<li><strong>Zero runtime dependencies</strong> - Ruby 4.0.9+ and its standard library</li>
|
|
24
|
+
</ul>
|
|
22
25
|
</td>
|
|
23
26
|
</tr>
|
|
24
27
|
</table>
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```ruby
|
|
29
|
-
require 'pm'
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Configuration
|
|
33
|
-
|
|
34
|
-
Set global defaults with `PM.configure`:
|
|
35
|
-
|
|
36
|
-
```ruby
|
|
37
|
-
PM.configure do |config|
|
|
38
|
-
config.prompts_dir = '~/.prompts' # default: ''
|
|
39
|
-
config.shell = true # default: true
|
|
40
|
-
config.erb = true # default: true
|
|
41
|
-
end
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**`prompts_dir`** is prepended to relative file paths passed to `PM.parse`. Absolute paths bypass it:
|
|
29
|
+
Full documentation: <https://madbomber.github.io/prompt_manager>
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
PM.configure { |c| c.prompts_dir = '/usr/share/prompts' }
|
|
48
|
-
|
|
49
|
-
PM.parse('code_review.md')
|
|
50
|
-
#=> reads /usr/share/prompts/code_review.md
|
|
51
|
-
|
|
52
|
-
PM.parse('/absolute/path/review.md')
|
|
53
|
-
#=> reads /absolute/path/review.md (prompts_dir ignored)
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
**`shell`** and **`erb`** set the global defaults for new parses. Per-file YAML metadata overrides the global setting:
|
|
31
|
+
## Installation
|
|
57
32
|
|
|
58
33
|
```ruby
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# All files now default to shell: false
|
|
62
|
-
# A file with "shell: true" in its YAML still gets shell expansion
|
|
34
|
+
# Gemfile
|
|
35
|
+
gem 'prompt_manager'
|
|
63
36
|
```
|
|
64
37
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```ruby
|
|
68
|
-
PM.config.reset!
|
|
38
|
+
```bash
|
|
39
|
+
gem install prompt_manager
|
|
69
40
|
```
|
|
70
41
|
|
|
71
|
-
Access the current configuration:
|
|
72
|
-
|
|
73
42
|
```ruby
|
|
74
|
-
|
|
75
|
-
PM.config.shell #=> true
|
|
76
|
-
PM.config.erb #=> true
|
|
43
|
+
require 'pm'
|
|
77
44
|
```
|
|
78
45
|
|
|
79
|
-
|
|
46
|
+
Requires Ruby 4.0.9 or later. There are no runtime dependencies.
|
|
80
47
|
|
|
81
|
-
|
|
48
|
+
## Quick start
|
|
82
49
|
|
|
83
|
-
|
|
84
|
-
# File path (String ending in .md or Pathname)
|
|
85
|
-
parsed = PM.parse('code_review.md')
|
|
86
|
-
parsed = PM.parse(Pathname.new('code_review.md'))
|
|
50
|
+
Given `code_review.md`:
|
|
87
51
|
|
|
88
|
-
|
|
89
|
-
parsed = PM.parse(:code_review) #=> parses code_review.md
|
|
90
|
-
parsed = PM.parse('code_review') #=> parses code_review.md
|
|
91
|
-
|
|
92
|
-
# String content
|
|
93
|
-
parsed = PM.parse("---\ntitle: Hello\n---\nContent here")
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
When given a file path, `parse` adds `directory`, `name`, `created_at`, and `modified_at` to the metadata. Both forms run the full processing pipeline.
|
|
97
|
-
|
|
98
|
-
Given a file `code_review.md`:
|
|
99
|
-
|
|
100
|
-
```md
|
|
52
|
+
```markdown
|
|
101
53
|
---
|
|
102
54
|
title: Code Review
|
|
103
|
-
provider: openai
|
|
104
55
|
model: gpt-4
|
|
105
|
-
temperature: 0.3
|
|
106
56
|
parameters:
|
|
107
57
|
language: ruby
|
|
108
58
|
code: null
|
|
109
|
-
style_guide: ~/guides/default.md
|
|
110
59
|
---
|
|
111
|
-
Review the following <%= language %> code
|
|
112
|
-
at <%= style_guide %>:
|
|
60
|
+
Review the following <%= language %> code:
|
|
113
61
|
|
|
114
62
|
<%= code %>
|
|
115
63
|
```
|
|
116
64
|
|
|
117
|
-
Parse it:
|
|
118
|
-
|
|
119
65
|
```ruby
|
|
120
66
|
parsed = PM.parse('code_review.md')
|
|
121
|
-
parsed.metadata.parameters
|
|
122
|
-
#=> {'language' => 'ruby', 'code' => nil, 'style_guide' => '~/guides/default.md'}
|
|
123
|
-
```
|
|
124
67
|
|
|
125
|
-
|
|
68
|
+
parsed.metadata.title #=> "Code Review"
|
|
69
|
+
parsed.metadata.model #=> "gpt-4"
|
|
70
|
+
parsed.parameters #=> {"language" => "ruby", "code" => nil}
|
|
126
71
|
|
|
127
|
-
```ruby
|
|
128
|
-
# Provide required params, accept defaults for the rest
|
|
129
72
|
parsed.to_s('code' => File.read('app.rb'))
|
|
130
|
-
|
|
131
|
-
# Override defaults
|
|
132
73
|
parsed.to_s('code' => File.read('app.py'), 'language' => 'python')
|
|
133
74
|
|
|
134
|
-
# All params have defaults — no arguments needed
|
|
135
|
-
parsed.to_s
|
|
136
|
-
|
|
137
|
-
# Missing a required parameter raises an error
|
|
138
75
|
parsed.to_s
|
|
139
|
-
#=>
|
|
76
|
+
#=> PM::MissingParameterError: Missing required parameters: code
|
|
140
77
|
```
|
|
141
78
|
|
|
142
|
-
|
|
79
|
+
A parameter whose default is `null` is required. Any other default is optional.
|
|
143
80
|
|
|
144
|
-
|
|
81
|
+
## What `PM.parse` accepts
|
|
145
82
|
|
|
146
|
-
|
|
83
|
+
```ruby
|
|
84
|
+
PM.parse('code_review.md') # String ending in .md -> file
|
|
85
|
+
PM.parse(Pathname.new('review.md')) # Pathname -> file
|
|
86
|
+
PM.parse(:code_review) # Symbol -> code_review.md
|
|
87
|
+
PM.parse('code_review') # single word -> code_review.md
|
|
88
|
+
PM.parse("---\ntitle: Hi\n---\nBody") # anything else -> string content
|
|
147
89
|
|
|
148
|
-
|
|
149
|
-
---
|
|
150
|
-
title: Deploy Check
|
|
151
|
-
parameters:
|
|
152
|
-
environment: null
|
|
153
|
-
---
|
|
154
|
-
Current user: $USER
|
|
155
|
-
Home directory: ${HOME}
|
|
156
|
-
Date: $(date +%Y-%m-%d)
|
|
157
|
-
Git branch: $(git rev-parse --abbrev-ref HEAD)
|
|
158
|
-
Deploy to: <%= environment %>
|
|
90
|
+
PM.parse_string('hello') # always string content, never a file lookup
|
|
159
91
|
```
|
|
160
92
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
- Missing environment variables are replaced with an empty string.
|
|
164
|
-
- Failed commands raise an error.
|
|
93
|
+
Relative paths are resolved against `PM.config.prompts_dir`. File parses add
|
|
94
|
+
`directory`, `name`, `created_at`, and `modified_at` to the metadata.
|
|
165
95
|
|
|
166
|
-
|
|
96
|
+
## Processing pipeline
|
|
167
97
|
|
|
168
|
-
|
|
169
|
-
PM
|
|
98
|
+
1. **Strip HTML comments** (`<!-- ... -->`)
|
|
99
|
+
2. **Extract YAML front matter** into `PM::Metadata`
|
|
100
|
+
3. **Shell expansion** of the content, when `shell: true` (the default)
|
|
101
|
+
4. **ERB rendering** when `to_s` is called, when `erb: true` (the default)
|
|
102
|
+
|
|
103
|
+
Stages 3 and 4 can be turned off per file (`shell: false`, `erb: false`) or
|
|
104
|
+
globally (`PM.config.shell = false`). `PM.config.lock!` makes the global values
|
|
105
|
+
final so a file cannot turn a stage back on.
|
|
106
|
+
|
|
107
|
+
## Shell expansion
|
|
108
|
+
|
|
109
|
+
```markdown
|
|
110
|
+
Current user: $USER
|
|
111
|
+
Home: ${HOME}
|
|
112
|
+
Date: $(date +%Y-%m-%d)
|
|
113
|
+
Literal: \$HOME
|
|
170
114
|
```
|
|
171
115
|
|
|
172
|
-
|
|
116
|
+
- `$VAR` and `${VAR}` (UPPERCASE names) are replaced with the environment value, or `""` when unset.
|
|
117
|
+
- `$(command)` runs via the shell and is replaced with its stdout (trailing newline removed).
|
|
118
|
+
- `\$` writes a literal `$`.
|
|
119
|
+
- Command output is spliced in verbatim: a `$VAR` or `<%= %>` that a command prints is **not** expanded or evaluated.
|
|
120
|
+
- Fenced code blocks (``` or ~~~) are copied through untouched.
|
|
121
|
+
- A failing command raises `PM::ShellCommandError` (with `#command`, `#status`, `#stderr`); an unmatched `$(` raises `PM::ShellExpansionError`.
|
|
173
122
|
|
|
174
|
-
|
|
123
|
+
## Composing prompts
|
|
175
124
|
|
|
176
|
-
```
|
|
125
|
+
```markdown
|
|
177
126
|
---
|
|
178
|
-
title: Full Review
|
|
179
127
|
parameters:
|
|
180
128
|
code: null
|
|
181
129
|
---
|
|
@@ -184,182 +132,98 @@ parameters:
|
|
|
184
132
|
Review this code:
|
|
185
133
|
<%= code %>
|
|
186
134
|
|
|
187
|
-
<%= include 'common/footer.md' %>
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
Included files go through the full processing pipeline (comment stripping, metadata extraction, shell expansion, ERB rendering). The parent's parameter values are passed to included files.
|
|
191
|
-
|
|
192
|
-
Nested includes work — A can include B which includes C. Circular includes raise an error.
|
|
193
|
-
|
|
194
|
-
### Inserting raw file content
|
|
195
|
-
|
|
196
|
-
Use `insert` (or its alias `read`) to insert any file's content verbatim. Unlike `include`, the inserted content is not parsed, shell-expanded, or ERB-rendered — it appears as-is:
|
|
197
|
-
|
|
198
|
-
```md
|
|
199
|
-
---
|
|
200
|
-
title: Code Review
|
|
201
|
-
parameters:
|
|
202
|
-
feedback_style: null
|
|
203
|
-
---
|
|
204
|
-
Review this Ruby code:
|
|
205
|
-
|
|
206
|
-
```ruby
|
|
207
135
|
<%= insert 'app/models/user.rb' %>
|
|
208
136
|
```
|
|
209
137
|
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
Paths are resolved relative to the parent file's directory (same as `include`). Absolute paths work from any context. Missing files raise an error.
|
|
214
|
-
|
|
215
|
-
`insert` vs `include`:
|
|
216
|
-
|
|
217
|
-
| | `insert` | `include` |
|
|
138
|
+
| | `include` | `insert` (alias `read`) |
|
|
218
139
|
|---|---|---|
|
|
219
|
-
|
|
|
220
|
-
|
|
|
221
|
-
|
|
|
222
|
-
|
|
|
223
|
-
|
|
|
140
|
+
| Content is | parsed, shell-expanded, and ERB-rendered | copied verbatim |
|
|
141
|
+
| Parameters | parent values passed down | n/a |
|
|
142
|
+
| Nesting | yes, up to `PM.config.max_include_depth` (default 10) | none |
|
|
143
|
+
| Same file twice | allowed; only a true cycle raises `PM::CircularIncludeError` | allowed |
|
|
144
|
+
| Tracked in `metadata.includes` | yes | no |
|
|
224
145
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
parsed = PM.parse('full_review.md')
|
|
229
|
-
parsed.to_s('code' => File.read('app.rb'))
|
|
230
|
-
|
|
231
|
-
parsed.metadata.includes
|
|
232
|
-
#=> [
|
|
233
|
-
# {
|
|
234
|
-
# path: "/prompts/common/header.md",
|
|
235
|
-
# depth: 1,
|
|
236
|
-
# metadata: { title: "Header", ... },
|
|
237
|
-
# includes: []
|
|
238
|
-
# },
|
|
239
|
-
# {
|
|
240
|
-
# path: "/prompts/common/footer.md",
|
|
241
|
-
# depth: 1,
|
|
242
|
-
# metadata: { title: "Footer", ... },
|
|
243
|
-
# includes: []
|
|
244
|
-
# }
|
|
245
|
-
# ]
|
|
246
|
-
```
|
|
146
|
+
Paths resolve relative to the including file's directory. `include` needs a file
|
|
147
|
+
context and raises `PM::IncludeContextError` from a string-parsed prompt.
|
|
247
148
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
#### Block-based registration
|
|
251
|
-
|
|
252
|
-
Register custom methods available in ERB templates:
|
|
149
|
+
## Custom directives
|
|
253
150
|
|
|
254
151
|
```ruby
|
|
255
|
-
PM.register(:env)
|
|
256
|
-
PM.register(:
|
|
152
|
+
PM.register(:env) { |_ctx, key| ENV.fetch(key, '') }
|
|
153
|
+
PM.register(:webpage, :website, :web) { |_ctx, url| fetch_page(url) } # aliases
|
|
257
154
|
```
|
|
258
155
|
|
|
259
|
-
Register multiple names for the same directive (aliases):
|
|
260
|
-
|
|
261
|
-
```ruby
|
|
262
|
-
PM.register(:webpage, :website, :web) { |_ctx, url| fetch_page(url) }
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
#### Class-based directives
|
|
266
|
-
|
|
267
|
-
For organized groups of directives, subclass `PM::Directive`:
|
|
268
|
-
|
|
269
156
|
```ruby
|
|
270
157
|
class MyDirectives < PM::Directive
|
|
271
|
-
desc "
|
|
272
|
-
def
|
|
273
|
-
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
desc "Run a shell command"
|
|
277
|
-
def run(ctx, cmd)
|
|
278
|
-
`#{cmd}`.chomp
|
|
158
|
+
desc "Recent commits"
|
|
159
|
+
def commits(ctx, count = 5)
|
|
160
|
+
`git log --oneline -#{count}`.chomp
|
|
279
161
|
end
|
|
280
|
-
alias_method :
|
|
162
|
+
alias_method :log, :commits
|
|
281
163
|
end
|
|
282
164
|
|
|
283
|
-
# Register all directive subclasses with PM
|
|
284
165
|
PM::Directive.register_all
|
|
285
166
|
```
|
|
286
167
|
|
|
287
|
-
|
|
168
|
+
Every directive receives a `PM::RenderContext` (`directory`, `params`, `included`,
|
|
169
|
+
`depth`, `metadata`) followed by the arguments from the ERB call. Registering a name
|
|
170
|
+
that is already taken raises `PM::DirectiveAlreadyRegisteredError`; `PM.register`
|
|
171
|
+
registers nothing if any requested name is taken. `PM.reset_directives!` restores
|
|
172
|
+
the built-ins (`include`, `insert`, `read`).
|
|
288
173
|
|
|
289
|
-
|
|
174
|
+
Inside a template a bare name resolves to a **parameter** when called without
|
|
175
|
+
arguments, then to a **directive**, then to `nil`. Set
|
|
176
|
+
`PM.config.strict_parameters = true` to make unknown names raise
|
|
177
|
+
`PM::UnknownParameterError` instead.
|
|
178
|
+
|
|
179
|
+
## Configuration
|
|
290
180
|
|
|
291
181
|
```ruby
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
182
|
+
PM.configure do |c|
|
|
183
|
+
c.prompts_dir = '~/.prompts' # default ''
|
|
184
|
+
c.shell = true # default true
|
|
185
|
+
c.erb = true # default true
|
|
186
|
+
c.max_include_depth = 10 # default 10; nil = unlimited
|
|
187
|
+
c.strict_parameters = false # default false
|
|
298
188
|
end
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
#### RenderContext
|
|
302
|
-
|
|
303
|
-
The first argument to every directive block (or class method) is a `PM::RenderContext`:
|
|
304
|
-
|
|
305
|
-
- `ctx.directory` — directory of the file being rendered
|
|
306
|
-
- `ctx.params` — merged parameter values
|
|
307
|
-
- `ctx.metadata` — the current file's metadata
|
|
308
|
-
- `ctx.depth` — include nesting depth
|
|
309
|
-
- `ctx.included` — Set of file paths already in the include chain
|
|
310
|
-
|
|
311
|
-
#### Duplicate detection and reset
|
|
312
189
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
```ruby
|
|
316
|
-
PM.register(:include) { |_ctx, path| path }
|
|
317
|
-
#=> RuntimeError: Directive already registered: include
|
|
190
|
+
PM.config.lock! # per-file shell:/erb: can no longer override the globals
|
|
191
|
+
PM.config.reset! # back to defaults (also unlocks)
|
|
318
192
|
```
|
|
319
193
|
|
|
320
|
-
|
|
194
|
+
## Exporting
|
|
321
195
|
|
|
322
196
|
```ruby
|
|
323
|
-
|
|
324
|
-
|
|
197
|
+
parsed.to_h #=> { metadata: {...}, content: "..." }
|
|
198
|
+
parsed.to_markdown #=> front matter + content, re-parseable with PM.parse_string
|
|
325
199
|
```
|
|
326
200
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
Set `shell: false` or `erb: false` in the metadata to skip those stages:
|
|
330
|
-
|
|
331
|
-
```md
|
|
332
|
-
---
|
|
333
|
-
title: Raw Template
|
|
334
|
-
shell: false
|
|
335
|
-
erb: false
|
|
336
|
-
---
|
|
337
|
-
This $USER and <%= name %> content is preserved as-is.
|
|
338
|
-
```
|
|
201
|
+
## Errors
|
|
339
202
|
|
|
340
|
-
|
|
203
|
+
Everything PM raises itself descends from `PM::Error`, except parameter errors,
|
|
204
|
+
which descend from `ArgumentError` for backward compatibility. Standard-library
|
|
205
|
+
errors pass through: `Errno::ENOENT` for a missing file, `Psych::SyntaxError` for
|
|
206
|
+
bad YAML, `SyntaxError` for bad ERB. See the
|
|
207
|
+
[Errors reference](https://madbomber.github.io/prompt_manager/api/errors/).
|
|
341
208
|
|
|
342
|
-
|
|
209
|
+
## Security model
|
|
343
210
|
|
|
344
|
-
|
|
211
|
+
Prompt files are **code**: ERB can run any Ruby, `$(...)` runs shell commands, and
|
|
212
|
+
`include` / `insert` read any path the process can. Only parse files you trust.
|
|
213
|
+
Command output and inserted file contents are inert (never expanded or evaluated),
|
|
214
|
+
and `PM.config.lock!` keeps a file from re-enabling a stage you turned off. See the
|
|
215
|
+
[Security guide](https://madbomber.github.io/prompt_manager/guides/security/).
|
|
345
216
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
---
|
|
349
|
-
title: My Prompt
|
|
350
|
-
---
|
|
351
|
-
Content here. <!-- This too -->
|
|
352
|
-
```
|
|
217
|
+
PM is not thread-safe for configuration or directive registration; configure and
|
|
218
|
+
register at boot, then parse and render from any thread.
|
|
353
219
|
|
|
354
|
-
|
|
220
|
+
## Development
|
|
355
221
|
|
|
356
|
-
```
|
|
357
|
-
|
|
222
|
+
```bash
|
|
223
|
+
bundle install
|
|
224
|
+
bundle exec rake test # runs the suite with a 100% line-coverage gate
|
|
358
225
|
```
|
|
359
226
|
|
|
360
|
-
##
|
|
227
|
+
## License
|
|
361
228
|
|
|
362
|
-
|
|
363
|
-
2. Extract YAML metadata and markdown content
|
|
364
|
-
3. Shell expansion (`$ENVAR`, `$(command)`) when `shell: true`
|
|
365
|
-
4. ERB rendering on demand via `to_s` when `erb: true`
|
|
229
|
+
MIT. See [LICENSE](LICENSE).
|
data/lib/pm/configuration.rb
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PM
|
|
4
|
+
# Global settings. Reach it through PM.config or PM.configure.
|
|
5
|
+
#
|
|
6
|
+
# prompts_dir -- prepended to relative paths given to PM.parse ('' = none)
|
|
7
|
+
# shell -- default for shell expansion when a file does not say
|
|
8
|
+
# erb -- default for ERB rendering when a file does not say
|
|
9
|
+
# locked -- when true, per-file `shell:` / `erb:` cannot override the globals
|
|
10
|
+
# max_include_depth -- deepest allowed `include` nesting (nil = unlimited)
|
|
11
|
+
# strict_parameters -- when true, an unknown name in a template raises instead of rendering ""
|
|
4
12
|
class Configuration
|
|
5
|
-
attr_accessor :prompts_dir, :shell, :erb
|
|
13
|
+
attr_accessor :prompts_dir, :shell, :erb, :max_include_depth
|
|
14
|
+
attr_writer :locked, :strict_parameters
|
|
15
|
+
|
|
16
|
+
DEFAULT_MAX_INCLUDE_DEPTH = 10
|
|
6
17
|
|
|
7
18
|
def initialize
|
|
8
19
|
reset!
|
|
9
20
|
end
|
|
10
21
|
|
|
11
22
|
def reset!
|
|
12
|
-
@prompts_dir
|
|
13
|
-
@shell
|
|
14
|
-
@erb
|
|
23
|
+
@prompts_dir = ''
|
|
24
|
+
@shell = true
|
|
25
|
+
@erb = true
|
|
26
|
+
@locked = false
|
|
27
|
+
@max_include_depth = DEFAULT_MAX_INCLUDE_DEPTH
|
|
28
|
+
@strict_parameters = false
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Stop per-file front matter from overriding the global shell/erb settings.
|
|
33
|
+
def lock!
|
|
34
|
+
@locked = true
|
|
35
|
+
self
|
|
15
36
|
end
|
|
37
|
+
|
|
38
|
+
def unlock!
|
|
39
|
+
@locked = false
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def locked? = @locked
|
|
44
|
+
|
|
45
|
+
def strict_parameters? = @strict_parameters
|
|
16
46
|
end
|
|
17
47
|
end
|
data/lib/pm/core_directives.rb
CHANGED
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
#
|
|
5
|
-
# PM's built-in directives: include, insert/read.
|
|
6
|
-
# Defined as a PM::Directive subclass using the desc/alias DSL.
|
|
3
|
+
require 'pathname'
|
|
7
4
|
|
|
8
5
|
module PM
|
|
6
|
+
# PM's built-in directives: include, insert (alias read).
|
|
9
7
|
class CoreDirectives < Directive
|
|
10
8
|
desc "Include and render another prompt file"
|
|
11
9
|
def include(ctx, path)
|
|
12
10
|
unless ctx.directory
|
|
13
|
-
raise 'include requires a file context (use PM.parse with a file path)'
|
|
11
|
+
raise IncludeContextError, 'include requires a file context (use PM.parse with a file path)'
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
full_path = File.expand_path(path, ctx.directory)
|
|
15
|
+
check_include_allowed!(ctx, full_path)
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
raise "Circular include detected: #{full_path}"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
child = PM.parse(full_path)
|
|
17
|
+
child = PM.parse(Pathname.new(full_path)) # Pathname: always read as a file, whatever the extension
|
|
23
18
|
result = child.render_with(ctx.params, ctx.included, ctx.depth + 1)
|
|
24
19
|
|
|
25
20
|
ctx.metadata.includes << {
|
|
26
21
|
path: full_path,
|
|
27
22
|
depth: ctx.depth + 1,
|
|
28
|
-
metadata: child.metadata.to_h.reject { |
|
|
23
|
+
metadata: child.metadata.to_h.reject { |key, _| key == :includes },
|
|
29
24
|
includes: child.metadata.includes
|
|
30
25
|
}
|
|
31
26
|
|
|
@@ -34,18 +29,24 @@ module PM
|
|
|
34
29
|
|
|
35
30
|
desc "Insert a file's raw content verbatim (no ERB, no shell expansion)"
|
|
36
31
|
def insert(ctx, path)
|
|
37
|
-
full_path =
|
|
38
|
-
|
|
39
|
-
else
|
|
40
|
-
File.expand_path(path)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
unless File.exist?(full_path)
|
|
44
|
-
raise "insert: file not found: #{full_path}"
|
|
45
|
-
end
|
|
32
|
+
full_path = File.expand_path(path, ctx.directory)
|
|
33
|
+
raise FileNotFoundError, "insert: file not found: #{full_path}" unless File.file?(full_path)
|
|
46
34
|
|
|
47
35
|
File.read(full_path)
|
|
48
36
|
end
|
|
49
37
|
alias_method :read, :insert
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def check_include_allowed!(ctx, full_path)
|
|
42
|
+
if ctx.included.include?(full_path)
|
|
43
|
+
raise CircularIncludeError, "Circular include detected: #{full_path}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
max = PM.config.max_include_depth
|
|
47
|
+
return unless max && ctx.depth + 1 > max
|
|
48
|
+
|
|
49
|
+
raise IncludeDepthError, "Include depth exceeds #{max} (PM.config.max_include_depth): #{full_path}"
|
|
50
|
+
end
|
|
50
51
|
end
|
|
51
52
|
end
|