PM (PromptManager)
Full documentation: https://madbomber.github.io/prompt_manager
Installation
# Gemfile
gem 'prompt_manager'gem install prompt_managerrequire 'pm'Requires Ruby 4.0.9 or later. There are no runtime dependencies.
Quick start
Given code_review.md:
---
title: Code Review
model: gpt-4
parameters:
language: ruby
code: null
---
Review the following <%= language %> code:
<%= code %>parsed = PM.parse('code_review.md')
parsed.metadata.title #=> "Code Review"
parsed.metadata.model #=> "gpt-4"
parsed.parameters #=> {"language" => "ruby", "code" => nil}
parsed.to_s('code' => File.read('app.rb'))
parsed.to_s('code' => File.read('app.py'), 'language' => 'python')
parsed.to_s
#=> PM::MissingParameterError: Missing required parameters: codeA parameter whose default is null is required. Any other default is optional.
What PM.parse accepts
PM.parse('code_review.md') # String ending in .md -> file
PM.parse(Pathname.new('review.md')) # Pathname -> file
PM.parse(:code_review) # Symbol -> code_review.md
PM.parse('code_review') # single word -> code_review.md
PM.parse("---\ntitle: Hi\n---\nBody") # anything else -> string content
PM.parse_string('hello') # always string content, never a file lookupRelative paths are resolved against PM.config.prompts_dir. File parses add
directory, name, created_at, and modified_at to the metadata.
Processing pipeline
-
Strip HTML comments (
<!-- ... -->) -
Extract YAML front matter into
PM::Metadata -
Shell expansion of the content, when
shell: true(the default) -
ERB rendering when
to_sis called, whenerb: true(the default)
Stages 3 and 4 can be turned off per file (shell: false, erb: false) or
globally (PM.config.shell = false). PM.config.lock! makes the global values
final so a file cannot turn a stage back on.
Shell expansion
Current user: $USER
Home: ${HOME}
Date: $(date +%Y-%m-%d)
Literal: \$HOME-
$VARand${VAR}(UPPERCASE names) are replaced with the environment value, or""when unset. -
$(command)runs via the shell and is replaced with its stdout (trailing newline removed). -
\$writes a literal$. - Command output is spliced in verbatim: a
$VARor<%= %>that a command prints is not expanded or evaluated. - Fenced code blocks (``` or ~~~) are copied through untouched.
- A failing command raises
PM::ShellCommandError(with#command,#status,#stderr); an unmatched$(raisesPM::ShellExpansionError.
Composing prompts
---
parameters:
code: null
---
<%= include 'common/header.md' %>
Review this code:
<%= code %>
<%= insert 'app/models/user.rb' %>include |
insert (alias read) |
|
|---|---|---|
| Content is | parsed, shell-expanded, and ERB-rendered | copied verbatim |
| Parameters | parent values passed down | n/a |
| Nesting | yes, up to PM.config.max_include_depth (default 10) |
none |
| Same file twice | allowed; only a true cycle raises PM::CircularIncludeError
|
allowed |
Tracked in metadata.includes
|
yes | no |
Paths resolve relative to the including file's directory. include needs a file
context and raises PM::IncludeContextError from a string-parsed prompt.
Custom directives
PM.register(:env) { |_ctx, key| ENV.fetch(key, '') }
PM.register(:webpage, :website, :web) { |_ctx, url| fetch_page(url) } # aliasesclass MyDirectives < PM::Directive
desc "Recent commits"
def commits(ctx, count = 5)
`git log --oneline -#{count}`.chomp
end
alias_method :log, :commits
end
PM::Directive.register_allEvery directive receives a PM::RenderContext (directory, params, included,
depth, metadata) followed by the arguments from the ERB call. Registering a name
that is already taken raises PM::DirectiveAlreadyRegisteredError; PM.register
registers nothing if any requested name is taken. PM.reset_directives! restores
the built-ins (include, insert, read).
Inside a template a bare name resolves to a parameter when called without
arguments, then to a directive, then to nil. Set
PM.config.strict_parameters = true to make unknown names raise
PM::UnknownParameterError instead.
Configuration
PM.configure do |c|
c.prompts_dir = '~/.prompts' # default ''
c.shell = true # default true
c.erb = true # default true
c.max_include_depth = 10 # default 10; nil = unlimited
c.strict_parameters = false # default false
end
PM.config.lock! # per-file shell:/erb: can no longer override the globals
PM.config.reset! # back to defaults (also unlocks)Exporting
parsed.to_h #=> { metadata: {...}, content: "..." }
parsed.to_markdown #=> front matter + content, re-parseable with PM.parse_stringErrors
Everything PM raises itself descends from PM::Error, except parameter errors,
which descend from ArgumentError for backward compatibility. Standard-library
errors pass through: Errno::ENOENT for a missing file, Psych::SyntaxError for
bad YAML, SyntaxError for bad ERB. See the
Errors reference.
Security model
Prompt files are code: ERB can run any Ruby, $(...) runs shell commands, and
include / insert read any path the process can. Only parse files you trust.
Command output and inserted file contents are inert (never expanded or evaluated),
and PM.config.lock! keeps a file from re-enabling a stage you turned off. See the
Security guide.
PM is not thread-safe for configuration or directive registration; configure and register at boot, then parse and render from any thread.
Development
bundle install
bundle exec rake test # runs the suite with a 100% line-coverage gateLicense
MIT. See LICENSE.
