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

v3.15.6

Published

CLI tool for AI agents to edit Contentful drafts and, when the user's role allows it, publish after an explicit yes

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.

Key constraint: save writes drafts. Publish, unpublish, and archive need publish plan then publish apply after an explicit yes, and a Contentful role that allows them. AI Editor cannot.

Installation

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

Or from the monorepo root:

pnpm install
pnpm build

MCP Setup (hosted)

Editors connect Claude via hosted cms-edit MCP (OAuth) — no Node.js or local setup wizard.

  1. Deploy a thin Next.js host (cms-edit host scaffold) or use an existing site host
  2. Configure OAuth env vars (see HOSTED.md)
  3. Send editors the /cms-edit onboarding URL

Shared route logic lives in @se-studio/contentful-cms/hosted/next. Editor walkthrough: INSTALL.md.

Local stdio MCP is not supported — do not register mcpServers.cms-edit in Claude Desktop. Developers and agents use the hosted connector URL from the site’s /cms-edit page. The local cms-edit binary only runs tooling (host, editor-pack, patterns, project, schema, guide). Content commands (open, save, set, …) are refused on the CLI.

What hosted MCP exposes

A single cms_edit tool that accepts an args array matching any cms-edit CLI command:

["open", "/pricing"]
["snapshot"]
["set", "@c0", "heading", "New heading"]
["save"]

All output is JSON. JSON bulk: --json or --json-base64. RTF: --content or --base64. No --file or stdin on MCP.

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 Keys → Content 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)

AI Editor cannot publish. People who should publish, unpublish, or archive need those actions on their Contentful role. cms-edit then uses publish plan and publish apply after an explicit yes. save still writes drafts.

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": "https://om1.localhost"
    },
    "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), commands that build URLs from devBaseUrl (e.g. preview url, preview showcase) 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}"
}

requestHeaders are read by cms-capture-screenshots when driving agent-browser (query bypass uses requestParams via cms-edit preview showcase).

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

Use one explicit lookup flag per open (see docs/help/lookup.md). Positional open <slug> is no longer supported.

# 1. Open a page by slug — fetches the entry tree and starts a session
cms-edit open --page-slug /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 — default format when no flag)
cms-edit rtf @c2 body --markdown "## 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 --page-slug <slug> | Load a page (see docs/help/lookup.md); hard-fails if session is dirty | | open --resume | Re-print active session without refetch (keeps staged edits) | | open --overwrite --confirm-discard … | Replace session even if dirty (discards staged field edits) | | 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). Rich Text: optional --rtf-text | --rtf-markdown (default) | --rtf-html | --rtf-json | | set @c1:field="val" @c2:field="val" … | Bulk scalar update — same Rich Text flags apply to all assignments | | set <ref> <field> --file <path> | Set from file — for Rich Text fields the JSON must be a full Rich Text document (nodeType: document) | | set <ref> <field> --json '<json>' | Inline JSON — for Rich Text fields the value must be a full Rich Text document object | | 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> "<content>" | Set Rich Text — format: --text | --markdown (default) | --html | --rtf-json | | rtf <ref> <field> --file <path> [--markdown\|--html\|…] | Set from file with matching format flag | | rtf <ref> <field> - [--format] | Set from stdin (e.g. cms-edit rtf @c2 body --markdown - < file.md) | | rtf replace <ref> <field> --find "…" --replace-plain "…" | Surgical replace — replacement: --replace-plain | --replace (Markdown) | --replace-html | --replace-json | | rtf patch <ref> <field> | JSON ops — use replaceIsMarkdown, replaceIsHtml, replaceIsJson, or replaceFormat per op | | 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 [--at N] | Insert embedded asset block (embedded-asset-block); Rich Text only allows data.target — no per-block width or alignment in the JSON |

