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

@mjml-agent-editor/core

v0.1.8

Published

Shared contract for the MJML agent editor: document addressing, stable IDs and tool schemas.

Readme

Core

The shared contract. No framework, no I/O, no AI SDK — everything both agent backends have to agree on, and nothing else.

Why this package exists

The spike had the same logic in two inconsistent copies: a Python module that addressed MJML sections with regexes, and a second implementation embedded in the React editor. They disagreed about how ids were generated (secrets.token_hex(4) against Math.random().toString(36).slice(2, 10), which sometimes produced fewer than eight characters), and the regex parser fell over on nested markup.

Now there is one implementation, and contract/tools.json — emitted by this package's build — is what the TypeScript and Python backends both read. Tests on both sides fail if either drifts from it.

Document addressing

Every mj-section carries a stable id in its css-class (sec-<id>, eight lowercase hex characters). Comments and the agent's tools address sections by it, and no operation is allowed to change one.

import {
  listSections,
  getSection,
  replaceSection,
  ensureSectionIds,
} from "@mjml-agent-editor/core";

const withIds = ensureSectionIds(mjml); // adds sec-… to sections that lack one
const sections = listSections(withIds.mjml);
const updated = replaceSection(withIds.mjml, sections[0].id, "<mj-section …>…</mj-section>");

The scanner is quote-aware and depth-aware: an attribute containing > does not end a tag, and a nested mj-section does not close its parent. Both were bugs in the regex version.

Ports

Storage and I/O are interfaces the host implements — DocumentStore, CommentStore, ImageProvider, MjmlCompiler. Nothing here opens a connection or reads a file, which is what lets the same code run in a Next.js route handler, a test with an in-memory object, or someone else's application entirely.

Field names are camelCase throughout. Mapping to snake_case database columns belongs in the adapter — see @mjml-agent-editor/store-supabase.

Tools and prompt

The nine tool definitions (get_document, set_section, insert_section, generate_image, resolve_comment, …) and the system prompt live here as typed constants. contract/tools.json is the generated artifact both backends load; it is committed, and because the build rewrites it, the check that catches drift is git diff after a build rather than a unit test.

The starter document

STARTER_MJML is the MJML a brand-new email begins as. It lives here rather than in the editor package because the code that needs it is the code that creates a document, and that runs on a server — @mjml-agent-editor/editor ships with a "use client" banner, so importing the constant from there in a route handler gives you a client reference instead of a string. The editor still re-exports it for browser callers.

Template catalogs

A host can offer more than one starting point without shipping MJML to the browser. The split that makes that true is two modules: labels the client may hold, bodies only the server reads. An id is the only thing that crosses.

// templates.ts — safe to import from a client component.
import { isTemplateId, type TemplateIdentity } from "@mjml-agent-editor/core";

export const TEMPLATES = [
  { id: "blank", name: "Blank", blurb: "Start empty." },
  { id: "newsletter", name: "Newsletter", blurb: "Monthly update." },
] as const satisfies readonly TemplateIdentity[];

export const isKnownTemplate = (value: unknown) => isTemplateId(TEMPLATES, value);
// template-bodies.ts — server only.
import { starterBody, type TemplateEntry } from "@mjml-agent-editor/core";

const CATALOG = [
  { id: "blank", mjml: STARTER_MJML },
  { id: "newsletter", mjml: NEWSLETTER },
] as const satisfies readonly TemplateEntry[];

export const bodyFor = (id?: (typeof CATALOG)[number]["id"]) => starterBody(CATALOG, id);

The names are the host's. This package reads only id and mjml — TemplateIdentity is the first, TemplateEntry is both, and EmailTemplate is an optional ready-made shape whose labels are all optional because none of them mean anything here.

EmailTemplate and TemplateEntry carry mjml. Importing either into a client component and mapping over it puts every template body in the bundle, to render a list of names. That is the mistake this section exists to prevent, so keep the client on TemplateIdentity.

validateTemplateMjml and validateTemplateCatalog check the invariants the editor and agent depend on: no handwritten sec-*/obj-* ids, no opaque tags such as mj-hero or mj-table, and every written mj-section must be visible to scanSections.

A worked catalog lives in the example app at apps/example/lib/templates.ts. It is not published: what a starting point should say is a product decision, and a package that ships email copy is a package deciding it for you.

Document size guard

assertDocumentSize(mjml, { maxBytes }) and DocumentTooLargeError give hosts one typed way to reject oversized documents from both browser saves and agent tool calls. The package does not choose a limit; demos and internal tools should set their own.

Documentation