npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kasava/prompt-builder

v0.3.0

Published

A prompt ORM for TypeScript. Typed variables, a composable template tag, an inspectable AST, and pluggable output dialects.

Readme


Typed variables, a composable template tag, an inspectable AST, and pluggable output dialects. If you know Drizzle, you know the shape of this.

import { definePrompt, text, list, bool, prompt, p, when } from '@kasava/prompt-builder'

const userContext = definePrompt('user_context', {
  userName:    text().notNull(),
  activeShows: list().default([]),
  isMobile:    bool().default(false),
})

const template = userContext.body((v) => prompt()
  .tag('user_context', p`Active shows (${v.activeShows.length}): ${v.activeShows}`)
  .include(when(v.isMobile, 'Keep replies short — this is a phone.')))

template.render({ userName: 'Ada', activeShows: ['Severance', 'Andor'] })
<user_context>
Active shows (2): Severance, Andor
</user_context>
  • Typed variables — declare what a prompt needs; TypeScript infers the payload
  • Composable — fragments, combinators, and reusable sections
  • Inspectable — prompts compile to an AST you can walk, diff, and trim
  • Multi-target — one prompt, rendered as markdown, XML, or chat messages
  • Zero dependencies in the core; zod is an optional peer

Created by Kasava — AI-powered development platform for product engineers.


Quick Start

npm install @kasava/prompt-builder

The fluent builder is the foundation, and it works exactly as it always has:

import { prompt } from '@kasava/prompt-builder'

const systemPrompt = prompt()
  .role('helpful assistant')
  .heading('Guidelines')
  .list([
    'Be concise and direct',
    'Ask clarifying questions when the request is ambiguous',
  ])
  .build()

Reach for the schema layer when a prompt needs runtime data.


The Mapping

An ORM maps between two representations. For Drizzle that's TypeScript objects ↔ relational rows. Here it's typed application context ↔ model-facing prompt text.

| Drizzle | prompt-builder | |---|---| | pgTable('users', {...}) | definePrompt('name', {...}) | | text().notNull().default() | text() / num() / bool() / list() / json() | | $inferSelect / $inferInsert | $inferVars | | sql`select * from ${t}` | p`Active shows: ${shows}` | | sql.raw() / .join() / .empty() | p.raw() / p.join() / p.empty() | | sql.placeholder('id') | placeholder('id') | | .prepare().execute({...}) | .prepare().render({...}) | | eq(), and(), or() | when(), unless(), all(), any(), each() | | .$dynamic() | .$dynamic() | | PgDialect / MySqlDialect | markdown() / xml() / toMessages() | | .toSQL() | .toPrompt() / .toAST() | | drizzle-zod | @kasava/prompt-builder/zod |

What deliberately isn't mapped

Drizzle's power comes from SQL being a real target language with a real engine. Prompts have no engine, no execution, no result set. So there's no select, where, join, or transaction here — chaining those onto a prompt would be cargo cult. What replaces them is $budget(), which has no SQL analogue at all.

p does not escape, and cannot

Drizzle's sql tag escapes because SQL has a grammar to break out of. Natural language does not, so no amount of escaping makes untrusted text safe to interpolate into a prompt. p gives you composition and consistent value serialization — arrays comma-join, objects become JSON, nullish becomes empty. It is not an injection defense. Treat interpolated data as data, and follow your provider's guidance on delimiting and instruction hierarchy.


Core Concepts

The p tag

p`Watched ${count} of ${shows}`          // "Watched 12 of Severance, Andor"
p`${p.raw(existingMarkdown)}`            // verbatim, unformatted
p.join([p`a`, p`b`], ', ')               // "a, b"

| Interpolated | Emitted | |---|---| | string / number / boolean | stringified | | array | comma-joined | | object | JSON | | null / undefined | empty string | | Fragment | inlined | | p.raw(x) | verbatim | | placeholder('x') | a slot, filled at render |

Leading indentation is stripped, so multi-line fragments can be written inline without dragging whitespace into the prompt.

Combinators

import { when, unless, all, any, each } from '@kasava/prompt-builder'

all(
  BASE_RULES,
  when(flags.toolSearch, TOOL_SEARCH_BLOCK),
  unless(flags.toolSearch, TOOL_CATALOG),
  each(protocols, (proto) => p`### ${proto.name}\n${proto.body}`),
)

These are values, not chain steps — build them anywhere, store them in a config object, pass them around.

Prepared prompts

Compile once, render many times. Same rationale as Drizzle: pay the serialization cost once instead of on every request.

const greeting = prompt().raw(p`Hello ${placeholder('name')}`).prepare('greeting')

greeting.render({ name: 'Ada' })
greeting.render({ name: 'Grace' })

Dialects

One AST, several serializations.

