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

kanuni

v0.0.4

Published

Kanuni - The Prompt-Builder Library

Readme

Kanuni

The Prompt-Builder Library for LLM Context Management

Kanuni is a TypeScript library designed to help developers build structured, type-safe prompts and manage conversation context for Large Language Model (LLM) interactions. It provides a fluent API for creating complex prompts with memory, tools, and structured output specifications.

Table of Contents

Installation

npm install kanuni

Core Concepts

Query

A Query is the main structure that contains everything needed for an LLM call:

  • Prompt: The structured content sent to the LLM
  • Memory: Conversation history and context
  • Tools: Available functions the LLM can call
  • Output: Specification for text or JSON responses

Queries can be serialized to JSON strings for storage, transmission, or caching using Kanuni.serializeQuery() and restored with Kanuni.deserializeQuery().

Memory

Memory stores conversation history including:

  • User and assistant utterances
  • Tool calls and their results
  • Custom roles and participants

Prompt Content

Prompts support rich content types:

  • Paragraphs with template strings
  • Lists and nested lists
  • Tables with headers and structured data
  • Sections with headings and nested content

Formatter

Formatters convert Kanuni queries into the specific format required by different LLM provider APIs. The core Kanuni library provides the structure and types, while separate packages like kanuni-openai provide formatter implementations for specific providers (OpenAI, Anthropic, etc.). Formatters handle the translation between Kanuni's structured format and each provider's expected API format, including prompt formatting, message structure, tool definitions, and response parsing.

Quick Start

import { Kanuni } from "kanuni";
import { z } from "zod";

// Define your data structure
interface MyData {
  userQuestion: string;
  context: string;
}

// Create a simple query
const query = Kanuni.newQuery<MyData>()
  .prompt(
    (p) =>
      p.paragraph`Context: ${"context"}`.paragraph`Question: ${"userQuestion"}`
        .paragraph`Please provide a helpful response.`
  )
  .build({
    userQuestion: "What is TypeScript?",
    context: "The user is learning programming",
  });

console.log(query.prompt);

Building Prompts

Basic Content Types

Paragraphs

const query = Kanuni.newQuery<{ name: string; age: number }>()
  .prompt((p) =>
    p.paragraph`Hello, ${"name"}! You are ${"age"} years old.`.paragraph(
      (data) => `Welcome, ${data.name}!`
    )
  )
  .build({ name: "Alice", age: 30 });

Lists

const query = Kanuni.newQuery<{ items: string[] }>()
  .prompt(p =>
    p.paragraph`Shopping list:`
     .list(l =>
       l.item`Buy milk`
        .item`Buy bread`
        .item((data) => `Buy ${data.items[0]}`)
     )
  )
  .build({ items: ["apples"] });

