promptdef
v0.1.0
Published
Typed, versioned prompt templates with variable interpolation, token estimation, and diff tracking
Maintainers
Readme
promptdef
Typed, versioned prompt templates for LLM applications — with variable interpolation, token estimation, pre-flight validation, diff tracking, and a registry for storing and managing prompt versions.
npm install promptdefWhy
Most teams store prompts as raw strings in code or config files. There is no type safety on variables (you find out at runtime that {{userName}} was never filled), no token budget awareness before sending, and no history of what changed between versions.
promptdef makes prompts first-class citizens in a codebase.
Quick start
import { definePrompt, renderPrompt } from 'promptdef'
const summarize = definePrompt({
id: 'summarize-article',
version: '1.0.0',
template: `
You are a {{tone}} summarizer.
Summarize this article in {{maxSentences}} sentences.
Article: {{article}}
`,
variables: {
tone: { type: 'string', default: 'neutral' },
maxSentences: { type: 'number', required: true, min: 1, max: 10 },
article: { type: 'string', required: true },
},
})
const result = renderPrompt(summarize, {
maxSentences: 3,
article: 'The Federal Reserve raised interest rates by 25bp...',
})
result.text // final string, ready to send
result.tokenCount // estimated tokens (no API call needed)
result.variables // the values that were interpolated
result.meta // { id, version }Type safety
Variable types are inferred at compile time. TypeScript catches problems before you run anything.
renderPrompt(summarize, {
maxSentences: 'three', // ❌ Type error: expected number
// article omitted // ❌ Type error: required field missing
})A variable is required when it has no default and no required: false. A variable is optional when it has a default value or required: false — and gets a ? in the inferred type.
Supported variable types:
| Type | TypeScript type | Extra constraints |
|-----------|-----------------|-------------------------|
| string | string | |
| number | number | min, max |
| boolean | boolean | |
| array | string[] / number[] | items: 'string' \| 'number' |
Token estimation
import { estimateTokens, assertFitsInContext } from 'promptdef'
estimateTokens('Hello, world!')
// { tokens: 4, method: 'tiktoken', encoding: 'cl100k_base' }
// Falls back to { tokens: 4, method: 'approximation' } if tiktoken is not installed.
// Throws ContextWindowExceededError if the rendered prompt won't fit.
assertFitsInContext(result, {
model: 'claude-sonnet-4-6',
contextWindow: 200_000,
reserveForResponse: 2_000,
})Install tiktoken as an optional peer dependency for accurate counts:
npm install tiktokenWithout it, promptdef falls back to a character-based approximation (ceil(chars / 4)) — no API calls required either way.
Pre-flight validation
validatePrompt collects all problems at once without rendering — useful for form validation or CI checks before a deploy.
import { validatePrompt } from 'promptdef'
const result = validatePrompt(summarize, {
maxSentences: 'three', // wrong type
// article missing
})
result.valid // false
result.errors
// [
// { variable: 'maxSentences', message: '"maxSentences" must be a number, got string' },
// { variable: 'article', message: 'Missing required variable "article"' },
// ]
result.warnings
// e.g. declared variable never used in templateUnlike renderPrompt, validatePrompt never throws. It accepts partial values so you can call it with no second argument to discover what is required.
Diff tracking
import { diffPrompts } from 'promptdef'
const diff = diffPrompts(v1, v2)
diff.templateChanges // line-level added / removed
diff.variablesAdded // ['date', 'focusAreas']
diff.variablesRemoved // []
diff.tokenDelta // +13
diff.breakingChange // true if a required variable was removedbreakingChange: true is the signal to block a merge in CI — existing callers would be missing a newly-required variable or would pass a variable that no longer exists.
Composition
Combine a system prompt and a task prompt into one, merging their variable schemas.
import { composePrompt } from 'promptdef'
const systemPrompt = definePrompt({
id: 'system',
version: '1.0.0',
template: 'You work for {{company}}. Be concise.',
variables: { company: { type: 'string', required: true } },
})
const composed = composePrompt([systemPrompt, summarize], { separator: '\n\n---\n\n' })
// composed.template — both templates joined with the separator
// composed.variables — merged from both (throws on conflicting types)Registry
Store, version, and retrieve prompts — in a directory, in memory, in S3, or in any SQL database.
File registry
import { createRegistry } from 'promptdef'
const registry = createRegistry({ storage: './prompts' })
await registry.save(summarize)
// writes ./prompts/[email protected]
await registry.load('summarize-article', '1.0.0')
await registry.list() // latest version of every prompt
await registry.history('summarize-article') // all versions, semver-sorted
await registry.rollback('summarize-article', '1.0.0')Memory registry (testing)
import { createMemoryAdapter, createRegistry } from 'promptdef'
const registry = createRegistry({ storage: createMemoryAdapter() })S3 registry
Requires @aws-sdk/client-s3 (optional peer dep):
npm install @aws-sdk/client-s3import { S3Client } from '@aws-sdk/client-s3'
import { createAwsS3Backend, createS3Adapter, createRegistry } from 'promptdef'
// or: import { createAwsS3Backend, createS3Adapter } from 'promptdef/adapters/s3'
const backend = createAwsS3Backend(
new S3Client({ region: 'us-east-1' }),
'my-prompts-bucket'
)
const registry = createRegistry({
storage: createS3Adapter(backend, { prefix: 'prompts/' }),
})SQL registry
Works with any database — pass a query function that matches your driver.
import { createSqlAdapter, createRegistry } from 'promptdef'
// or: import { createSqlAdapter } from 'promptdef/adapters/sql'
// Create the table once:
// CREATE TABLE prompts (
// id TEXT NOT NULL,
// version TEXT NOT NULL,
// data TEXT NOT NULL,
// PRIMARY KEY (id, version)
// );
// mysql2 / better-sqlite3 (? placeholders)
const registry = createRegistry({
storage: createSqlAdapter({
query: async (sql, params) => {
const [rows] = await pool.execute(sql, params)
return rows as Record<string, unknown>[]
},
}),
})
// pg (convert ? → $N)
const registry = createRegistry({
storage: createSqlAdapter({
query: async (sql, params) => {
let i = 0
const pgSql = sql.replace(/\?/g, () => `$${++i}`)
const { rows } = await pool.query(pgSql, params)
return rows
},
}),
})CLI
Install globally or use via npx:
npm install -g promptdef
# or
npx promptdef <command>diff — compare two versions
promptdef diff v1.json v2.jsonComparing summarize-article: 1.0.0 → 2.0.0
Template changes (2):
+ [line 1] You are a {{tone}} summarizer. Today is {{date}}.
- [line 1] You are a {{tone}} summarizer.
Variables added (1):
+ date (string, required)
Token delta: +13
✓ No breaking changesExits 1 when a breaking change is detected — use this to block merges in CI:
- run: promptdef diff prompts/v1.json prompts/v2.jsonUse --json for machine-readable output:
promptdef diff v1.json v2.json --json | jq .diff.breakingChangevalidate — pre-flight check
promptdef validate prompt.json --values '{"maxSentences": 3, "article": "..."}'Exits 0 when valid, 1 when there are errors.
render — interpolate and print
promptdef render prompt.json --values '{"maxSentences": 3, "article": "..."}'Rendered [email protected] (~30 tokens)
You are a neutral summarizer.
Summarize this article in 3 sentences.
Article: ...Use --raw to print only the interpolated text (for piping):
promptdef render prompt.json --values '...' --raw | pbcopylist — show all prompts in a registry
promptdef list ./prompts3 prompts in ./prompts
answer-question 1.0.0
classify-sentiment 1.0.0 [classification]
summarize-article 2.0.0 [summarization]history — version timeline for one prompt
promptdef history summarize-article ./promptssummarize-article 2 versions
1.0.0 7 tokens
2.0.0 16 tokens +9Global options
| Flag | Description |
|---------------|------------------------------------------|
| --json | Machine-readable JSON output |
| --no-color | Disable ANSI colour |
| --values | JSON string of variable values |
| --raw | Text-only output (render command) |
Exit codes
| Code | Meaning |
|------|------------------------------------------------------|
| 0 | Success |
| 1 | Breaking change / validation failure |
| 2 | Usage error (bad args, file not found, invalid JSON) |
Subpath imports
Import only what you need to keep bundles lean:
import { createS3Adapter } from 'promptdef/adapters/s3'
import { createSqlAdapter } from 'promptdef/adapters/sql'
import { createFileAdapter } from 'promptdef/adapters/file'
import { createMemoryAdapter } from 'promptdef/adapters/memory'License
MIT
