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

textprompts

v1.0.0

Published

TypeScript companion to textprompts for loading and formatting prompt files.

Readme

textprompts

Prompts as text files, with typed variables, conditional logic, and zero configuration.

TypeScript/JavaScript port of textprompts for loading and formatting prompt files.

Are you tired of vendors trying to sell you fancy UIs for prompt management that just make your system more confusing and harder to debug? Isn't it nice to just have your prompts next to your code?

But then you worry: Did my formatter change my prompt? Are those spaces at the beginning actually part of the prompt or just indentation?

textprompts solves this elegantly: treat your prompts as text files and keep your linters and formatters away from them. v2 adds typed flags, {if} and {switch} conditionals, and AST-backed validation, while keeping the file-on-disk simplicity.

One-page overview

The full story on one page:

  • Variables{name} substitutes; {{name}} escapes and renders the literal text {name} (same convention as Python's str.format).
  • Metadata = guardrails + context for newcomers. Frontmatter declares title, version, description, owner, last_reviewed, and each flag's type (boolean or enum with declared values). Newcomers see what changed, why, and who owns it — right next to the prompt. format() type-checks every call against this schema and throws FormatError if anything is missing or the wrong type.
  • Conditionals — gate optional features with {if has_memory}…{end}, roll out tier variants with {switch tier}{case …}{end}. No string concatenation in app code.
  • format() is fully type-checked. Variables sit at the top level of the call; flags go under a reserved flags key; missing inputs fail fast with a stable error code.
  • Going to prod? Turn strict on globally. Per call: loadPrompt(path, { metadata: "strict" }). Process-wide: setMetadata(MetadataMode.STRICT). Or via env: TEXTPROMPTS_METADATA_MODE=strict. Strict enforces non-empty title/description/version, every body flag declared, every flag described, and exhaustive enum switches — at load time, before a single prompt is rendered.

Authoring guide

For deeper guidance on writing v2 prompts — {if} vs {switch}, flag patterns, anti-patterns, debugging, and v1 → v2 migration — see docs/writing-prompts-with-textprompts/SKILL.md. The skill is also installable into Claude / Codex agents as writing-prompts-with-textprompts.

Why textprompts?

  • Prompts live next to your code — no external systems to manage
  • Git is your version control — diff, branch, and experiment
  • No formatter headaches — your prompts stay exactly as you wrote them
  • Typed flags and variables — declared in TOML or YAML frontmatter
  • Conditional content{if}, {switch}, {else} with strict validation
  • Strict mode — require flag descriptions on production prompts
  • Edge-readytextprompts/core has zero node:* imports for Cloudflare Workers, Deno Deploy, Vercel Edge, and the browser
  • Lightweight — TOML and YAML parsers, nothing more

Installation

npm install textprompts
# or
bun add textprompts
# or
pnpm add textprompts

Quick start — flags + conditional

Create prompts/support.txt:

---
title = "Customer support agent"
version = "2.1.0"
description = "Customer support prompt with tier-based routing"

[flags.tier]
type = "enum"
values = ["free", "premium", "enterprise"]
description = "User subscription tier"

[flags.has_urgent]
description = "Whether the conversation has been flagged as urgent"

[variables.user_name]
description = "The user's display name"

[variables.last_question]
description = "Previous question, used when has_urgent is true"
---
You are a helpful support agent assisting {user_name}.

{switch tier}
{case free}
You have standard support. Response times may vary.
{case premium}
You have priority support with guaranteed response within 1 hour.
{case enterprise}
You have a dedicated account manager. Their name is on file.
{end}
{if has_urgent}
This conversation has been flagged as urgent. The user previously asked: {last_question}
{end}
How can I help today?

Load and format it:

import { loadPrompt } from "textprompts";

const support = await loadPrompt("prompts/support.txt", { metadata: "strict" });

const message = support.format({
  user_name: "Jan",
  last_question: "How do I upgrade?",
  flags: { tier: "premium", has_urgent: true },
});

Output:

You are a helpful support agent assisting Jan.

You have priority support with guaranteed response within 1 hour.
This conversation has been flagged as urgent. The user previously asked: How do I upgrade?
How can I help today?

That's the full story. Variables go at the top level of format(). Flags go under a reserved flags key. Frontmatter declares both; missing inputs throw FormatError with a stable code; extra inputs are silently ignored.

In-memory prompts

When you can't load from disk (bundlers, edge runtimes, tests), use Prompt.fromString:

import { Prompt } from "textprompts";

const prompt = Prompt.fromString(
  "Hello {name}{if friendly}, friend{end}!",
);

prompt.format({ name: "Alice", flags: { friendly: true } });
// "Hello Alice, friend!"

With a bundler:

// Vite — file is bundled as a string at build time
import content from "./prompt.txt?raw";
const prompt = Prompt.fromString(content, { path: "prompt.txt" });

Edge runtimes

// Cloudflare Workers, Deno Deploy, Vercel Edge, browser
import { Prompt, parseString } from "textprompts/core";

The core entry point has zero node:* imports. It exposes Prompt, parseString, parseSections, error classes, and MetadataMode. File-system entry points (loadPrompt, loadSection, savePrompt, parseFile) live in the default textprompts entry only.

Conditional syntax at a glance

{var}                       — variable substitution
{if flag}...{end}           — block conditional (each tag alone on a line)
{if flag}...{else}...{end}  — with else branch
{if !flag}...{end}          — negation
{switch flag}{case x}...{case y}...{else}...{end}
{{ }}                       — escapes: {{ -> {, }} -> }

Inline form keeps everything on one line:

You are a {role}{if is_admin} (administrator){end}.

Block form puts each control tag alone on its own line:

{switch tier}
{case free}
Free plan.
{case premium}
Premium plan.
{end}

The keyword lines disappear; body line indentation is preserved exactly as written. See docs/file-format.md for the full grammar and docs/examples.md for rendered examples.

Metadata modes

const prompt = await loadPrompt("prompts/support.txt", {
  metadata: "strict",          // "allow" (default) | "strict" | "ignore"
  frontmatterFormat: "toml",   // "auto" (default) | "toml" | "yaml"
});
  • "allow" (default) — frontmatter optional; flags can be implicit.
  • "strict" — frontmatter required; title/description/version required; every body-referenced flag must be declared in [flags.*], and every declared flag needs a non-empty description.
  • "ignore" — the source is not inspected for frontmatter at all; the whole file (including any leading ---...--- block) is treated as the prompt body. Title defaults to the filename stem.

You can also set setMetadata(MetadataMode.STRICT) as a process-global default, or the TEXTPROMPTS_METADATA_MODE env var.

Migrating from v1

v2 is a breaking release. The substantive changes:

Positional placeholders → named placeholders

- Hello {0}, your order {1} is {2}.
- prompt.format(["Alice", "12345", "shipped"]);
+ Hello {name}, your order {order_id} is {status}.
+ prompt.format({ name: "Alice", order_id: "12345", status: "shipped" });

Conditional logic — was string concatenation, now declarative

- // Before: branching in the caller
- let body = baseTemplate;
- if (isAdmin) body = body.replace("{admin_note}", " (administrator)");
- else body = body.replace("{admin_note}", "");
+ // After: branching in the prompt
+ // You are a {role}{if is_admin} (administrator){end}.
+ prompt.format({ role: "analyst", flags: { is_admin: true } });

Format call shape

- prompt.format({ name: "Alice", role: "admin" });
- prompt.format(["Alice", "admin"]);
- prompt.format({ name: "Alice" }, { item: "widget" }, { skipValidation: true });
+ prompt.format({ name: "Alice", role: "admin" });
+ prompt.format({ name: "Alice", role: "admin", flags: { premium: true } });

The new shape: one object, with optional reserved flags key, and every other top-level key is a variable.

In-memory prompts: PromptStringPrompt.fromString

- import { PromptString } from "textprompts";
- const t = new PromptString("Hello {name}!");
- t.format({ name: "Alice" });
+ import { Prompt } from "textprompts";
+ const t = Prompt.fromString("Hello {name}!");
+ t.format({ name: "Alice" });

PromptString is internal in v2; it is not exported from textprompts or textprompts/core.

Loader option name

- await loadPrompt("file.txt", { meta: "strict" });
+ await loadPrompt("file.txt", { metadata: "strict" });

format no longer takes skipValidation

Partial / multi-stage formatting is no longer a first-class feature. If you need it, render the prompt once with the variables you have and compose the rest at the call site, or split the prompt into two files. The required-input rule (SPEC §5.2) makes "leave variables behind for later" silently ambiguous.

For the full v2 design rationale and a longer-form migration walkthrough, see the authoring skill and the cross-language SPEC.

API surface

import {
  // Loading
  loadPrompt,
  loadSection,
  parseFile,
  parseString,
  Prompt,                  // includes Prompt.fromPath, Prompt.fromString
  // Saving
  savePrompt,
  // Sections (Markdown / XML multi-section files)
  parseSections,
  getSectionText,
  sliceSectionContent,
  injectAnchors,
  renderToc,
  normalizeAnchorId,
  generateSlug,
  // Metadata mode control
  MetadataMode,
  setMetadata,
  getMetadata,
  skipMetadata,
  // Errors
  TextPromptsError,
  ParseError,
  FrontmatterError,
  SemanticError,
  FormatError,
  FileMissingError,
} from "textprompts";

// Edge-safe subset (no node:fs)
import { Prompt, parseString /* ... */ } from "textprompts/core";

Full details: API reference.

Documentation

License

MIT — see LICENSE.