// Using items() to create dynamic lists from arrays
const dynamicQuery = Kanuni.newQuery<{ tasks: string[] }>()
  .prompt(p =>
    p.paragraph`Today's tasks:`
     .list((l, data) =>
       l.items(data.tasks, (i, task) =>
         i.item(_ => task)
     )
  )
  .build({ tasks: ["Review code", "Write tests", "Deploy to production"] });

Tables

const query = Kanuni.newQuery<{
  users: Array<{ name: string; role: string }>;
}>()
  .prompt((p) =>
    p.paragraph`User Information:`.table((t) =>
      t
        .columnHeaders(() => ["Name", "Role"])
        .row((r) =>
          r.cell((c) => c.paragraph`Alice`).cell((c) => c.paragraph`Admin`)
        )
        .row((r) =>
          r.cell((c) => c.paragraph`Bob`).cell((c) => c.paragraph`User`)
        )
    )
  )
  .build({ users: [] });

Sections

const query = Kanuni.newQuery<{ title: string }>()
  .prompt((p) =>
    p.section((s) =>
      s.heading`Instructions for ${"title"}`
        .paragraph`Please follow these steps:`.list(
        (l) =>
          l.item`Step 1: Read the documentation`
            .item`Step 2: Practice with examples`
      )
    )
  )
  .build({ title: "Learning Kanuni" });

Managing Memory

Basic Memory Usage

const query = Kanuni.newQuery<{ userName: string }>()
  .prompt((p) => p.paragraph`How can I help you today?`)
  .memory((m) =>
    m
      .utterance("user", (data) => `Hi, my name is ${data.userName}`)
      .utterance(
        "assistant",
        () => "Hello! Nice to meet you. How can I assist you?"
      )
  )
  .build({ userName: "Alice" });

Tool Calls in Memory

type WeatherTool = Tool<"get_weather", { location: string }>;

const query = Kanuni.newQuery<{}, RoleDefault, WeatherTool>()
  .memory((m) =>
    m
      .utterance("user", () => "What's the weather in London?")
      .toolCall("get_weather", '{"location": "London"}', "call-123")
      .toolCallResult("call-123", "Sunny, 22°C")
      .utterance(
        "assistant",
        () => "The weather in London is sunny with a temperature of 22°C."
      )
  )
  .build({});

Memory with Names

const query = Kanuni.newQuery()
  .memory((m) =>
    m
      .utterance("user", "Customer", () => "I need help with my order")
      .utterance(
        "assistant",
        "Support Agent",
        () => "I'd be happy to help you with your order"
      )
  )
  .build({});

Reusing Memory

// Build memory separately
const memoryBuilder = Kanuni.newMemory<{ prevContext: string }>().utterance(
  "user",
  (data) => `Previous context: ${data.prevContext}`
);

// Extract memory from a previous query
const previousMemory = Kanuni.extractMemoryFromQuery(previousQuery);

// Use in new query
const newQuery = Kanuni.newQuery<{ newInput: string }>()
  .prompt((p) => p.paragraph`New input: ${"newInput"}`)
  .memory((m) =>
    m
      .append(previousMemory?.contents || [])
      .utterance("user", (data) => data.newInput)
  )
  .build({ newInput: "Continue the conversation" });

Working with Tools

Defining Tools

import { z } from "zod";

type SearchTool = Tool<"search", { query: string; limit?: number }>;
type CalculatorTool = Tool<"calculate", { expression: string }>;
type MyTools = SearchTool | CalculatorTool;

const tools: ToolRegistry<MyTools> = {
  search: {
    name: "search",
    description: "Search for information online",
    parameters: {
      query: z.string().describe("The search query"),
      limit: z.number().optional().describe("Maximum number of results"),
    },
  },
  calculate: {
    name: "calculate",
    description: "Perform mathematical calculations",
    parameters: {
      expression: z.string().describe("Mathematical expression to evaluate"),
    },
  },
};

Using Tools in Queries

const query = Kanuni.newQuery<{ question: string }, RoleDefault, MyTools>()
  .prompt((p) => p.paragraph`Question: ${"question"}`)
  .tools(tools)
  .memory((m) => m.utterance("user", (data) => data.question))
  .build({ question: "What is 15 * 24?" });

Output Formats

Text Output (Default)

const query = Kanuni.newQuery<{ topic: string }>()
  .prompt((p) => p.paragraph`Explain ${"topic"} in simple terms.`)
  .outputText() // Optional - this is the default
  .build({ topic: "quantum computing" });

JSON Output

const PersonSchema = z.object({
  name: z.string(),
  age: z.number(),
  skills: z.array(z.string()),
});

const query = Kanuni.newQuery<{ resume: string }>()
  .prompt(
    (p) =>
      p.paragraph`Extract person information from this resume:`
        .paragraph`${"resume"}`
  )
  .outputJson(PersonSchema, "person_info")
  .build({
    resume: "John Doe, 30 years old, skilled in TypeScript and Python",
  });

Switching Output Formats

const baseQuery = Kanuni.newQuery<{ data: string }>().prompt(
  (p) => p.paragraph`Process: ${"data"}`
);

// Text version
const textQuery = baseQuery.outputText().build({ data: "some data" });

// JSON version
const jsonQuery = baseQuery
  .outputJson(z.object({ result: z.string() }))
  .build({ data: "some data" });

Built-in Formatters

TextualMarkdownFormatter

Kanuni includes a TextualMarkdownFormatter that converts queries into human-readable markdown text. This is useful for debugging, logging, or when working with text-based LLM APIs.

import { Kanuni, TextualMarkdownFormatter } from "kanuni";

const query = Kanuni.newQuery<{ topic: string }>()
  .prompt((p) =>
    p.paragraph`Explain ${"topic"} in simple terms.`.list(
      (l) => l.item`Use clear examples`.item`Avoid jargon`
    )
  )
  .memory((m) =>
    m.utterance("user", (data) => `I want to learn about ${data.topic}`)
  )
  .build({ topic: "machine learning" });

// Format as markdown text
const formattedText = TextualMarkdownFormatter.format(query);
console.log(formattedText);

Configuration Options

The formatter can be customized with various options:

const formatter = new TextualMarkdownFormatter({
  indentationString: "    ", // Custom indentation (default: "  ")
  unnumberedListItemPrefix: "* ", // List item prefix (default: "- ")
  memoryIntroductionText: "Conversation History: ", // Memory section intro
  toolsIntroductionText: "Available Functions: ", // Tools section intro
  outputJsonIntroductionText: "Expected Response Format: ", // JSON output intro
  excludes: {
    memory: false, // Set to true to exclude memory from output
    tools: false, // Set to true to exclude tools from output
    outputSpecs: false, // Set to true to exclude output specifications
  },
});

const customFormatted = formatter.format(query);

Special Sections

The formatter handles special sections automatically:

  • Memory sections render conversation history with role-based tags
  • Tools sections include JSON schema definitions for available tools
  • Output specification sections show expected JSON response schemas
const complexQuery = Kanuni.newQuery<{ task: string }>()
  .prompt((p) => p.paragraph`Task: ${"task"}`)
  .memorySection(
    (s) =>
      s.heading`Previous Conversation`
        .paragraph`Reference the conversation history below`
  )
  .toolsSection(
    (s) =>
      s.heading`Available Tools`
        .paragraph`You can use these functions to complete the task`
  )
  .outputSpecsSection(
    (s) =>
      s.heading`Response Format`
        .paragraph`Respond using the specified JSON structure`
  )
  .memory((m) =>
    m
      .utterance("user", () => "Hello, I need help")
      .utterance("assistant", () => "I'm here to help!")
  )
  .tools(myTools)
  .outputJson(
    z.object({
      result: z.string(),
      confidence: z.number(),
    })
  )
  .build({ task: "Complete analysis" });

const formatted = TextualMarkdownFormatter.format(complexQuery);
// Produces markdown with sections for prompt, memory, tools, and output schema

Query Serialization

Kanuni provides built-in support for serializing and deserializing queries, allowing you to store, transmit, or persist query objects as JSON strings. This is particularly useful for caching, database storage, or sending queries across network boundaries.

Serializing Queries

Use Kanuni.serializeQuery() to convert any Query object into a JSON string:

import { Kanuni, Tool, ToolRegistry } from "kanuni";
import { z } from "zod";

const query = Kanuni.newQuery<{ topic: string }>()
  .prompt((p) =>
    p.paragraph`Analyze the topic: ${"topic"}`.list(
      (l) =>
        l.item`Identify key themes`.item`Extract important details`
          .item`Provide recommendations`
    )
  )
  .memory((m) =>
    m.utterance("user", (data) => `I want to learn about ${data.topic}`)
  )
  .outputJson(
    z.object({
      themes: z.array(z.string()),
      details: z.array(z.string()),
      recommendations: z.array(z.string()),
    }),
    "analysis_result"
  )
  .build({ topic: "machine learning" });

// Serialize the query to JSON string
const serializedQuery = Kanuni.serializeQuery(query);

// Store in localStorage, send over network, save to database, etc.
localStorage.setItem("analysisQuery", serializedQuery);

Deserializing Queries

Use Kanuni.deserializeQuery() to convert a serialized JSON string back into a functional Query object:

// Retrieve the serialized query
const storedQuery = localStorage.getItem("analysisQuery");

  if (storedQuery) {
    // Deserialize back to a Query object
    const restoredQuery = await Kanuni.deserializeQuery<{ topic: string }>(storedQuery);

    // The restored query is fully functional
    console.log(restoredQuery.prompt.type); // 'prompt'
    console.log(restoredQuery.output.type); // 'output-json'
    console.log(restoredQuery.memory?.contents.length); // 1

    // You can use it just like the original query
    // Note: You'll need to format it with a provider-specific formatter
    return restoredQuery;
  }
  return null;
};

