Search, versions, manifests, archives and the index, over HTTP.
The route shape is the decision worth recording. Package ids contain `/`, so
the obvious /packages/{id}/{version} is ambiguous under a greedy path
parameter. Rather than invent an HTTP-specific identifier, the routes speak the
format's own <registry>:<id>@<version> syntax and parse it — `:` and `@` are
both legal in a path segment, and each route keeps a distinct prefix so
greediness cannot swallow a neighbouring one. The API therefore exercises
section 3.2's reference notation instead of working around it.
A bare id present in more than one registry returns 409 with the candidates,
never a guess. 409 rather than 300 because the request is answerable once the
caller says which registry they meant. Omitting a version applies section
17.1's selector rules, so a prerelease is never chosen implicitly.
Validation is delegated to reference/, installed into the service environment
rather than reimplemented. One validator means the service and the CLI cannot
disagree about what a valid package is; a service accepting something the CLI
rejects would be the divergence this project exists to prevent. Importing it is
not changing it — reference/ stays the dependency-light conformance witness.
Storage keeps the format's distinctions: an immutable package version, its
files as content rather than parsed rows, and an index entry recording arrival.
Only reserved paths and manifest-referenced files are stored (section 2), and a
re-publish of identical content is accepted while different content under the
same id@version is a conflict (section 17).
Handles a real test-vs-production difference: SQLite autoincrements INTEGER
PRIMARY KEY only, never BIGINT, so the SQLite-backed tests could not insert a
row. BigInteger().with_variant(Integer, "sqlite") keeps BIGINT on PostgreSQL
while letting the tests exercise the same models and migration.
Verified live against a seeded store holding this repo's examples and four
helix-forge prompt packages. Service tests 11 -> 22.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
|
||
|---|---|---|
| .. | ||
| canned_prompts_reference.egg-info | ||
| tests | ||
| canned_prompts.py | ||
| pyproject.toml | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements.txt | ||
canned-prompts reference CLI
This is intentionally a small reference implementation, not the intended final architecture.
It demonstrates nine verbs:
add PATH
search QUERY
show ID
resolve ID --set key=value
render ID --set key=value
eval ID
index [ID]
install ID [--version VERSION]
publish PATH
The implementation uses a local catalog plus a filesystem registry and performs no model calls.
resolve and render are separate because the specification separates them
(§ 5.1): resolution decides each value and may be non-deterministic, rendering
substitutes and always is. resolve prints where every value came from —
supplied, default, or fallback — before any prompt is produced.
Stores
Default locations:
~/.canned-prompts/catalog
~/.canned-prompts/default
A registry stores packages flat, because an id is unambiguous within one registry:
<registry>/<id path>/<version>/...
A catalog is namespaced by registry, because identity is registry-scoped (§ 3.2) and the same id may be installed from more than one place:
<catalog>/<registry name>/<id path>/<version>/...
For example:
~/.canned-prompts/catalog/default/practice/pqrst-estimate/1.0.0/
~/.canned-prompts/catalog/local/practice/pqrst-estimate/1.0.0/
A registry's name comes from its optional registry.yaml, and otherwise from
its directory basename — which is why the default registry directory is called
default and not registry: the basename reaches qualified references,
catalog paths and index rows, and registry:practice/thing reads poorly. A
store left at the old ~/.canned-prompts/registry is reported with the command
to move it, rather than a fresh empty one being created beside it. add takes a package from a path rather than a
registry, so it files it under local (override with --as).
Commands that take an ID accept a bare id or a qualified <registry>:<id>.
A bare id installed from more than one registry is reported as ambiguous
rather than resolved by guessing.
Design choices
- YAML manifest via PyYAML.
{{ name }}template substitution only.- No arbitrary expression/code execution.
- Published versions are immutable by default.
add,publishandinstallcopy only reserved paths and manifest- referenced files. A working directory's.git, virtualenv or scratch files never ship; whatever is left out is named on stderr.- Version precedence follows SemVer: a prerelease ranks below its release, and
any,newestand>=skip prereleases entirely. Only an exact pin selects one. installcopies from registry to catalog.addcopies a package directly to catalog.search,show,resolve, andrenderoperate on catalog packages, and print qualified<registry>:<id>references.includedefaults are satisfied (inclusion is deterministic);derivedefaults are reported, not run. Inclusion cycles are detected and named.resolve,renderandevalwarn when an included value will render more than once — two inputs including the same thing, or an included package inheriting an outer input of the same name. Nothing is deduplicated; the fix is a better factoring.evalruns the deterministic render checks of any eval declaring thecanned-prompts/eval-rubric/v0.1schema, and reports output criteria as declared but not run. Unrecognized schemas are skipped, not rejected. A failed render check exits non-zero.resolvelists required capabilities and context dependencies, which this tool cannot verify, rather than implying it checked them.addandinstallname declared prompt dependencies the catalog cannot satisfy. They do not fetch them — resolution is the consumer's job (§ 10) — but a package that looks installed and cannot render should say so.add,installandpublishrecord an entry in the store'sindex.yaml: source, method, first-inclusion date, and the package's declared author, source and licence.indexlists it. Re-adding keeps the originalincluded_atand updateslast_seen_at.- An optional
registry.yamlnames a registry and records namespace claims.publishwarns when a namespace is declaredclosed— it cannot authenticate a publisher, and says so rather than implying it checked. - Static input defaults are applied; derived defaults (§ 6.1) are not. This
tool never calls a model, so a derived default is satisfied only by its
static fallback
value. Without one,resolvereports the input as unresolved andrenderrefuses rather than substituting empty text.
Use this implementation to challenge the format. Replace it once real usage reveals the right architecture.