Rich text embeds — asset vs Media (width / alignment):

  • Raw asset (--asset): embeds the file as-is. Layout comes from the asset/visual defaults only; you cannot store alignment or width on the block (Contentful validates embedded-asset-block.data and rejects extra properties).
  • Media entry (omit --asset, pass a Media entry ID): embed a Media entry whose fields include width (percentage) and horizontal position. The delivery API resolves it to an IVisual and apps render it via the same RTF embedded-entry path (VisualComponent). Create a Media entry from the CLI (does not replace your session), then embed by ID:
    cms-edit create media --asset-id <asset-id> --name "Figure 1" --position Middle --width 40
    cms-edit rtf embed @c1 body <media-entry-id> --at 4
    Raw asset for comparison: cms-edit rtf embed @c1 body <asset-id> --asset If the same image needs different width or alignment in two places, use two Media entries that reference the same underlying asset (layout is on the Media entry, not per RTF occurrence).

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). --parent derives the target field from the parent's content type: collections use contents, pages/articles default to content (override with --target). | | 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. component → componentType, externalComponent → externalComponentType), 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" [--description "…"] [--cms-label "…"] [--template-id <id>] [--featured-image <asset-id>] [--indexed] [--hidden] | Create a new page entry; all fields after --title are optional | | create article --slug /x --title "X" --article-type-id <id> | Create a new article | | create from-json --json '<json>' | Create a complete page or article from JSON (--json-base64 for large payloads) | | create from-json --json '…' --dry-run --strict | Validate locally (no CMA). Fails on content trees, empty components, componentType | | export from-json | Dump the open session as from-json (ids + versions) | | apply from-json --json '…' | Update/create from that document (1 CMA update per changed entry) | | create taxonomy-from-json --json '…' [--if-not-exists] [--dry-run] | Batch-create tagType, articleType, and tag entries from JSON | | create person-from-json --json '…' [--if-not-exists] [--dry-run] | Batch-create person/author entries from JSON | | create person --slug <slug> --name "X" [--job-title <t>] [--person-type-id <id>] [--person-type-slug <slug>] [--indexed true\|false] [--hidden true\|false] [--if-not-exists] [--dry-run] | Create a person entry (draft) | | create person-type --slug <slug> --name "X" [--person-page-template-id <id>] [--index-page-template-id <id>] [--index-page-top-content-ids <ids>] [--person-page-indexed true\|false] [--person-page-hidden true\|false] [--indexed true\|false] [--hidden true\|false] [--if-not-exists] [--dry-run] | Create a personType entry (draft). Index flags default indexed on and hidden off. Person-page flags stay unset when omitted. | | create person … [--bio-markdown <md>] or [--bio-file <path>] | Bio as Markdown (mutually exclusive paths) | | create tag-type --slug <slug> --name "X" [--json-file <path>] [--if-not-exists] [--dry-run] | Create a tagType entry (draft) | | create article-type --slug <slug> --name "X" [--json-file <path>] [--if-not-exists] [--dry-run] | Create an articleType entry (draft) | | create tag --slug <slug> --name "X" --tag-type <tagType-slug> [--tag-type-id <id>] [--description <desc>] [--featured-image <asset-id>] [--json-file <path>] [--if-not-exists] [--dry-run] | Create a tag entry (draft) | | create template --label "X" | Create a new template entry | | create media --asset-id <id> --name "X" [--position Left\|Middle\|Right] [--width <1–100>] | Create a Media entry (draft); does not start a new cms-edit session; prints entry ID for rtf embed | | create banner / create page-test / create page-variant / create custom-type | Draft creates for site banners, experiments, page variants, and custom types. See cms-edit help create-editor-entries |

create from-json notes: Optional cmsLabel on each component helps editors tell instances apart. Asset and Media links use explicit shorthands (featuredImage, featuredImageAssetFilename, visualAssetFilename, visualMedia, …) — see cms-edit help create-from-json. Unknown JSON keys warn by default; --strict or --dry-run fails instead. String fields values become Rich Text when they auto-detect as Markdown, HTML, or Rich Text JSON; otherwise use { "value", "format" }. Slugs should not end with / (the CLI strips one and warns). *AssetFilename keys require cms-edit index sync.

Taxonomy creates: Use --json-file for full field control on tagType, articleType, and tag entries. See cms-edit help fields-taxonomy and cms-edit help taxonomy-from-json. Idempotent imports: cms-edit create tag-type … --if-not-exists.

Idempotent flags (--if-not-exists, --if-exists-by-filename): These use check-then-create against Contentful. They are safe for sequential import scripts, but not safe under parallel runs on the same slug or fileName — two concurrent commands can both create entries. Serialize imports per slug/fileName, or re-run with the idempotent flag to converge.