b.build()                       // markdown (default)
b.build(xml())                  // fields as XML elements
b.build(markdown({strict:true}))// pre-0.3.0 bytes, defects included
toMessages(b)                   // [{ role, content, cache_control? }]
b.toAST()                       // Node[] — walk it, diff it, count it

Write your own by implementing renderNode and join.

Cache boundaries

Agent prompts usually segregate cache-stable instructions from per-request context, then hand-roll the split into two system messages. Model the boundary instead:

const messages = toMessages(
  prompt().include(STATIC_INSTRUCTIONS).cacheBoundary().include(perRequestContext),
)
// [{ role, content, cache_control: { type: 'ephemeral' } }, { role, content }]

Provider caches match on an exact prefix, so the stable half must be byte-identical between requests to hit.

Token budgets

No Drizzle analogue — this is the AI-native LIMIT.

prompt()
  .priority('required').include(CORE_RULES)
  .priority('low').include(WORKED_EXAMPLES)
  .$budget({ maxTokens: 8000, counter: myTokenizer })

Drops whole nodes rather than truncating text, so the result is always well-formed. required never drops; if it alone exceeds the budget, this throws rather than quietly returning something oversized. Bring your own tokenizer — the default is a rough four-characters-per-token estimate and the library takes no tokenizer dependency.

Validation

import { createVarsSchema } from '@kasava/prompt-builder/zod'

const varsSchema = createVarsSchema(userContext, { userName: (s) => s.max(80) })
template.render(varsSchema.parse(await request.json()))

API Reference

Schema

| Export | Description | |---|---| | definePrompt(name, vars) | Declare a prompt and its variables | | text() num() bool() list() json<T>() | Variable builders | | .notNull() .default(v) .$type<T>() | Modifiers | | schema.$inferVars | The render-payload type | | schema.body(fn) | Attach a body → PromptTemplate | | template.render(vars, dialect?) | Render | | template.toAST(vars) / .prepare(vars) | Inspect / compile |

Building

| Method | Description | |---|---| | .heading(text, level?) .raw(content) .section(title, content) | Core | | .field(label, value) .booleanField() .inlineList() | Fields | | .list() .numberedList() .limitedList() .keyValues() | Lists | | .table(columns, rows) .lookupTable({...}) | Tables (cells escaped) | | .codeBlock() .diffBlock() .filesList() | Code & files | | .tag(name, content) + 11 semantic helpers | XML | | .protocol() .arrowRules() .role() .guidelines() | Generators | | .confidenceScale() .severityScale() .investigationStrategy() | Scales | | .outputFormat() .verificationChecklist() | Output specs | | .include(other) .conditional(cond, fn) | Composition | | .node(astNode) | Escape hatch |

Rendering

| Method | Description | |---|---| | .build(dialect?) | Render to a string | | .toAST() / .toPrompt(dialect?) | Inspect | | .params() | Unbound placeholder names | | .prepare(name?, dialect?) | Compile for repeated rendering | | .priority(level) / .$budget(opts) | Token budgeting | | .cacheBoundary() / .$dynamic() | Cache splits, dynamic building |

Subpaths

| Import | Contents | |---|---| | @kasava/prompt-builder | Everything above. Zero dependencies. | | @kasava/prompt-builder/presets | toolGuidance, gracefulDegradation, followThroughMatrix, analysisRequirements, workedExample(s) | | @kasava/prompt-builder/zod | createVarsSchema (needs zod) |


Migrating from 0.2.x

Nothing to do. Every 0.2.x call site compiles and runs unchanged. The builder API is unchanged; only what it emits internally is different.

Output is semantically equivalent, not byte-identical — 0.3.0 fixes nine formatting defects, listed in PLAN-0.3.0.md §6. The notable one is that table cells are now escaped; a | in a cell used to silently corrupt the table. If you need the old bytes exactly — for a prompt cache you aren't ready to invalidate — pass markdown({ strict: true }).

The five domain-shaped generators moved to /presets and remain available as deprecated methods. newline(), paragraph(), blankLine(), bullets(), and steps() are still no-ops and aliases. All of these are removed in 1.0.


Origin

Extracted from Kasava's internal AI platform, where it powers 30+ agents and 70+ workflows in production.


Contributing

pnpm install        # Install dependencies
pnpm test           # Run the test suite
pnpm test:watch     # Watch mode
pnpm test:coverage  # Coverage (thresholds are set to 100%)
pnpm test:types     # Type-level assertions (*.test-d.ts)
pnpm type-check     # tsc over src + test
pnpm build          # Build package

The consumer baseline

test/integration/consumer-baseline.test.ts loads real prompt modules from sibling checkouts (../monroe, ../kasava) and asserts they still build identically against a committed baseline. It skips cleanly when those repos aren't present.

After an intentional formatting change, regenerate and read the diff:

pnpm test:baseline

License

MIT — See LICENSE for details.