Composable prompt builder with deferred file / git expansion.
A Prompt is a pure value: the builders (new/1, prepend/2,
append/2, attach/2, git_diff/2, git_log/2, git_status/1,
vars/2) only record intent. All IO -- reading attached files, shelling
out to git -- happens in render/1, which assembles the final string in
a fixed order:
prepends -> base -> context (attach / git) -> appendsEach section is separated from the next by a blank line, and within the context section the attach/git blocks appear in the order their builder calls were made (interleaved exactly as you wrote them).
This split keeps composition cheap and total -- you can build a prompt
without touching the filesystem -- and concentrates every fallible
operation in render/1, which returns a tagged tuple.
Example
iex> ClaudeWrapper.Prompt.new("Review this change")
...> |> ClaudeWrapper.Prompt.prepend("You are a careful reviewer.")
...> |> ClaudeWrapper.Prompt.append("Focus on correctness.")
...> |> ClaudeWrapper.Prompt.render()
{:ok, "You are a careful reviewer.\n\nReview this change\n\nFocus on correctness."}Context blocks
Attached files and git output are emitted as fenced blocks, each headed
by a # <source> comment line:
attach/2-- one# <path>block per matched text file. Globs are expanded withPath.wildcard/1and sorted; a glob matching nothing is an error. Files that are not valid UTF-8, or exceed 256 KB, are skipped.git_diff/2--# git diff(working tree) or# git diff <ref>, inside a```difffence.git_log/2--# git log --oneline -n N(default 5).git_status/1--# git status --short.
A git block whose output is empty (clean tree, no commits, no changes) contributes nothing rather than an empty fence.
Template variables
vars/2 records {{key}} substitutions applied to the authored text
(base / prepends / appends) at render time; captured context blocks are
left verbatim. Unknown {{...}} slots are left untouched.
iex> ClaudeWrapper.Prompt.new("Summarize {{file}} for a {{who}} reader")
...> |> ClaudeWrapper.Prompt.vars(file: "config.ex", who: "beginner")
...> |> ClaudeWrapper.Prompt.render()
{:ok, "Summarize config.ex for a beginner reader"}
Summary
Functions
Add a block after everything else.
Record a file path or glob to attach.
Record a git diff to include.
Record a git log --oneline -n N block (default N = 5).
Record a git status --short block.
Start a new prompt from its base text.
Add a block before the base text.
Render the prompt to its final string.
Like render/1 but returns the string directly, raising
ClaudeWrapper.Error on failure.
Record {{key}} template substitutions.
Types
@type context_item() :: {:attach, String.t()} | {:diff, String.t() | nil} | {:log, pos_integer()} | :status
A recorded context item, expanded at render time in call order.
{:attach, glob}-- a path or glob; each matched text file becomes a fenced, path-headed block.{:diff, ref}-- agit diff;nilis the working tree, a binary is a ref/commit to diff against.{:log, n}-- agit log --oneline -n nblock.:status-- agit status --shortblock.
Functions
Add a block after everything else.
Multiple appends render in the order they were added, after the context section.
Record a file path or glob to attach.
Nothing is read now; the glob is expanded at render/1 time with
Path.wildcard/1, sorted, and each text file is emitted as a fenced,
path-headed code block. Recorded in call order relative to the other
context builders.
Record a git diff to include.
ref is nil for the working tree, or a ref/commit string to diff
against (git diff <ref>). Produced at render/1 time under a
# git diff header inside a ```diff fence. Recorded in call
order relative to the other context builders.
Record a git log --oneline -n N block (default N = 5).
Options:
:n-- number of commits (default 5).
Produced at render/1 time under a # git log header. Recorded in call
order relative to the other context builders.
Record a git status --short block.
Produced at render/1 time under a # git status header. A clean
working tree contributes no block. Recorded in call order relative to
the other context builders.
Start a new prompt from its base text.
iex> ClaudeWrapper.Prompt.new("hello").base
"hello"
Add a block before the base text.
Multiple prepends render in the order they were added, ahead of the base.
@spec render(t()) :: {:ok, String.t()} | {:error, ClaudeWrapper.Error.t()}
Render the prompt to its final string.
Performs all deferred IO -- reads attached files, runs git -- substitutes template vars in the authored text, and joins the sections (prepends, base, context, appends) with blank lines.
Returns {:error, %ClaudeWrapper.Error{}} if a glob matches no files
(:not_found), or if a git command fails (:git_failed) or git is
unavailable (:git_unavailable). The working directory for file globs
and git is the current process cwd.
Like render/1 but returns the string directly, raising
ClaudeWrapper.Error on failure.
Record {{key}} template substitutions.
Applied at render/1 time to the authored text (base, prepends,
appends) only -- captured context blocks (attach/git_*) are left
verbatim. Accepts a map or keyword list; keys and values are stringified.
Unknown {{...}} placeholders are left untouched. Repeatable (later
calls merge, last wins).
iex> ClaudeWrapper.Prompt.new("hi {{name}}").vars
%{}