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

@se-studio/contentful-cms

v1.0.15

Published

CLI tool for AI agents to read and edit Contentful draft content without publish permissions

Readme

@se-studio/contentful-cms

A CLI tool for AI agents to read and edit Contentful draft content across all SE Studio apps. Provides a cms-edit binary with a snapshot → ref → edit → save workflow similar to agent-browser.

Key constraint: This tool has NO ability to publish, unpublish, archive, or delete published entries. All writes create drafts that a human must review and publish in Contentful.

Installation

npm install -g @se-studio/contentful-cms

Or from the monorepo root:

pnpm install
pnpm build

Contentful Role Setup (Required)

Before using this tool, create a dedicated AI Editor role in your Contentful space with restricted permissions. This provides defence-in-depth safety on top of the CLI's own restrictions.

Steps

  1. Go to Settings → Roles & Permissions in your Contentful web app
  2. Click Add Role and name it AI Editor
  3. Under Content permissions, set:
    • Entries: ✅ Read, ✅ Create, ✅ Edit — ❌ Publish, ❌ Unpublish, ❌ Archive, ❌ Delete
    • Assets: ✅ Read — ❌ everything else
  4. Under Space and Settings — leave all ❌
  5. Save the role
  6. Go to Settings → API KeysContent management tokens
  7. Create a Personal Access Token scoped to this role (or invite a service account user with this role and generate their token)
  8. Store the token as an environment variable (e.g. CMS_EDIT_TOKEN)

Configuration

Create .contentful-cms.json in your project root (copy from .contentful-cms.example.json):

{
  "defaultSpace": "om1",
  "spaces": {
    "om1": {
      "spaceId": "your-space-id",
      "environment": "master",
      "managementToken": "${CMS_EDIT_TOKEN_OM1}",
      "defaultLocale": "en-US",
      "devBaseUrl": "http://localhost:3013"
    },
    "brightline": {
      "spaceId": "another-space-id",
      "environment": "master",
      "managementToken": "${CMS_EDIT_TOKEN_BRIGHTLINE}",
      "defaultLocale": "en-US",
      "devBaseUrl": "${DEV_BASE_URL}",
      "requestParams": {
        "x-vercel-protection-bypass": "${VERCEL_BYPASS_TOKEN}"
      }
    }
  }
}

Token values use ${ENV_VAR} syntax and are resolved from your .env.local at runtime.

Deployment Protection Bypass

When your dev site is deployed behind Vercel deployment protection (or similar), cms-edit commands that navigate to devBaseUrl (e.g. screenshot) need to bypass the protection.

Via query parameter (Vercel protection bypass):

"requestParams": {
  "x-vercel-protection-bypass": "${VERCEL_BYPASS_TOKEN}"
}

The token is appended as a query parameter to every URL built from devBaseUrl.

Via HTTP header (custom auth headers):

"requestHeaders": {
  "x-custom-header": "${CUSTOM_HEADER_VALUE}"
}

The headers are injected into the agent-browser open call via --headers.

Both requestParams and requestHeaders values support ${ENV_VAR} token syntax. Set the token values in .env.local — they are never committed to version control.

Workflow

# 1. Open a page by slug — fetches the entry tree and starts a session
cms-edit open /pricing

# 2. View the content tree with @refs
cms-edit snapshot

# 3. Read a specific component's fields
cms-edit read @c2

# 4. Read a specific rich text field
cms-edit read @c2 body

# 5. Edit a scalar field
cms-edit set @c2 heading "New heading text"
cms-edit set @c2 showHeading true

# 5b. Set an Object/JSON field from a file
cms-edit set @c2 data --file chart-data.json

# 5c. Set an entry link field (e.g. page template)
cms-edit set @c0 template 3I0HxGKbUd173wIpFCsbVr --link

# 6. Edit a rich text field (Markdown input)
cms-edit rtf @c2 body "## Why it matters\n\nOur platform helps teams **move faster** with [confidence](https://example.com)."

# 7. Review changes before saving
cms-edit diff

# 8. Save all changes as Contentful drafts (NEVER publishes)
cms-edit save

# 9. Discard changes if needed
cms-edit discard

Command Reference

Navigation

| Command | Description | |---------|-------------| | open <slug> | Load a page/article by slug and start a session | | open <id> --id | Load by Contentful entry ID | | snapshot [-c] | Re-print content tree (-c for compact) | | read [ref] [field] | Read all fields of a ref, or a specific field | | diff | Show unsaved changes | | save | Write all changes to Contentful as drafts | | discard [--all] | Discard session changes |

Field Editing

