prompt_manager 1.0.2 → 1.2.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -27
  3. data/README.md +127 -271
  4. data/lib/pm/configuration.rb +34 -4
  5. data/lib/pm/core_directives.rb +21 -20
  6. data/lib/pm/directive.rb +26 -34
  7. data/lib/pm/directives.rb +55 -16
  8. data/lib/pm/errors.rb +71 -0
  9. data/lib/pm/metadata.rb +110 -9
  10. data/lib/pm/parsed.rb +51 -46
  11. data/lib/pm/render_scope.rb +74 -0
  12. data/lib/pm/renderer.rb +59 -0
  13. data/lib/pm/shell.rb +95 -40
  14. data/lib/pm/version.rb +1 -1
  15. data/lib/pm.rb +116 -57
  16. data/lib/prompt_manager.rb +6 -0
  17. metadata +13 -113
  18. data/.envrc +0 -3
  19. data/.irbrc +0 -14
  20. data/COMMITS.md +0 -196
  21. data/Rakefile +0 -12
  22. data/docs/api/configuration.md +0 -59
  23. data/docs/api/constants.md +0 -60
  24. data/docs/api/index.md +0 -14
  25. data/docs/api/metadata.md +0 -99
  26. data/docs/api/parsed.md +0 -98
  27. data/docs/api/pm-module.md +0 -169
  28. data/docs/api/render-context.md +0 -51
  29. data/docs/architecture/design-decisions.md +0 -70
  30. data/docs/architecture/index.md +0 -6
  31. data/docs/architecture/processing-pipeline.md +0 -112
  32. data/docs/assets/css/custom.css +0 -1
  33. data/docs/assets/images/prompt_manager.gif +0 -0
  34. data/docs/assets/images/prompt_manager.mp4 +0 -0
  35. data/docs/examples/ai-agent-prompts.md +0 -173
  36. data/docs/examples/code-review-prompt.md +0 -107
  37. data/docs/examples/index.md +0 -7
  38. data/docs/examples/multi-file-composition.md +0 -123
  39. data/docs/getting-started/configuration.md +0 -106
  40. data/docs/getting-started/index.md +0 -7
  41. data/docs/getting-started/installation.md +0 -34
  42. data/docs/getting-started/quick-start.md +0 -94
  43. data/docs/guides/comment-stripping.md +0 -64
  44. data/docs/guides/custom-directives.md +0 -180
  45. data/docs/guides/erb-rendering.md +0 -102
  46. data/docs/guides/includes.md +0 -184
  47. data/docs/guides/index.md +0 -11
  48. data/docs/guides/parameters.md +0 -96
  49. data/docs/guides/parsing.md +0 -151
  50. data/docs/guides/shell-expansion.md +0 -108
  51. data/docs/index.md +0 -70
  52. data/mkdocs.yml +0 -181