Working with Tools in Serialization

Queries with tools are automatically handled during serialization. Zod schemas for tool parameters are converted to JSON Schema format:

import { z } from "zod";

// Define tool types for the examples
type SearchTool = Tool<"search", { query: string; limit?: number }>;
type AnalysisTool = Tool<
  "analyze",
  { data: string; type: "sentiment" | "keywords" | "summary" }
>;

const tools: ToolRegistry<SearchTool | AnalysisTool> = {
  search: {
    name: "search",
    description: "Search for information",
    parameters: {
      query: z.string().describe("Search query"),
      limit: z.number().optional().describe("Max results"),
    },
  },
  analyze: {
    name: "analyze",
    description: "Analyze data",
    parameters: {
      data: z.string().describe("Data to analyze"),
      type: z.enum(["sentiment", "keywords", "summary"]),
    },
  },
};

const queryWithTools = Kanuni.newQuery<{ task: string }>()
  .prompt((p) => p.paragraph`Task: ${"task"}`)
  .tools(tools)
  .outputJson(z.object({ result: z.string() }))
  .build({ task: "Analyze user feedback" });

// Serialize and deserialize
const serialized = Kanuni.serializeQuery(queryWithTools);
const restored = await Kanuni.deserializeQuery(serialized);

// Tools are preserved and functional
console.log(Object.keys(restored.tools!)); // ['search', 'analyze']