| Command | Description | |---------|-------------| | set <ref> <field> <value> | Set a scalar field (string, boolean, number) | | set <ref> <field> --file <path> | Set an Object/JSON field from a file (e.g. data) | | set <ref> <field> --json '<json>' | Set an Object/JSON field from an inline JSON string | | set <ref> <field> <entry-id> --link | Set an entry link field (e.g. template) | | set <ref> <field> <refs-or-ids> --links [--append] | Set a content array (topContent, content, bottomContent, contents); replace by default, or append with --append | | rtf <ref> <field> "<markdown>" | Set a rich text field from Markdown | | rtf <ref> <field> --file <path> | Set rich text from a file | | rtf <ref> <field> - | Set rich text from stdin (e.g. cms-edit rtf @c2 body - < file.md) | | rtf embed <ref> <field> <entry-id> [--at N] | Insert embedded entry block; use --at N for 0-based position, omit to append | | rtf embed <ref> <field> <asset-id> --asset | Insert embedded asset block |

Structure

| Command | Description | |---------|-------------| | add <type> --content-type <ct> [--after <ref>] [--parent <ref>] [--target topContent\|content\|bottomContent] | Create and link a new entry; --content-type is required (e.g. component, collection, externalComponent, person) | | remove <ref> | Unlink from page (deletes if unreferenced draft) | | move <ref> [--after <ref2>] [--before <ref2>] | Reorder within parent |

add — content type is always explicit:

cms-edit add "Hero"           --content-type component
cms-edit add "Card Grid"      --content-type collection
cms-edit add "Research chart" --content-type externalComponent
cms-edit add "Dr. Jane Smith" --content-type person

For content types that follow the ${contentType}Type naming convention (e.g. componentcomponentType, externalComponentexternalComponentType), the <type> argument is stored in that field. For content types without a type discriminator the <type> is used as the initial cmsLabel only.

Create

| Command | Description | |---------|-------------| | create page --slug /x --title "X" | Create a new page entry | | create article --slug /x --title "X" --article-type-id <id> | Create a new article | | create template --label "X" | Create a new template entry |

Templates

Create a template: cms-edit create template --label 'Campaign Landing'. Edit: cms-edit open <template-id> --id. List template IDs: cms-edit list --type template.

Links (CTAs)

Put the ref after the subcommand (e.g. links add @c5).

| Command | Description | |---------|-------------| | links list <ref> | List CTA links on an entry | | links add <ref> --type external --label "X" --href <url> | Add an external link (requires --href) | | links add <ref> --type internal --label "X" --slug /page | Add an internal link by page/article slug | | links add <ref> --type internal --label "X" --id <entry-id> | Add an internal link by Contentful entry ID | | links add <ref> --type download --label "X" --asset-id <asset-id> | Add a download link to an asset (requires --asset-id) | | links remove <ref> <index> | Remove a link by index | | links move <ref> <from> <to> | Reorder links |

Examples:

cms-edit links list @c5
cms-edit links add @c5 --type external --label "Download PDF" --href "https://www.example.com/whitepaper.pdf"
cms-edit links add @c5 --type internal --label "Pricing" --slug /pricing
cms-edit links add @c5 --type internal --label "About" --id 4xKj2abcDef
cms-edit links add @c5 --type download --label "Download PDF" --asset-id 5xKj2abcDef
cms-edit links remove @c5 1
cms-edit links move @c5 2 0

Assets

| Command | Description | |---------|-------------| | asset search "<query>" | Search assets by title | | asset info <asset-id> | Show asset details | | asset set <ref> <field> <asset-id> | Set a visual/asset field |

Navigation Entries

| Command | Description | |---------|-------------| | nav open <slug-or-id> | Load a navigation entry | | nav add --label "X" --slug /page | Add a navigation item |

Discovery

| Command | Description | |---------|-------------| | types <content-type> | List valid type-discriminator values for any content type (looks up the ${contentType}Type field) | | colours | List valid backgroundColour and textColour values from the content model (one list each, no session required) | | search "<query>" | Full-text search across entries | | list --type <type> | List all entries of a content type (paginates automatically) |

types examples:

cms-edit types component          # lists componentType values
cms-edit types collection         # lists collectionType values
cms-edit types externalComponent  # lists externalComponentType values

Screenshot

Capture a PNG of a component, collection, external component, person, or page. Requires agent-browser (npm install -g agent-browser && agent-browser install). For @ref and --json-file, the app must be running at devBaseUrl (see .contentful-cms.json).

| Target | Command | |--------|---------| | Session ref (full-fidelity) | cms-edit screenshot @c0 — all types (component, collection, externalComponent, person) via convert API and /cms/preview/render-json | | JSON file (no Contentful) | cms-edit screenshot --json-file path/to/entry.json — IBase* JSON; validates or screenshots without a session | | By type (mock, no session) | cms-edit screenshot --component HeroSimple, cms-edit screenshot --collection CardGrid | | Page | cms-edit screenshot (current page) or cms-edit screenshot /pricing |