@@ -1,184 +0,0 @@
1
- # Including Files
2
-
3
- The `include` directive lets you compose prompts from multiple files.
4
-
5
- ## Basic Usage
6
-
7
- ```markdown
8
- ---
9
- title: Full Review
10
- parameters:
11
- code: null
12
- ---
13
- <%= include 'common/header.md' %>
14
-
15
- Review this code:
16
- <%= code %>
17
-
18
- <%= include 'common/footer.md' %>
19
- ```
20
-
21
- ```ruby
22
- parsed = PM.parse('full_review.md')
23
- puts parsed.to_s('code' => File.read('app.rb'))
24
- ```
25
-
26
- ## How Includes Work
27
-
28
- Each included file goes through the full processing pipeline:
29
-
30
- 1. HTML comments stripped
31
- 2. YAML metadata extracted
32
- 3. Shell expansion applied
33
- 4. ERB rendered (with the parent's parameter values)
34
-
35
- The included file's rendered content replaces the `<%= include ... %>` tag.
36
-
37
- ## Path Resolution
38
-
39
- Include paths are resolved relative to the **parent file's directory**:
40
-
41
- ```
42
- prompts/
43
- review.md # contains: <%= include 'common/header.md' %>
44
- common/
45
- header.md # resolved from prompts/common/header.md
46
- ```
47
-
48
- ## Parameter Passing
49
-
50
- The parent's parameter values are passed to included files. Child files can use the same parameter names:
51
-
52
- ```markdown
53
- <!-- parent.md -->
54
- ---
55
- parameters:
56
- name: null
57
- ---
58
- <%= include 'greeting.md' %>
59
- Done.
60
- ```
61
-
62
- ```markdown
63
- <!-- greeting.md -->
64
- ---
65
- title: Greeting
66
- ---
67
- Hello, <%= name %>!
68
- ```
69
-
70
- ```ruby
71
- parsed = PM.parse('parent.md')
72
- parsed.to_s('name' => 'Alice')
73
- #=> "Hello, Alice!\nDone."
74
- ```
75
-
76
- ## Nested Includes
77
-
78
- Includes can be nested. File A can include File B, which includes File C:
79
-
80
- ```markdown
81
- <!-- a.md -->
82
- <%= include 'b.md' %>
83
-
84
- <!-- b.md -->
85
- <%= include 'c.md' %>
86
-
87
- <!-- c.md -->
88
- Content from C
89
- ```
90
-
91
- ## Circular Include Detection
92
-
93
- PM detects circular includes and raises an error:
94
-
95
- ```markdown
96
- <!-- circular_a.md -->
97
- <%= include 'circular_b.md' %>
98
-
99
- <!-- circular_b.md -->
100
- <%= include 'circular_a.md' %>
101
- ```
102
-
103
- ```ruby
104
- PM.parse('circular_a.md').to_s
105
- #=> RuntimeError: Circular include detected: /path/to/circular_a.md
106
- ```
107
-
108
- ## Includes Metadata
109
-
110
- After calling `to_s`, the parent's metadata has an `includes` key with a tree of what was included:
111
-
112
- ```ruby
113
- parsed = PM.parse('full_review.md')
114
- parsed.to_s('code' => source)
115
-
116
- parsed.metadata.includes
117
- #=> [
118
- # {
119
- # path: "/prompts/common/header.md",
120
- # depth: 1,
121
- # metadata: { title: "Header", ... },
122
- # includes: []
123
- # },
124
- # {
125
- # path: "/prompts/common/footer.md",
126
- # depth: 1,
127
- # metadata: { title: "Footer", ... },
128
- # includes: []
129
- # }
130
- # ]
131
- ```
132
-
133
- Nested includes form a tree -- each entry's `includes` array contains its own children.
134
-
135
- The `includes` array is `nil` before `to_s` is called and is reset on each call.
136
-
137
- ## `insert` / `read` — Raw File Insertion
138
-
139
- Use `insert` (or its alias `read`) to insert a file's content verbatim. Unlike `include`, the content is not parsed, shell-expanded, or ERB-rendered:
140
-
141
- ```markdown
142
- ---
143
- title: Code Review
144
- ---
145
- Review this code:
146
-
147
- ```ruby
148
- <%= insert 'src/app.rb' %>
149
- ```
150
- ```
151
-
152
- ### Differences from `include`
153
-
154
- | | `insert` | `include` |
155
- |---|---|---|
156
- | File types | Any | `.md` only |
157
- | ERB in content | Preserved as literal text | Rendered |
158
- | Shell expansion | Not applied | Applied |
159
- | Recursion | None | Nested includes supported |
160
- | Metadata tracking | None | `metadata.includes` tree |
161
-
162
- ### Path resolution
163
-
164
- Same as `include` — paths resolve relative to the parent file's directory. Absolute paths work from any context, including string-parsed prompts.
165
-
166
- ### Missing files
167
-
168
- Raises an error:
169
-
170
- ```ruby
171
- PM.parse("---\n---\n<%= insert '/no/such/file' %>").to_s
172
- #=> RuntimeError: insert: file not found: /no/such/file
173
- ```
174
-
175
- ## Requirements
176
-
177
- - The `include` directive requires file context. Using it with string-parsed prompts raises an error:
178
-
179
- ```ruby
180
- PM.parse("---\n---\n<%= include 'other.md' %>").to_s
181
- #=> RuntimeError: include requires a file context (use PM.parse with a file path)
182
- ```
183
-
184
- - When `erb: false`, the include directive does not execute and `metadata.includes` is an empty array.
data/docs/guides/index.md DELETED
@@ -1,11 +0,0 @@
1
- # Guides
2
-
3
- In-depth guides for each PM feature.
4
-
5
- - [Parsing Prompts](parsing.md) -- File paths vs raw strings, metadata extraction
6
- - [Parameters](parameters.md) -- Required and default parameters, rendering with `to_s`
7
- - [Shell Expansion](shell-expansion.md) -- Environment variables and command substitution
8
- - [ERB Rendering](erb-rendering.md) -- Template evaluation and disabling
9
- - [Including Files](includes.md) -- Composing prompts from multiple files
10
- - [Custom Directives](custom-directives.md) -- Registering your own ERB helpers
11
- - [Comment Stripping](comment-stripping.md) -- HTML comment removal
@@ -1,96 +0,0 @@
1
- # Parameters
2
-
3
- Parameters are declared in the YAML `parameters` hash and used as ERB template variables.
4
-
5
- ## Declaring Parameters
6
-
7
- ```markdown
8
- ---
9
- title: Code Review
10
- parameters:
11
- language: ruby
12
- code: null
13
- style_guide: ~/guides/default.md
14
- ---
15
- Review the following <%= language %> code using the style guide
16
- at <%= style_guide %>:
17
-
18
- <%= code %>
19
- ```
20
-
21
- ## Required vs Default
22
-
23
- - **`null` value** -- the parameter is **required**. `to_s` raises `ArgumentError` if not provided.
24
- - **Any other value** -- used as the **default**. Can be overridden when calling `to_s`.
25
-
26
- ```ruby
27
- parsed = PM.parse('code_review.md')
28
-
29
- parsed.metadata.parameters
30
- #=> {"language" => "ruby", "code" => nil, "style_guide" => "~/guides/default.md"}
31
- ```
32
-
33
- ## Rendering with to_s
34
-
35
- `to_s` accepts a Hash of parameter values:
36
-
37
- ```ruby
38
- # Supply required params, accept defaults for the rest
39
- parsed.to_s('code' => File.read('app.rb'))
40
-
41
- # Override a default
42
- parsed.to_s('code' => File.read('app.py'), 'language' => 'python')
43
- ```
44
-
45
- Symbol keys work too:
46
-
47
- ```ruby
48
- parsed.to_s(code: File.read('app.rb'))
49
- ```
50
-
51
- ### All Defaults
52
-
53
- When every parameter has a default value, `to_s` needs no arguments:
54
-
55
- ```ruby
56
- parsed.to_s #=> renders with all defaults
57
- ```
58
-
59
- ### Missing Required Parameters
60
-
61
- Calling `to_s` without supplying a required parameter raises `ArgumentError`:
62
-
63
- ```ruby
64
- parsed.to_s
65
- #=> ArgumentError: Missing required parameters: code
66
- ```
67
-
68
- The error message lists all missing parameters.
69
-
70
- ## Parameters in Included Files
71
-
72
- When a file includes another file via `<%= include 'child.md' %>`, the parent's parameter values are passed to the child. The child can use the same parameter names:
73
-
74
- ```markdown
75
- <!-- parent.md -->
76
- ---
77
- title: Parent
78
- parameters:
79
- name: null
80
- ---
81
- <%= include 'greeting.md' %>
82
- ```
83
-
84
- ```markdown
85
- <!-- greeting.md -->
86
- ---
87
- title: Greeting
88
- ---
89
- Hello, <%= name %>!
90
- ```
91
-
92
- ```ruby
93
- parsed = PM.parse('parent.md')
94
- parsed.to_s('name' => 'Alice')
95
- #=> "Hello, Alice!"
96
- ```
@@ -1,151 +0,0 @@
1
- # Parsing Prompts
2
-
3
- `PM.parse` is the main entry point. It accepts a file path, a Symbol, a single word, or a raw string.
4
-
5
- ## File Paths
6
-
7
- A source is treated as a file path when it:
8
-
9
- - Is a `Pathname` object
10
- - Responds to `to_path`
11
- - Is a String ending in `.md`
12
- - Is a Symbol (`.md` is appended, e.g. `:code_review` → `code_review.md`)
13
- - Is a single word containing only letters, digits, or underscores (`.md` is appended, e.g. `"code_review"` → `code_review.md`)
14
-
15
- ```ruby
16
- # String file path
17
- parsed = PM.parse('code_review.md')
18
-
19
- # Pathname
20
- parsed = PM.parse(Pathname.new('code_review.md'))
21
-
22
- # Symbol (basename — .md appended)
23
- parsed = PM.parse(:code_review)
24
-
25
- # Single word (basename — .md appended)
26
- parsed = PM.parse('code_review')
27
- ```
28
-
29
- When parsing a file, PM adds these keys to the metadata automatically:
30
-
31
- | Key | Type | Description |
32
- |-----|------|-------------|
33
- | `directory` | String | Absolute path to the file's parent directory |
34
- | `name` | String | Filename (e.g., `code_review.md`) |
35
- | `created_at` | Time | File creation timestamp |
36
- | `modified_at` | Time | Last modification timestamp |
37
-
38
- ### prompts_dir
39
-
40
- Relative file paths are resolved against `PM.config.prompts_dir`:
41
-
42
- ```ruby
43
- PM.configure { |c| c.prompts_dir = '~/.prompts' }
44
-
45
- PM.parse('agents/summarize.md')
46
- #=> reads ~/.prompts/agents/summarize.md
47
- ```
48
-
49
- Absolute paths bypass `prompts_dir`:
50
-
51
- ```ruby
52
- PM.parse('/etc/prompts/system.md')
53
- #=> reads /etc/prompts/system.md regardless of prompts_dir
54
- ```
55
-
56
- ## Raw Strings
57
-
58
- Any other string is parsed directly:
59
-
60
- ```ruby
61
- parsed = PM.parse("---\ntitle: Hello\n---\nContent here")
62
-
63
- parsed.metadata.title #=> "Hello"
64
- parsed.content #=> "\nContent here\n"
65
- ```
66
-
67
- String-parsed prompts do not have `directory`, `name`, `created_at`, or `modified_at` in their metadata.
68
-
69
- !!! warning "Include limitations"
70
- The `include` directive requires file context to resolve relative paths. It raises an error when used with string-parsed prompts.
71
-
72
- ### PM.parse_string vs PM.parse
73
-
74
- Because `PM.parse` treats single words as prompt IDs (appending `.md` and looking them up as files), short strings like `"hello"` or `"summarize"` will trigger a file lookup instead of being parsed as content. Use `PM.parse_string` to always parse a string as content:
75
-
76
- ```ruby
77
- # PM.parse treats single words as prompt IDs
78
- PM.parse('hello') #=> looks for hello.md in prompts_dir
79
- PM.parse('summarize') #=> looks for summarize.md in prompts_dir
80
-
81
- # PM.parse_string always parses as string content
82
- PM.parse_string('hello') #=> Parsed with content "hello"
83
- PM.parse_string('summarize') #=> Parsed with content "summarize"
84
-
85
- # Multi-word strings without .md extension work the same in both
86
- PM.parse("Hello world") #=> Parsed with content "Hello world"
87
- PM.parse_string("Hello world") #=> Parsed with content "Hello world"
88
- ```
89
-
90
- Use `PM.parse_string` when:
91
-
92
- - Your input may be a single word that should not be treated as a file
93
- - You know the source is always a string, never a file path
94
- - You want to skip the file-detection logic entirely
95
-
96
- ## Metadata Extraction
97
-
98
- YAML front-matter is delimited by `---` fences:
99
-
100
- ```markdown
101
- ---
102
- title: My Prompt
103
- provider: openai
104
- model: gpt-4
105
- temperature: 0.3
106
- ---
107
- The prompt content starts here.
108
- ```
109
-
110
- All YAML keys become accessible via dot notation on the `PM::Metadata` object:
111
-
112
- ```ruby
113
- parsed.metadata.title #=> "My Prompt"
114
- parsed.metadata.provider #=> "openai"
115
- parsed.metadata.temperature #=> 0.3
116
- ```
117
-
118
- Boolean keys also get predicate methods:
119
-
120
- ```ruby
121
- parsed.metadata.shell #=> true
122
- parsed.metadata.shell? #=> true
123
- ```
124
-
125
- ### No Metadata
126
-
127
- Files without front-matter are valid. The metadata object is empty and the entire content is preserved:
128
-
129
- ```ruby
130
- parsed = PM.parse("Just plain text")
131
- parsed.metadata.title #=> nil
132
- parsed.content #=> "Just plain text"
133
- ```
134
-
135
- ### Bracket Accessor
136
-
137
- You can also access metadata with bracket notation:
138
-
139
- ```ruby
140
- parsed[:title] #=> "My Prompt"
141
- ```
142
-
143
- ## Pipeline Stages
144
-
145
- After parsing, the content has been through these stages:
146
-
147
- 1. HTML comments stripped
148
- 2. YAML metadata extracted
149
- 3. Shell expansion applied (if `shell: true`)
150
-
151
- ERB rendering happens later, on demand, when `to_s` is called.
@@ -1,108 +0,0 @@
1
- # Shell Expansion
2
-
3
- Shell expansion replaces environment variable references and command substitutions with their values at parse time.
4
-
5
- ## Syntax
6
-
7
- | Pattern | Description | Example |
8
- |---------|-------------|---------|
9
- | `$VAR` | Environment variable | `$USER` |
10
- | `${VAR}` | Environment variable (braced) | `${HOME}` |
11
- | `$(command)` | Command substitution | `$(date +%Y-%m-%d)` |
12
-
13
- ## Environment Variables
14
-
15
- ```markdown
16
- ---
17
- title: Info
18
- ---
19
- Current user: $USER
20
- Home directory: ${HOME}
21
- ```
22
-
23
- After parsing:
24
-
25
- ```ruby
26
- parsed = PM.parse('info.md')
27
- parsed.content
28
- #=> "Current user: dewayne\nHome directory: /Users/dewayne\n"
29
- ```
30
-
31
- Only UPPERCASE variable names are expanded. Lowercase names like `$foo` are left as-is.
32
-
33
- Missing environment variables are replaced with an empty string.
34
-
35
- ## Command Substitution
36
-
37
- ```markdown
38
- ---
39
- title: Context
40
- ---
41
- Date: $(date +%Y-%m-%d)
42
- Branch: $(git rev-parse --abbrev-ref HEAD)
43
- ```
44
-
45
- Commands are executed via the shell and replaced with their stdout (trailing newline stripped).
46
-
47
- ### Nested Commands
48
-
49
- Nested parentheses are handled correctly:
50
-
51
- ```markdown
52
- $(echo $(echo hello))
53
- ```
54
-
55
- ### Failed Commands
56
-
57
- A command that exits with a non-zero status raises `RuntimeError`:
58
-
59
- ```ruby
60
- PM.parse("Output: $(exit 1)")
61
- #=> RuntimeError: Command failed with exit status 1: exit 1
62
- ```
63
-
64
- ## Direct Access
65
-
66
- Shell expansion is available as a standalone method:
67
-
68
- ```ruby
69
- PM.expand_shell("Hello $USER from $(hostname)")
70
- #=> "Hello dewayne from venus"
71
- ```
72
-
73
- ## Disabling Shell Expansion
74
-
75
- Set `shell: false` in the file's YAML metadata:
76
-
77
- ```markdown
78
- ---
79
- title: Raw
80
- shell: false
81
- ---
82
- This $USER reference is preserved as-is.
83
- ```
84
-
85
- Or disable globally:
86
-
87
- ```ruby
88
- PM.configure { |c| c.shell = false }
89
- ```
90
-
91
- Per-file metadata always overrides the global setting. A file with `shell: true` gets shell expansion even when the global default is `false`.
92
-
93
- ## Interaction with ERB
94
-
95
- Shell expansion happens at parse time (step 3 of the pipeline). ERB rendering happens later when `to_s` is called (step 4). This means shell references in the raw template are expanded before ERB sees the content.
96
-
97
- If you need dynamic shell output at render time, use a custom directive instead:
98
-
99
- ```ruby
100
- PM.register(:sh) { |_ctx, cmd| `#{cmd}`.chomp }
101
- ```
102
-
103
- ```markdown
104
- ---
105
- title: Dynamic
106
- ---
107
- Branch: <%= sh 'git rev-parse --abbrev-ref HEAD' %>
108
- ```
data/docs/index.md DELETED
@@ -1,70 +0,0 @@
1
- # PM (PromptManager)
2
-
3
- <table>
4
- <tr>
5
- <td width="40%" align="center" valign="top">
6
- <img src="assets/images/prompt_manager.gif" alt="PromptManager" width="100%"><br>
7
- <em>"Prompts with superpowers"</em>
8
- </td>
9
- <td width="60%" valign="top">
10
- <strong>Parse YAML metadata from markdown, expand shell references, and render ERB templates on demand</strong><br><br>
11
- PM (PromptManager) treats prompt files as composable, parameterized templates. Write prompts in markdown with YAML front matter, shell references, and ERB — PM handles the rest.
12
-
13
- <h3>Key Features</h3>
14
-
15
- <li> <strong>YAML Metadata</strong> - Parse from markdown strings or files<br>
16
- <li> <strong>Shell Expansion</strong> - $VAR, ${VAR}, and $(command) substitution<br>
17
- <li> <strong>ERB Rendering</strong> - On-demand rendering with named parameters<br>
18
- <li> <strong>File Includes</strong> - Compose prompts from multiple files<br>
19
- <li> <strong>Custom Directives</strong> - Register custom methods for ERB templates<br>
20
- <li> <strong>Configurable Pipeline</strong> - Enable/disable stages per prompt or globally<br>
21
- <li> <strong>Comment Stripping</strong> - HTML comments removed before processing
22
- </td>
23
- </tr>
24
- </table>
25
-
26
- ## Processing Pipeline
27
-
28
- Every prompt passes through four stages:
29
-
30
- ```mermaid
31
- graph LR
32
- A[Strip HTML Comments] --> B[Extract YAML Metadata]
33
- B --> C[Shell Expansion]
34
- C --> D["ERB Rendering (on to_s)"]
35
- ```
36
-
37
- 1. **Strip HTML comments** -- `<!-- ... -->` removed before anything else
38
- 2. **Extract YAML metadata** -- Front-matter between `---` fences parsed into `PM::Metadata`
39
- 3. **Shell expansion** -- Environment variables and commands expanded (when `shell: true`)
40
- 4. **ERB rendering** -- Templates evaluated on demand when `to_s` is called (when `erb: true`)
41
-
42
- ## Quick Example
43
-
44
- Given a file `review.md`:
45
-
46
- ```markdown
47
- ---
48
- title: Code Review
49
- parameters:
50
- language: ruby
51
- code: null
52
- ---
53
- Review the following <%= language %> code:
54
-
55
- <%= code %>
56
- ```
57
-
58
- Parse and render:
59
-
60
- ```ruby
61
- require 'pm'
62
-
63
- parsed = PM.parse('review.md')
64
- puts parsed.metadata.title #=> "Code Review"
65
- puts parsed.to_s('code' => source) #=> rendered prompt
66
- ```
67
-
68
- ## Getting Started
69
-
70
- Head to [Installation](getting-started/installation.md) to add PM to your project, then follow the [Quick Start](getting-started/quick-start.md) guide.