| Command | Description | |---------|-------------| | article-tags-from-json --file <path> [--dry-run] | Set tags on multiple articles from JSON | | bulk-rtf-replace --find <text> --replace-plain <text> [--include page,article] [--prefix <slug>] [--limit <n>] [--dry-run] | Find-replace plain text in Rich Text across page/article trees |

Set CMS_EDIT_JSON=1 for machine-readable output ({ ok, id, slug, contentType, status }).

Templates

Create a template: cms-edit create template --label 'Campaign Landing'. Edit: cms-edit open --id <template-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 add <ref> … --variant link\|button | Set link variant on create (see cms-edit schema link) | | links set <ref> <index> <field> <value> | Set a scalar field on an existing link entry (e.g. variant link) | | 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 set @c5 0 variant link
cms-edit links remove @c5 1
cms-edit links move @c5 2 0

Batch edit (by entry ID)

Edit unrelated standalone entries without a page session. Uses session name batch by default (--session to override). Scalar fields only in v1.

| Command | Description | |---------|-------------| | batch set <entryId>:field=value … | Queue scalar updates for one or more entry IDs | | batch diff | Show all pending batch changes | | batch save | Write all batch changes as drafts | | batch discard | Clear batch session without saving |

Example — link variant sweep:

cms-edit batch set \
  3my7n2M84OrhOaaSLHVHvo:variant=link \
  7MjsLmOq52CR9C5xrYlcub:variant=button
cms-edit batch diff
cms-edit batch save

See cms-edit help batch for limits and named sessions.

Content type definitions (field types, enum validations) are cached on the session and under ~/.contentful-cms/content-types/<spaceId>/<environment>/ (24-hour TTL) so batch workflows and repeated sets avoid redundant CMA calls.

Content index (assets, media, content catalog)

Build a local SQLite index from the Content Preview API before filename search, media-by-filename list, asset audit, or fast page/article/taxonomy/nav lookup:

cms-edit index sync          # draft assets, media, full content catalog (default)
cms-edit index sync --verbose  # progress on stderr (or LOG_CMS_INDEX=1)
cms-edit index status
cms-edit index dump          # browse index in the terminal
cms-edit index dump --reference-only
cms-edit index sync --published   # separate DB for published-only content

Catalog types (page, article, pageVariant, template, articleType, tagType, tag, navigation, navigationItem) list from the index when present; use list --type page --force-cma to bypass. Scoped filters: list --type tag --tag-type-name "…", list --type article --article-type-slug …. Open/resolve use flat flags (--page-slug, --page-cms-label, --nav-name, etc.) — see docs/help/lookup.md.

Auto-sync: Index-backed commands refresh the index automatically when it is missing, on an outdated schema (v3), or older than 24h (incremental upsert when only stale). Manual index sync always does a full rebuild. See docs/help/index-sync.md.

Agent skills: Edit packages/skills/skills/contentful-cms-*, then pnpm --filter @se-studio/contentful-cms sync:skills. See docs/SKILLS.md.

Configure optional tokens in .contentful-cms.json (previewAccessToken for default sync; deliveryAccessToken, deliveryApiKeyName, autoCreateDeliveryApiKey for --published) — see docs/MEDIA_INDEX_PLAN.md. If unset, index sync resolves a preview or delivery key via the management token.

Assets

| Command | Description | |---------|-------------| | asset search [query] | Search assets by title (requires index); --filename / --filename-match for fileName | | asset audit | List image assets missing alt text (requires index) | | asset review | Full visual asset review — filename, alt, dimensions, size, GIF (requires index; use --json for spreadsheets) | | asset upload --base64 <encoded> --mime <type> [--file-name <name>] | SVG/PDF/Lottie only (local CLI). Raster/video rejected | | asset upload --staged <uploadId> --mime <type> --file-name <name> | SVG/PDF/Lottie on hosted MCP. Raster/video: use media prepare | | asset upload --url <https-url> [--mime <type>] [--file-name <name>] | Pipeline-ready images (Blob pack host) or SVG/PDF/Lottie. Video: cms_edit_media import only | | asset upload … --with-media [--media-name "X"] [--media-position Left\|Middle\|Right] [--media-width <1–100>] | Also create a draft Media wrapper | | asset upload … --if-exists-by-filename | Return existing asset (and media if --with-media) when fileName matches | | asset info <asset-id> | Show asset details | | asset search --concept <id> [--concept-all <id>] | Live concept search (id, title, file name, description, concepts). Not the blog tag index | | asset set-title <asset-id> <title> | Set the asset title. Draft. Does not change the file name | | asset replace <asset-id> --url <ready-url> [--dry-run] | New binary on the same id. Keeps the file name. Draft. Raster/video URL must be a processed pack | | asset concept assign\|unassign <asset-id> <concept-id...> | Write metadata.concepts. Fails if the concept does not exist | | asset set <ref> <field> <asset-id> | Set a visual/asset field |