Options: --out <path>, --full, --embedded, --wait <ms>, --url-only (print URL only), --json (machine-readable output). For --component / --collection: --param key=value (repeatable) to override showcase controls (e.g. --param backgroundColour=Navy --param textColour="Off White"). --width <px> and --height <px> set viewport size before capture (e.g. --width 375 for mobile).

Use --out before.png / --out after.png with agent-browser diff screenshot for visual diffing. See the screenshots skill for details.

Batch screenshots: The cms-capture-screenshots script (same package) captures multiple variants to <app-dir>/docs/cms-guidelines/screenshots/. Run it from the monorepo root so the output path is correct: node packages/contentful-cms/dist/bin/cms-capture-screenshots.js --variants /tmp/type-variants.json --app-dir apps/example-se2026.

export-converted

Export a session ref's entry as converted (IBase*) JSON via the app's convert API. Use the output with screenshot --json-file for custom variants without a session.

cms-edit open /your-page
# Snapshot shows refs; find the component ref (e.g. @c1)
cms-edit export-converted @c1 --out hero-base.json

App must be running at devBaseUrl. See example-brightline docs/cms-edit-hero-variants.md for a full workflow (Hero variants, viewport widths, custom params).

Machine-readable Output (--json)

All commands support JSON output via the global --json flag or the CMS_EDIT_JSON=1 environment variable (see JSON Mode above). In addition, a subset of data-query commands also accept a per-command --json flag:

| Command | --json output | |---------|-----------------| | list --type <type> --json | JSON array of entry objects including all fields | | search "<query>" --json | JSON array of entry objects including all fields | | read <ref> --json | JSON object for one entry including all fields | | read <ref> <field> --json | JSON value for a single field | | asset info <id> --json | JSON object for one asset |

Entry JSON shape (for list, search, read):

{
  "id": "abc123",
  "contentType": "article",
  "title": "My Article",
  "slug": "my-article",
  "status": "published",
  "updatedAt": "2024-01-01T00:00:00Z",
  "fields": {
    "title": "My Article",
    "slug": "my-article",
    "articleType": { "id": "entryId", "linkType": "Entry" },
    "download": { "id": "assetId", "linkType": "Asset" },
    "tags": ["tag1", "tag2"],
    "body": "## Heading\n\nRich text rendered as Markdown."
  }
}

Fields are flattened to the space's default locale. Link fields become { id, linkType } objects. Rich text fields become Markdown strings.

Asset JSON shape (for asset info):

{
  "id": "assetId",
  "title": "My PDF",
  "fileName": "report-2024.pdf",
  "contentType": "application/pdf",
  "url": "https://assets.ctfassets.net/...",
  "width": null,
  "height": null,
  "size": 102400
}

Progress messages from list are always written to stderr, so they do not contaminate the JSON on stdout.

Scripting example: build an article–asset mapping

# 1. Get all articles as JSON (no session required)
cms-edit list --type article --json > articles.json

# 2. For each article, fetch the download asset filename
jq -r '.[].fields.download.id // empty' articles.json | while read assetId; do
  # Requires an open session for space resolution — run `cms-edit open /any-page` first
  cms-edit asset info "$assetId" --json
done

Global Options

--space <name>    Override the default space (from config)
--config <path>   Custom config file path
--json            Output all results as machine-readable JSON (see below)
--docs            Print absolute path to this README and exit (for LLM or script tooling)

Use cms-edit --docs to get the path to the README so an LLM or script can read it.

JSON Mode (LLM / machine-readable output)

Enable JSON mode to get machine-readable output from every command. Two ways to activate:

# Global flag (placed before the subcommand name):
cms-edit --json snapshot
cms-edit --json diff
cms-edit --json set @c2 heading "New title"
cms-edit --json add "Research chart" --content-type externalComponent --target content

# Environment variable (set once for the whole session):
export CMS_EDIT_JSON=1
cms-edit snapshot      # → JSON
cms-edit diff          # → JSON
cms-edit set @c2 data --file chart.json  # → JSON

JSON output shapes

Action commands (set, rtf, add, remove, move, save, discard, links, …):

{"ok": true, "message": "Set heading on @c2 to: New title"}
{"ok": false, "error": "Ref @c99 not found. Available refs: @c0, @c1, @c2"}
{"warn": "Entry is saved to Contentful but changes to the parent page are unsaved."}

Multiple lines may appear for a single command (one per message). Errors go to stderr; warnings and results go to stdout.

snapshot / tree:

{
  "rootType": "article",
  "rootSlug": "/publications/study-1",
  "rootId": "4xKj2abc",
  "rootStatus": "published",
  "spaceKey": "om1",
  "fetchedAt": "2026-03-05T10:00:00.000Z",
  "unsavedChanges": 1,
  "pendingDeletions": 0,
  "entries": [
    {
      "ref": "@c0",
      "entryId": "abc123",
      "contentType": "component",
      "type": "RichText",
      "label": "Introduction",
      "status": "published",
      "depth": 1,
      "parentRef": null,
      "parentField": "content"
    },
    {
      "ref": "@c1",
      "entryId": "def456",
      "contentType": "externalComponent",
      "type": "Research chart",
      "label": "Figure 1: Cost-Efficiency",
      "status": "draft",
      "depth": 1,
      "parentRef": null,
      "parentField": "content"
    }
  ]
}

diff:

{
  "hasChanges": true,
  "modified": [
    {
      "ref": "@c1",
      "entryId": "def456",
      "contentType": "externalComponent",
      "type": "Research chart",
      "label": "Figure 1",
      "isNew": false,
      "fields": {
        "heading": {"before": null, "after": "Figure 1: Cost-Efficiency"},
        "data": {"before": null, "after": {"type": "bar", "categories": ["A", "B"]}}
      }
    }
  ],
  "deletions": []
}

read, list, search, asset info: same JSON shapes as their existing per-command --json flag.

Order of operations: article + CTA

To add body content and a CTA (e.g. PDF download) to an article:

  1. Open the article: cms-edit open <article-slug> or cms-edit open <id> --id
  2. Set or add body: If the article has a rich text component, use cms-edit rtf @<ref> body "..." or cms-edit rtf @<ref> body --file path/to.md. If you need to add a new body component first, use add then rtf.
  3. Add CTA at bottom: cms-edit add CTA --content-type component --target bottomContent (or add to main content with --after @<ref> if you prefer)
  4. Set CTA links: Use one of:
    • External (e.g. PDF URL): cms-edit links add @<ctaRef> --type external --label "Download PDF" --href <url>
    • Download (Contentful asset): cms-edit links add @<ctaRef> --type download --label "Download PDF" --asset-id <asset-id> (get the ID from cms-edit asset search "..." or cms-edit asset info <id>)
  5. Save: cms-edit save

Order of operations: article + Research chart

To populate an article with a Research chart (external component):

# 1. Open the article
cms-edit open <article-slug>

# 2. Discover valid externalComponentType values
cms-edit types externalComponent

# 3. Add a Research chart to the content array
cms-edit add "Research chart" --content-type externalComponent --target content

# 4. Set the heading (scalar field)
cms-edit set @c2 heading "Figure 1: Cost-Efficiency of Data Automation"

# 5. Set the data field from a JSON file
cms-edit set @c2 data --file figure1-data.json

# 6. Set the additionalCopy footnote (rich text)
cms-edit rtf @c2 additionalCopy --file footnote.md

# 7. Review and save
cms-edit diff
cms-edit save

Snapshot Format

Page: /pricing  [4xKj2abc | published]  ·  om1

  @c0   Component[HeroSimple]             "Transparent Pricing"         [changed]
  @c1   Component[RichText]               "How it works"                [published]
  @c2   Collection[CardGrid]              "Plans"                       [published]
    @c3   Component[Card]                   "Starter"                   [published]
    @c4   Component[Card]                   "Pro"                       [draft]
  @c5   ExternalComponent[Research chart] "Figure 1: Cost-Efficiency"   [draft]
  @c6   Component[CTA]                    "Get started today"           [published]
  @c7   Person                            "Dr. Jane Smith"              [published]

The type label follows the ${contentType}Type convention:

  • componentComponent[<componentType>]
  • collectionCollection[<collectionType>]
  • externalComponentExternalComponent[<externalComponentType>]
  • Any content type without a type discriminator → capitalised content type ID (e.g. Person)

Status badges:

  • [published] — live, no pending changes
  • [draft] — never published
  • [pending] — published but has newer draft in Contentful
  • [changed] — has unsaved local changes (run save)

Rich Text (Markdown) Support

The rtf command accepts basic Markdown:

# Heading 1      → heading-1
## Heading 2     → heading-2
**bold**         → bold mark
_italic_         → italic mark
***bold italic***
[text](url)      → hyperlink
- item           → unordered list
1. item          → ordered list
> quote          → blockquote
---              → horizontal rule
`code`           → code mark

Multi-space Usage

cms-edit --space om1 open /pricing
cms-edit --space brightline open /home

What This Tool Cannot Do

By design, the following operations are not available:

  • Publish entries (no publish command)
  • Unpublish entries
  • Archive entries
  • Delete published entries
  • Upload new assets

These restrictions are enforced at two levels:

  1. Contentful Role — the management token should use a role with no publish permissions
  2. CLI — no publish/delete commands exist in this tool

All saves create draft versions that remain invisible to visitors until a human reviews and publishes them in the Contentful web app.