Practical Use Cases

1. Query Templates and Caching

// Create reusable query templates
const createAnalysisQuery = (analysisType: string) => {
  return Kanuni.newQuery<{ data: string }>()
    .prompt((p) => p.paragraph`Perform ${analysisType} analysis on: ${"data"}`)
    .outputJson(
      z.object({
        analysis_type: z.string(),
        results: z.array(z.string()),
        confidence: z.number(),
      })
    )
    .build({ data: "" }); // Template with empty data
};

// Serialize templates for reuse
const templates = {
  sentiment: Kanuni.serializeQuery(createAnalysisQuery("sentiment")),
  keyword: Kanuni.serializeQuery(createAnalysisQuery("keyword")),
  summary: Kanuni.serializeQuery(createAnalysisQuery("summary")),
};

// Later, deserialize and use with actual data
const sentimentQuery = await Kanuni.deserializeQuery(templates.sentiment);
// Update with actual data by rebuilding or using a formatter

2. Database Storage

interface StoredQuery {
  id: string;
  name: string;
  description: string;
  query_json: string;
  created_at: Date;
}

// Save query to database
const saveQuery = async (
  name: string,
  description: string,
  query: Query<any, any, any>
) => {
  const serialized = Kanuni.serializeQuery(query);

  await database.queries.create({
    id: generateId(),
    name,
    description,
    query_json: serialized,
    created_at: new Date(),
  });
};

// Load and use query from database
const loadQuery = async (queryId: string) => {
  const stored = await database.queries.findById(queryId);
  if (stored) {
    return await Kanuni.deserializeQuery(stored.query_json);
  }
  return null;
};

3. API Communication

// Client side - send query to server
const clientQuery = Kanuni.newQuery<{ userInput: string }>()
  .prompt((p) => p.paragraph`Process: ${"userInput"}`)
  .build({ userInput: "User's request" });

const response = await fetch("/api/process-query", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    query: Kanuni.serializeQuery(clientQuery),
    metadata: { userId: "user123" },
  }),
});

// Server side - receive and process query
app.post("/api/process-query", async (req, res) => {
  const { query: serializedQuery, metadata } = req.body;

  // Deserialize the query
  const query = await Kanuni.deserializeQuery(serializedQuery);

  // Process with your LLM provider
  const result = await processWithLLM(query);

  res.json({ result });
});

Important Considerations

  • Schema Conversion: Zod schemas are converted to JSON Schema during serialization and back to Zod during deserialization. The functional behavior is preserved, but object identity is not.
  • Security: Deserialization uses eval() for schema reconstruction. Only deserialize trusted input.
  • Performance: Serialization is fast, but deserializing complex schemas may have some overhead due to schema reconstruction. Deserialization is asynchronous.
  • Compatibility: Serialized queries are forward-compatible but may not be backward-compatible across major Kanuni versions.

Advanced Examples

Complex Multi-Section Prompt

interface AnalysisRequest {
  document: string;
  requirements: string[];
  previousFindings: string;
  outputFormat: string;
}

