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

@promptier/lint

v0.2.1

Published

Linting engine for promptier prompts

Readme

@promptier/lint

Linting engine for promptier prompts. Catch common issues before runtime.

Part of the promptier toolkit:

  • @promptier/core - Prompt composition and rendering
  • @promptier/lint - Linting engine with heuristic rules (you are here)
  • @promptier/cli - CLI for linting, rendering, and debugging

Installation

npm install @promptier/lint

Quick Start

import { Linter } from '@promptier/lint';
import { prompt } from '@promptier/core';

const agent = prompt('assistant')
  .model('claude-sonnet-4-20250514')
  .identity('You are helpful.')
  .build();

const linter = new Linter();
const result = await linter.lint(agent);

console.log(result.errors); // Must fix
console.log(result.warnings); // Should fix
console.log(result.info); // Consider

Built-in Rules

Token Budget

| Rule | Severity | Description | | ---------------------- | -------- | ------------------------------------- | | token-limit-exceeded | error | Prompt exceeds model's context window | | token-limit-warning | warning | Prompt uses >80% of context window |

Model Mismatch

| Rule | Severity | Description | | ---------------------- | -------- | ------------------------------------------------ | | xml-tags-with-gpt | warning | Using XML tags with GPT models (prefer markdown) | | markdown-with-claude | info | Using markdown headers with Claude (prefer XML) |

Cache Efficiency

| Rule | Severity | Description | | ----------------------- | -------- | ------------------------------------------------------------------------- | | dynamic-before-static | warning | Dynamic content appears before cacheable content, reducing cache hit rate |

Best Practice

| Rule | Severity | Description | | ------------------ | -------- | ------------------------------------------------------------- | | missing-identity | warning | No identity section defined - agents should know who they are | | empty-sections | warning | Section has empty or whitespace-only content |

Ordering

| Rule | Severity | Description | | ----------------- | -------- | ---------------------------------------------------------- | | format-not-last | info | Format section not at end of prompt (recommended position) |

Duplication

| Rule | Severity | Description | | ------------------------ | -------- | --------------------------------------- | | duplicate-instructions | warning | Same instruction appears multiple times |

Security

| Rule | Severity | Description | | ---------------------- | -------- | ------------------------------------------------------------------------------------------ | | user-input-in-system | error | User input markers ({{user.*}}, $user.*) in static sections - potential injection risk |

Contradiction

| Rule | Severity | Description | | ---------------------- | -------- | ----------------------------------------------------------- | | conflicting-patterns | warning | Contradictory instructions (e.g., "always X" and "never X") |

Inline Ignores

Disable rules directly in your prompt text:

const agent = prompt('assistant')
  .model('claude-sonnet-4-20250514')
  .identity(
    `
    <!-- promptier-ignore missing-identity -->
    You are a helpful assistant.
  `,
  )
  .build();

Syntax

XML-style (recommended for Claude prompts):

<!-- promptier-ignore rule-id -->
<!-- promptier-ignore rule-id, other-rule -->
<!-- promptier-ignore-all -->

Bracket-style (works everywhere):

[promptier-ignore: rule-id]
[promptier-ignore: rule-id, other-rule]
[promptier-ignore-all]

Examples

Ignore a specific rule:

<!-- promptier-ignore missing-identity -->

Ignore multiple rules:

<!-- promptier-ignore missing-identity, empty-sections -->

Ignore all rules (use sparingly):

<!-- promptier-ignore-all -->

Configuration

Rule Severity

Override rule severity in config:

const linter = new Linter({
  rules: {
    'missing-identity': 'error', // Upgrade to error
    'format-not-last': 'off', // Disable
    'markdown-with-claude': 'warning', // Upgrade to warning
  },
});

Severity levels: 'error' | 'warning' | 'info' | 'off'

Rule Options

Some rules accept options. Use tuple syntax [severity, options]:

const linter = new Linter({
  rules: {
    // token-limit-warning accepts a threshold (0-1, default 0.8)
    'token-limit-warning': ['warning', { threshold: 0.7 }],
  },
});

Available options:

| Rule | Option | Default | Description | | --------------------- | ----------- | ------- | ---------------------------------------- | | token-limit-warning | threshold | 0.8 | Warn when usage exceeds this ratio (0-1) |

Via Config File

// promptier.config.ts
import { defineConfig } from '@promptier/core';

export default defineConfig({
  lint: {
    rules: {
      'missing-identity': 'error',
      'format-not-last': 'off',
    },
  },
});

Custom Rules

Define a Rule

import { defineRule } from '@promptier/lint';

const noTodos = defineRule({
  id: 'no-todos',
  category: 'best-practice',
  defaultSeverity: 'warning',
  description: 'No TODO comments in production prompts',
  check: (ctx) => {
    if (/TODO/i.test(ctx.text)) {
      return [
        {
          id: 'no-todos',
          category: 'best-practice',
          severity: 'warning',
          message: 'Remove TODO comments before deployment',
          suggestion: 'Complete or remove the TODO item',
        },
      ];
    }
    return [];
  },
});

Add to Linter

Via constructor:

const linter = new Linter({
  custom: [noTodos],
});

Via method:

const linter = new Linter();
linter.addRule(noTodos);

Via config file:

// promptier.config.ts
import { defineConfig } from '@promptier/core';
import { defineRule } from '@promptier/lint';