JSON output for upload (CMS_EDIT_JSON=1): { ok, id, fileName, url, mediaId?, mediaIds? }. Bytes that pass through this process are limited to 10MB (local/URL/base64) or 4MB (hosted function POST for JSON, text, SVG, PDF, and Lottie). Video, and an image master over 4MB, use a presigned direct upload of up to 100MB, then the media pipeline. Contentful stores the processed file. With --if-exists-by-filename, run uploads one at a time per fileName if your script uses parallelism (see idempotent note above).

Navigation Entries

| Command | Description | |---------|-------------| | nav open --nav-name <name> | Load a navigation entry (--nav-id also supported) | | nav add --label "X" --slug /page | Add a navigation item |

Discovery

| Command | Description | |---------|-------------| | schema types <content-type> | List valid type-discriminator values for any content type (looks up the ${contentType}Type field) | | schema 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) | | list --type media --asset-id <id> | List Media entries linked to an asset | | list --type media --asset-filename <name> | List Media entries by exact asset fileName | | list --type media --asset-filename-match <pattern> | List Media entries by asset fileName regex (requires index) | | resolve --page-slug <slug> | Resolve entry by flat lookup flag (no session). See docs/help/lookup.md |

Asset search, media list by filename, and asset audit use the local index (no bulk CMA asset scans). Run cms-edit index sync first — see docs/help/index-sync.md.

schema types examples:

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

Preview URLs

Generate URLs for visual checks with a browser MCP (no capture built into cms-edit):

| Target | Command | |--------|---------| | Page slug | cms-edit preview url /pricing (or preview url with an open session) | | Component showcase | cms-edit preview showcase --component HeroSimple | | Collection showcase | cms-edit preview showcase --collection CardGrid --param backgroundColour=Navy |

Requires devBaseUrl in .contentful-cms.json. Use --json for machine-readable output.

CMS guidelines batch capture: @se-studio/project-build ships cms-capture-screenshots for the guidelines pipeline only (requires agent-browser). Run from the site repo root: cms-capture-screenshots --variants /tmp/type-variants.json --app-dir <app-dir> (e.g. apps/example-empty here, or the app path in a customer repo).

Machine-readable Output (--json)

All commands support JSON output via the global --json-output 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 --page-slug /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-output snapshot
cms-edit --json-output diff
cms-edit --json-output set @c2 heading "New title"
cms-edit --json-output 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": "Created entry is in Contentful with default fields only. Field edits (set/rtf) … until cms-edit save."}

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 <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 --markdown "..." or cms-edit rtf @<ref> body --markdown --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 <slug>

# 2. Discover valid externalComponentType values
cms-edit schema 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 --markdown --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:

  • component → Component[<componentType>]
  • collection → Collection[<collectionType>]
  • externalComponent → ExternalComponent[<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 input formats

Use exactly one of --text, --markdown (default), --html, or --rtf-json with cms-edit rtf and rtf edit. For scalar cms-edit set on Rich Text fields, use the --rtf-* flags.

Markdown (--markdown) supports:

# 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

HTML (--html): subset including h1–h6, p, ul/ol/li, blockquote, table, a, strong/em, br.

JSON (--json): full Contentful Rich Text document JSON.

Plain text (--text): paragraph breaks from \n\n or \n; inline **bold** etc. still parsed per Markdown inline rules.

Multi-space Usage

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

What This Tool Cannot Do

By design, the following operations are not available:

  • Delete published entries
  • Scheduled publish

Publish, unpublish, and archive exist as plan then apply. They run only when the signed-in Contentful role allows that action, and only after an explicit yes. save still creates drafts.

  1. Contentful Role — AI Editor cannot publish, unpublish, or archive. A role that can, can do those actions through cms-edit.
  2. Plan — publish plan does not write. publish apply --plan <id> runs only after the user says yes.