JSPM

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

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

Package Exports

  • @se-studio/contentful-cms

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"
    },
    "brightline": {
      "spaceId": "another-space-id",
      "environment": "master",
      "managementToken": "${CMS_EDIT_TOKEN_BRIGHTLINE}",
      "defaultLocale": "en-US"
    }
  }
}

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

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

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.

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

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.