export default defineConfig({
  lint: {
    custom: [
      defineRule({
        id: 'no-placeholder',
        category: 'best-practice',
        defaultSeverity: 'error',
        description: 'No placeholder text',
        check: (ctx) =>
          /\[placeholder\]/i.test(ctx.text)
            ? [
                {
                  id: 'no-placeholder',
                  category: 'best-practice',
                  severity: 'error',
                  message: 'Remove placeholders',
                },
              ]
            : [],
      }),
    ],
  },
});

LintContext

The check function receives a LintContext with:

interface LintContext {
  prompt: Prompt; // The Prompt instance
  text: string; // Rendered prompt text
  sections: SectionConfig[]; // Array of sections
  modelId: string; // Target model ID
  modelConfig: {
    contextWindow: number;
    preferredFormat: 'xml' | 'markdown' | 'plain';
    supportsCaching: boolean;
  };
  tokenCount: number; // Token count of rendered text
  options?: RuleOptions; // Rule-specific options from config
}

LintWarning

Rules return an array of warnings:

interface LintWarning {
  id: string; // Rule ID
  category: LintCategory; // Category for grouping
  severity: LintSeverity; // 'error' | 'warning' | 'info'
  message: string; // Human-readable message
  suggestion?: string; // Optional fix suggestion
  evidence?: string; // Optional quoted text from prompt (semantic rules)
  position?: {
    // Optional source location
    start: number;
    end: number;
    line: number;
    column: number;
  };
}

Categories

Built-in categories:

  • token-budget - Token limit issues
  • model-mismatch - Model-specific format issues
  • cache-inefficiency - Prompt caching issues
  • best-practice - General best practices
  • ordering - Section ordering suggestions
  • duplication - Duplicate content
  • security - Security concerns
  • contradiction - Conflicting instructions

API

Linter

const linter = new Linter(config?);

// Lint a prompt
const result = await linter.lint(prompt);

// Add custom rule
linter.addRule(rule, severity?);

// Configure rule severity
linter.configureRule('rule-id', 'error');

// Disable rule
linter.disableRule('rule-id');

// Get all rule IDs
const ids = linter.getRuleIds();

// Get rule config
const severity = linter.getRuleConfig('rule-id');

createLinter

Factory function for creating configured linters:

import { createLinter } from '@promptier/lint';

const linter = createLinter({
  rules: { 'missing-identity': 'error' },
  custom: [myRule],
});

lint

Quick lint function for simple use cases:

import { lint } from '@promptier/lint';

const warnings = await lint(prompt);

LintResult

interface LintResult {
  passed: boolean; // No errors
  errors: LintWarning[]; // Severity: error
  warnings: LintWarning[]; // Severity: warning
  info: LintWarning[]; // Severity: info
  stats: {
    rulesChecked: number;
    timeMs: number;
    llmCalls: number; // Number of LLM calls made (semantic linting)
  };
}

Semantic Linting (LLM-powered)

Enable LLM-powered analysis to catch issues heuristics can't — contradictions, ambiguity, injection risks, verbosity, and more. Runs locally via Ollama.

ollama pull llama3.2:3b
const linter = new Linter({
  llm: {
    enabled: true,
    model: 'llama3.2:3b', // default
  },
});

const result = await linter.lint(agent);
// result.info may include semantic-* findings

Semantic Rules

| Rule | Category | Description | | --------------------------- | ------------- | -------------------------------------------- | | semantic-contradiction | contradiction | Instructions that conflict with each other | | semantic-ambiguity | ambiguity | Vague instructions open to misinterpretation | | semantic-injection-risk | security | Patterns vulnerable to prompt injection | | semantic-verbosity | token-budget | Redundant phrasing that wastes tokens | | semantic-missing-practice | best-practice | Missing error handling or edge case guidance | | semantic-scope-creep | best-practice | Instructions beyond the agent's stated role |

Custom LLM Client

You can bring your own LlmClient implementation instead of using the built-in Ollama client. This is useful for connecting to OpenAI, Anthropic, or any other provider:

import { Linter, type LlmClient } from '@promptier/lint';

const myClient: LlmClient = {
  modelName: 'gpt-4o',
  async generate(prompt, system) {
    // Call your preferred LLM provider
    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      },
      body: JSON.stringify({
        model: 'gpt-4o',
        messages: [
          ...(system ? [{ role: 'system', content: system }] : []),
          { role: 'user', content: prompt },
        ],
      }),
    });
    const data = await response.json();
    return data.choices[0].message.content;
  },
  async healthCheck() {
    return { ok: true };
  },
};

const linter = new Linter({
  llm: {
    enabled: true,
    client: myClient, // Bypasses built-in provider factory
  },
});

LlmClient Interface

interface LlmClient {
  generate(prompt: string, system?: string): Promise<string>;
  healthCheck(): Promise<{ ok: boolean; error?: string }>;
  readonly modelName: string;
}

| Method | Description | | --------------------------- | ----------------------------------------------------------------------------- | | generate(prompt, system?) | Send a prompt (with optional system message) and return the raw text response | | healthCheck() | Verify the provider is reachable and the model is available | | modelName | Display name of the configured model |

When client is provided on LlmConfig, the provider, model, host, and timeout fields are ignored — your client is used directly.

License

MIT