JSPM

  • Created
  • Published
  • Downloads 802
  • Score
    100M100P100Q122315F
  • License MIT

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

Package Exports

  • @se-studio/contentful-cms
  • @se-studio/contentful-cms/hosted

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

MCP Setup (Claude Desktop / Claude Code)

cms-edit ships an MCP server so Claude can call it as a tool directly — no manual CLI use needed.

Claude Desktop

The quickest way to set up Claude Desktop is the interactive setup wizard:

npx @se-studio/contentful-cms@latest setup

It discovers your Contentful spaces and environments automatically from your management token, writes ~/.contentful-cms.json, and updates the Claude Desktop config. See INSTALL.md for a full walkthrough.

Hosted mode (no Node.js for editors): deploy apps/cms-edit-host to Vercel, build a per-client .mcpb extension (pnpm mcpb:pack -- --customer <key> — see mcpb/README.md), and share the .mcpb file plus HOSTED.md with your content team.

Manual config — if you'd prefer to configure by hand, add this to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "cms-edit": {
      "command": "npx",
      "args": ["-y", "@se-studio/contentful-cms@latest"],
      "cwd": "/path/to/your/project"
    }
  }
}
  • cwd must point to the directory containing your .contentful-cms.json (or omit cwd and place the file in your home directory — ~/.contentful-cms.json on macOS/Linux, C:\Users\<you>\.contentful-cms.json on Windows).
  • npx -y @se-studio/contentful-cms always pulls the latest published version — no global install needed.

If you have the package installed globally, you can use "command": "cms-edit-mcp" (no args) instead.

Claude Code

claude mcp add cms-edit -- npx -y @se-studio/contentful-cms

Then set env vars in your project's .env.local (they are picked up automatically via the config's ${ENV_VAR} syntax).

What the MCP server 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. Stdin-mode commands (rtf with -) are not supported via MCP — use --file instead.

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

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

Command Description
open --page-slug <slug> Load a page (see docs/help/lookup.md)
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. 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" [--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 --file <path> Create a complete page or article (with all components) from a declarative JSON file
create from-json --file <path> --dry-run Preview the creation plan without writing to Contentful
create from-json --file <path> --strict Fail on unknown JSON keys (also strict on --dry-run)
create taxonomy-from-json --file <path> [--if-not-exists] [--dry-run] Batch-create tagType, articleType, and tag entries from JSON
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] 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 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 ensure tag-type … (alias for create with --if-not-exists).

Idempotent flags (--if-not-exists, ensure, --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.

Ensure (idempotent create)

Command Description
ensure tag-type --slug <slug> --name "X" [--json-file <path>] Create tagType if missing; return existing ID when slug matches
ensure article-type --slug <slug> --name "X" [--json-file <path>] Create articleType if missing
ensure tag --slug <slug> --name "X" --tag-type <slug> [--description <desc>] [--featured-image <asset-id>] [--json-file <path>] Create tag if missing

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.

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 upload [file-path] Upload a local file (draft asset)
asset upload --file <path> Upload from explicit file path
asset upload --base64 <encoded> --mime <type> [--file-name <name>] Upload from base64
asset upload --url <https-url> [--mime <type>] [--file-name <name>] Fetch and upload from URL
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 set <ref> <field> <asset-id> Set a visual/asset field

JSON output for upload (CMS_EDIT_JSON=1): { ok, id, fileName, url, mediaId?, mediaIds? }. Uploads are limited to 10MB per file (local, URL, or base64). With --if-exists-by-filename, run uploads one at a time per fileName if your script uses parallelism (see idempotent note above).

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
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)
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.

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.

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 --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 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 <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 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:

  • 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 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 h1h6, 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:

  • 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.