const query = Kanuni.newQuery<AnalysisRequest>()
  .prompt((p) =>
    p.paragraph`Document Analysis Request`

      .section((s) => s.heading`Document Content`.paragraph`${"document"}`)

      .section((s) =>
        s.heading`Analysis Requirements`.list((l, data) => {
          data.requirements.forEach((req) => {
            l.item(() => req);
          });
          return l;
        })
      )
  )
  .memorySection(
    (s) => s.heading`Previous Context`.paragraph`${"previousFindings"}`
  )
  .toolsSection(
    (s) =>
      s.heading`Available Analysis Tools`
        .paragraph`Use the document_analyzer tool for detailed analysis`
  )
  .outputSpecsSection(
    (s) => s.heading`Output Requirements`.paragraph`Format: ${"outputFormat"}`
  )
  .outputJson(
    z.object({
      summary: z.string(),
      keyFindings: z.array(z.string()),
      confidence: z.number().min(0).max(1),
    })
  )
  .build({
    document: "Long document content...",
    requirements: [
      "Extract key themes",
      "Identify risks",
      "Suggest improvements",
    ],
    previousFindings: "Previous analysis showed...",
    outputFormat: "Structured JSON with summary and findings",
  });

Dynamic Content Generation

interface ReportData {
  title: string;
  sections: Array<{
    heading: string;
    content: string;
    data: Array<{ label: string; value: string }>;
  }>;
  conclusions: string[];
}

const query = Kanuni.newQuery<ReportData>()
  .prompt((p) => {
    const builder = p.paragraph`Report: ${"title"}`;

    return builder.section((s, data) => {
      let sectionBuilder = s.heading`Executive Summary`;

      data.sections.forEach((section) => {
        sectionBuilder = sectionBuilder.section((subsection) =>
          subsection
            .heading(() => section.heading)
            .paragraph(() => section.content)
            .table((t) => {
              let tableBuilder = t.columnHeaders(() => ["Metric", "Value"]);
              section.data.forEach((row) => {
                tableBuilder = tableBuilder.row((r) =>
                  r
                    .cell((c) => c.paragraph(() => row.label))
                    .cell((c) => c.paragraph(() => row.value))
                );
              });
              return tableBuilder;
            })
        );
      });

      return sectionBuilder.section((conclusionSection) =>
        conclusionSection.heading`Conclusions`.list((l, data) => {
          data.conclusions.forEach((conclusion) => {
            l.item(() => conclusion);
          });
          return l;
        })
      );
    });
  })
  .build({
    title: "Q4 Performance Report",
    sections: [
      {
        heading: "Sales Performance",
        content: "Sales exceeded expectations...",
        data: [
          { label: "Revenue", value: "$1.2M" },
          { label: "Growth", value: "15%" },
        ],
      },
    ],
    conclusions: [
      "Strong performance in Q4",
      "Revenue targets exceeded",
      "Positive outlook for Q1",
    ],
  });

Conversation Flow with Memory Management

interface ConversationState {
  currentMessage: string;
  userPreferences: {
    responseStyle: string;
    topics: string[];
  };
}

// First interaction
const initialQuery = Kanuni.newQuery<ConversationState>()
  .prompt(
    (p) =>
      p.paragraph`You are a helpful assistant. Respond in ${"userPreferences"} style.`
        .paragraph`User preferences: ${"userPreferences"}`
  )
  .memory((m) => m.utterance("user", (data) => data.currentMessage))
  .build({
    currentMessage: "Hello, I'm interested in learning about TypeScript",
    userPreferences: {
      responseStyle: "friendly and detailed",
      topics: ["programming", "web development"],
    },
  });

// Continue conversation by extracting memory
const previousMemory = Kanuni.extractMemoryFromQuery(initialQuery);

const followUpQuery = Kanuni.newQuery<ConversationState>()
  .prompt(
    (p) =>
      p.paragraph`Continue the conversation, maintaining the same helpful tone.`
  )
  .memory((m) =>
    m
      .append(previousMemory?.contents || [])
      .utterance(
        "assistant",
        () =>
          "I'd be happy to help you learn TypeScript! TypeScript is a strongly typed programming language..."
      )
      .utterance("user", (data) => data.currentMessage)
  )
  .build({
    currentMessage: "Can you show me some examples?",
    userPreferences: {
      responseStyle: "friendly and detailed",
      topics: ["programming", "web development"],
    },
  });

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT