diff --git a/CLAUDE.md b/CLAUDE.md index 46d5da2..ae6c503 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ Use these references instead: When working with Contentrain content operations (models, content, normalize, validate), you MUST follow the rules and skills in this repo: -### Essential Rules (always-loaded, ~86 lines) +### Essential Rules (always-loaded) - `packages/rules/essential/contentrain-essentials.md` — compact guardrails for every conversation ### Skills (Agent Skills standard, on-demand) diff --git a/README.md b/README.md index 8fc985f..83dd534 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ npx contentrain studio connect # connect repo to Studio project - **[Getting Started](https://ai.contentrain.io/getting-started)** — install, connect an agent, and run the first workflow - **[Normalize Guide](https://ai.contentrain.io/guides/normalize)** — the main hardcoded-string rescue flow - **[Ecosystem Map](https://ai.contentrain.io/ecosystem)** — package-to-product bridges across AI and Studio -- **[Contentrain Studio](https://ai.contentrain.io/studio)** — open-core team operations for Git-native structured content, self-hostable or available as a managed Pro/Enterprise offering +- **[Contentrain Studio](https://ai.contentrain.io/studio)** — open-core team operations for Git-native structured content, self-hostable or available as managed plans (Starter/Pro/Enterprise) - **[Full Docs](https://ai.contentrain.io)** — guides, package reference, and framework integration ## Development diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 571f8e4..347f800 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,6 +1,80 @@ +import { readdirSync, readFileSync, writeFileSync } from 'node:fs' +import { join, relative, resolve } from 'node:path' import { defineConfig } from 'vitepress' +/** + * Preferred order for /llms-full.txt — mirrors the sidebar. Pages not listed + * here are appended afterwards so the corpus stays complete when a page is + * added without updating this list. + */ +const LLMS_PAGE_ORDER = [ + 'index', + 'getting-started', + 'demo', + 'ecosystem', + 'concepts', + 'studio', + 'packages/mcp', + 'packages/cli', + 'packages/sdk', + 'packages/rules', + 'packages/types', + 'guides/providers', + 'guides/http-transport', + 'guides/embedding-mcp', + 'guides/normalize', + 'guides/frameworks', + 'guides/i18n', + 'guides/serve-ui', + 'reference/model-kinds', + 'reference/field-types', + 'reference/config', + 'reference/providers', +] + +function allMarkdownPages(dir: string, root: string): string[] { + const out: string[] = [] + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'public') continue + const full = join(dir, entry.name) + if (entry.isDirectory()) out.push(...allMarkdownPages(full, root)) + else if (entry.name.endsWith('.md')) out.push(relative(root, full).replace(/\.md$/, '')) + } + return out +} + +function stripFrontmatter(md: string): { body: string, title?: string } { + const m = /^---\n([\s\S]*?)\n---\n?/.exec(md) + if (!m) return { body: md } + const title = /(?:^|\n)title:\s*["']?([^"'\n]+)["']?/.exec(m[1])?.[1]?.trim() + return { body: md.slice(m[0].length), title } +} + +/** Generate /llms-full.txt into the build output — the whole site as one markdown file. */ +function buildLlmsFull(srcDir: string, outDir: string): void { + const discovered = allMarkdownPages(srcDir, srcDir) + const ordered = [ + ...LLMS_PAGE_ORDER.filter(p => discovered.includes(p)), + ...discovered.filter(p => !LLMS_PAGE_ORDER.includes(p)).toSorted(), + ] + const sections = ordered.map((page) => { + const raw = readFileSync(resolve(srcDir, `${page}.md`), 'utf-8') + const { body, title } = stripFrontmatter(raw) + const heading = title ?? /^#\s+(.+)$/m.exec(body)?.[1] ?? page + const url = page === 'index' ? 'https://ai.contentrain.io/' : `https://ai.contentrain.io/${page}` + return `# ${heading}\nSource: ${url}\n\n${body.trim()}\n` + }) + const header = '# Contentrain AI — Full Documentation\n\n' + + '> The complete ai.contentrain.io documentation as a single markdown file for LLM ingestion. ' + + 'Curated index: https://ai.contentrain.io/llms.txt\n\n' + writeFileSync(join(outDir, 'llms-full.txt'), header + sections.join('\n---\n\n')) +} + export default defineConfig({ + buildEnd(siteConfig) { + buildLlmsFull(siteConfig.srcDir, siteConfig.outDir) + }, + title: 'Contentrain AI', description: 'Extract, govern, and ship structured content from your codebase.', @@ -70,6 +144,7 @@ export default defineConfig({ text: 'Introduction', items: [ { text: 'What is Contentrain AI?', link: '/getting-started' }, + { text: '2-Minute Demo', link: '/demo' }, { text: 'Ecosystem Map', link: '/ecosystem' }, { text: 'Core Concepts', link: '/concepts' }, { text: 'Contentrain Studio', link: '/studio' }, diff --git a/docs/concepts.md b/docs/concepts.md index 137771b..dccc467 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -1,7 +1,7 @@ --- title: Core Concepts description: "Understand the architecture: models, content kinds, domains, locales, and the agent-driven workflow" -order: 2 +order: 4 category: getting-started slug: concepts --- @@ -49,7 +49,7 @@ Contentrain AI inverts the traditional CMS workflow: - **Project setup:** `contentrain_init`, `contentrain_scaffold` - **Content and schema writes:** `contentrain_model_save`, `contentrain_model_delete`, `contentrain_content_save`, `contentrain_content_delete` - **Normalize:** `contentrain_scan`, `contentrain_apply` -- **Workflow and operations:** `contentrain_validate`, `contentrain_submit`, `contentrain_merge`, `contentrain_bulk` +- **Workflow and operations:** `contentrain_validate`, `contentrain_submit`, `contentrain_merge`, `contentrain_branch_list`, `contentrain_branch_delete`, `contentrain_bulk` MCP is **deterministic infrastructure** — it doesn't make content decisions. The agent decides what to create; MCP executes it. @@ -207,7 +207,7 @@ Contentrain stores everything as plain JSON and Markdown files in your Git repo. Web projects deploy content with `git push` — no extra infrastructure needed. But non-web platforms (iOS, Android, React Native, Flutter, desktop apps, game engines, IoT) can't read from a git repo at runtime. -For these use cases, Contentrain publishes merged content to a CDN (`cdn.contentrain.io`), delivering the same structured JSON your SDK queries — just over HTTP instead of the filesystem. +For these use cases, [Contentrain Studio](/studio)'s managed CDN publishes merged content over HTTP (`GET /api/cdn/v1/{projectId}/{path}` on the Studio host), delivering the same structured JSON your SDK queries — just over HTTP instead of the filesystem. The CDN is a Studio delivery feature (Cloudflare R2-backed), not part of the MIT core; the SDK's CDN client points at it via `cdn.url` in `config.json`. ``` Git repo → merge → CDN publish → iOS/Android/Flutter fetch → typed response @@ -217,7 +217,7 @@ This means one content source powers your website, mobile app, and any other pla ## Contentrain Studio -[Contentrain Studio](/studio) is the open-core team operations panel for Git-native structured content. Teams can self-host the AGPL core or use a managed Pro/Enterprise offering on top of the same model. +[Contentrain Studio](/studio) is the open-core team operations panel for Git-native structured content. Teams can self-host the AGPL core or use the managed plans (Starter/Pro/Enterprise) on top of the same model. The local open-source stack gives you: diff --git a/docs/ecosystem.md b/docs/ecosystem.md index 3c794ad..f10cfa7 100644 --- a/docs/ecosystem.md +++ b/docs/ecosystem.md @@ -1,7 +1,7 @@ --- title: Ecosystem Map description: "How Contentrain AI packages and Contentrain Studio fit together across discovery, governance, collaboration, and delivery." -order: 2 +order: 3 category: getting-started slug: ecosystem --- diff --git a/docs/getting-started.md b/docs/getting-started.md index 000b9d2..2f1b1b6 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -282,5 +282,5 @@ Want to skip setup? Start from a production-ready template with content models, - [Framework Integration](/guides/frameworks) — Platform-specific setup patterns ::: info Contentrain Studio -[Contentrain Studio](/studio) is the open-core team operations surface for Git-native structured content. Teams can self-host the AGPL core or use a managed Pro/Enterprise offering when they want web-based collaboration, review, media, and CDN delivery on top of the same content model. +[Contentrain Studio](/studio) is the open-core team operations surface for Git-native structured content. Teams can self-host the AGPL core or use the managed plans (Starter/Pro/Enterprise) when they want web-based collaboration, review, media, and CDN delivery on top of the same content model. ::: diff --git a/docs/guides/embedding-mcp.md b/docs/guides/embedding-mcp.md index 5fd9e4a..4b5bc49 100644 --- a/docs/guides/embedding-mcp.md +++ b/docs/guides/embedding-mcp.md @@ -51,6 +51,8 @@ const transport = new StdioServerTransport() await server.connect(transport) ``` +`createServer` also accepts an options object — `createServer({ provider, projectRoot?, instructions? })` — where `instructions` overrides the MCP server instructions string advertised to clients. + This is what `contentrain serve --stdio` does internally. Every IDE that speaks MCP (Claude Code, Cursor, Windsurf) talks to this shape. ### 2. HTTP + LocalProvider (local CI, Studio-like hosting pointed at a working tree) @@ -63,6 +65,7 @@ const handle = await startHttpMcpServer({ port: 3333, host: '0.0.0.0', authToken: process.env.MCP_BEARER_TOKEN, + // path: '/mcp' — custom mount path, default '/mcp' }) // handle.url — "http://0.0.0.0:3333/mcp" @@ -153,7 +156,7 @@ const handle = await startHttpMcpServerWith({ For **multi-tenant** deployments where each request targets a different project, see the **per-request resolver** section below. -Swap in `createGitLabProvider({ auth, project })` for GitLab. Self-hosted GitLab instances pass `project.host`. +Swap in `createGitLabProvider({ auth, project })` for GitLab — `project.projectId` (numeric ID or `group/name` path) is required; self-hosted GitLab instances also pass `project.host`. Both remote providers accept an optional `contentRoot` on the repo/project ref for monorepos whose `.contentrain/` lives under a prefix. ### 3a. HTTP + per-request provider resolver (multi-tenant) @@ -193,8 +196,9 @@ import { buildContextChange } from '@contentrain/mcp/core/context' import { validateProject } from '@contentrain/mcp/core/validator' import { CONTENTRAIN_BRANCH } from '@contentrain/types' +// Throws on invalid input; soft signals come back as per-entry advisories const plan = await planContentSave(provider, { model, entries, config, vocabulary }) -if (plan.result.some(r => r.error)) throw new Error('plan invalid') +const advisories = plan.result.flatMap(r => r.advisories ?? []) const overlay = new OverlayReader(provider, plan.changes) const contextChange = await buildContextChange(overlay, { @@ -245,7 +249,7 @@ Tools whose requirements can never be met by the session's provider are not regi { "error": "contentrain_validate requires local filesystem access.", "capability_required": "localWorktree", - "hint": "This tool is unavailable when MCP is driven by a remote provider. Use a LocalProvider or the stdio transport." + "hint": "This tool is unavailable when MCP is driven by a remote provider (e.g. GitHubProvider). Use a LocalProvider or the stdio transport." } ``` @@ -366,7 +370,7 @@ No HTTP, no MCP JSON-RPC — just the core primitives. ## Troubleshooting -**`ERR_PACKAGE_PATH_NOT_EXPORTED`** — you're reaching into a subpath that isn't in `package.json#exports`. Known good subpaths: `/server`, `/server/http`, `/core/config`, `/core/context`, `/core/contracts`, `/core/model-manager`, `/core/content-manager`, `/core/validator`, `/core/ops`, `/core/overlay-reader`, `/core/scanner`, `/core/graph-builder`, `/core/apply-manager`, `/core/scan-config`, `/providers/local`, `/providers/github`, `/providers/gitlab`, `/util/detect`, `/util/fs`, `/git/transaction`, `/git/branch-lifecycle`, `/templates`. +**`ERR_PACKAGE_PATH_NOT_EXPORTED`** — you're reaching into a subpath that isn't in `package.json#exports`. Known good subpaths: `/server`, `/server/http`, `/core/config`, `/core/context`, `/core/contracts`, `/core/model-manager`, `/core/content-manager`, `/core/validator`, `/core/doctor`, `/core/ops`, `/core/overlay-reader`, `/core/scanner`, `/core/graph-builder`, `/core/apply-manager`, `/core/scan-config`, `/providers/local`, `/providers/github`, `/providers/gitlab`, `/tools/annotations`, `/tools/availability`, `/testing/conformance`, `/util/detect`, `/util/fs`, `/git/transaction`, `/git/branch-lifecycle`, `/templates`. **Stale context.json stats after a remote commit** — you forgot `OverlayReader`. `buildContextChange(provider, op)` reads the pre-change branch; wrap with `new OverlayReader(provider, plan.changes)`. diff --git a/docs/guides/http-transport.md b/docs/guides/http-transport.md index 6dfce39..28ed238 100644 --- a/docs/guides/http-transport.md +++ b/docs/guides/http-transport.md @@ -71,7 +71,7 @@ const handle = await startHttpMcpServerWith({ // handle.close() stops the server ``` -The same pattern works for `createGitLabProvider` with a `GitLabProvider`. Both require their respective optional peers (`@octokit/rest`, `@gitbeaker/rest`). +The same pattern works for `createGitLabProvider({ auth, project })` — `project.projectId` (numeric ID or `group/name` path) is required, and self-hosted instances pass `project.host`. Both providers require their respective optional peers (`@octokit/rest`, `@gitbeaker/rest`). Multi-tenant deployments (`{ resolveProvider }`) should also set `sessionFingerprint` to bind each MCP session to the tenant it was created for — follow-up requests whose fingerprint doesn't match the session's get `404` and the client re-initializes. See the [embedding guide](/guides/embedding-mcp#3a-http--per-request-provider-resolver-multi-tenant) for the full pattern. @@ -117,13 +117,13 @@ An agent running on a laptop can drive a Contentrain project that lives on a ser ## Capability gates over HTTP -Not every tool works on every provider. A tool driven by an HTTP client against a remote provider returns a structured capability error when the required capability is missing: +Not every tool works on every provider. Most gating happens at registration time: tools whose requirements a remote provider cannot meet (e.g. `contentrain_scan`, which needs `astScan` + a local project root) are filtered out of `tools/list` entirely. Two cases depend on the *input*, so they surface as structured call-time errors instead — `contentrain_validate` with `fix: true` and `contentrain_apply` in reuse mode: ```json { - "error": "contentrain_scan requires local filesystem access.", - "capability_required": "astScan", - "hint": "This tool is unavailable when MCP is driven by a remote provider. Use a LocalProvider or the stdio transport." + "error": "contentrain_validate requires local filesystem access.", + "capability_required": "localWorktree", + "hint": "This tool is unavailable when MCP is driven by a remote provider (e.g. GitHubProvider). Use a LocalProvider or the stdio transport." } ``` diff --git a/docs/guides/providers.md b/docs/guides/providers.md index 9f19653..0049260 100644 --- a/docs/guides/providers.md +++ b/docs/guides/providers.md @@ -51,7 +51,7 @@ Read-only tools (`status`, `describe`, `describe_format`, `content_list`, `valid - **`LocalProvider`** — Day-to-day development from an IDE. Offline-capable, zero API keys, full source-tree access for normalize. - **`GitHubProvider`** — CI-driven content operations, Studio's hosted agent, or any automation that should push directly to a GitHub repository without a clone. Requires `@octokit/rest` (optional peer dependency) and a personal access token or GitHub App installation. -- **`GitLabProvider`** — Same as above for GitLab (SaaS or self-hosted). Requires `@gitbeaker/rest` (optional peer dependency) and a PAT / OAuth / job token. +- **`GitLabProvider`** — Same as above for GitLab (SaaS or self-hosted). Requires `@gitbeaker/rest` (optional peer dependency) and an auth token: `{ type: 'pat', token }`, `{ type: 'oauth', oauthToken }`, or `{ type: 'job', jobToken }` (CI job token). The project ref needs `projectId` (numeric ID or `group/name` path); self-hosted instances add `host`. The choice is operational, not commercial. All three providers live in MIT; enterprise features are on top of Contentrain Studio, not behind provider gates. See [Ecosystem Map](/ecosystem) for the full package-to-product relationship. diff --git a/docs/guides/serve-ui.md b/docs/guides/serve-ui.md index 6467955..870b9f8 100644 --- a/docs/guides/serve-ui.md +++ b/docs/guides/serve-ui.md @@ -37,11 +37,11 @@ The server starts on `http://localhost:3333` by default. |---|---|---| | `--port` | `3333` | Port number | | `--host` | `localhost` | Host address | -| `--open=false` | `true` | Prevent auto-opening browser | +| `--open=false` | `true` | Prevent auto-opening browser (or set `CONTENTRAIN_NO_OPEN=true`) | | `--demo` | disabled | Temporary demo project (no setup required) | | `--stdio` | disabled | MCP stdio transport for IDE integration (no web UI) | -| `--mcpHttp` | disabled | MCP HTTP transport at `POST /mcp` (secure-by-default Bearer auth) | -| `--authToken` | — | Bearer token required on non-localhost HTTP binds | +| `--mcpHttp` | disabled | MCP HTTP transport at `POST /mcp` (Bearer auth enforced when a token is set) | +| `--authToken` | — | Bearer token; the web UI refuses to start on non-localhost binds without it | ### Check if already running @@ -213,7 +213,8 @@ The serve backend exposes REST endpoints at `http://localhost:3333/api/*`. Key r | `/api/preview/merge?branch=cr/...` | GET | Side-effect-free merge preview (FF / conflicts / already-merged) | | `/api/history` | GET | Recent contentrain-related commits | | `/api/context` | GET | `.contentrain/context.json` | -| `/api/normalize/*` | various | Scan / plan / approve / reject / apply / approve-branch | +| `/api/normalize/*` | various | Scan / results / plan / approve / reject / apply / approve-branch / sources / file-context | +| `/api/tree` | GET | Project source-file tree (`?ext=` filter) | Every write route validates its body through Zod schemas (`parseOrThrow`) before reaching the MCP tool layer. @@ -267,7 +268,9 @@ For Studio, CI, or remote agents, serve can expose MCP over Streamable HTTP: npx contentrain serve --mcpHttp --authToken $(openssl rand -hex 32) ``` -**Secure-by-default:** binding to a non-localhost address (`0.0.0.0` / specific IPs) without `--authToken` (or `CONTENTRAIN_AUTH_TOKEN` env var) is a hard error — the HTTP MCP endpoint exposes full project write access. +When a token is supplied, every request must send `Authorization: Bearer `. Binding to `0.0.0.0` without a token logs a warning but the server still starts — the endpoint exposes full project write access, so always pass a token on non-localhost binds. + +**The web UI is stricter:** the default (non-MCP) serve mode **hard-errors** on any non-localhost bind without `--authToken` / `CONTENTRAIN_AUTH_TOKEN` and refuses to start (OWASP Secure-by-Default). The UI has no per-request auth today; the token requirement is a start-up gate for exposing it beyond localhost. See the [HTTP Transport guide](/guides/http-transport) for deployment patterns. diff --git a/docs/index.md b/docs/index.md index b9bfdfb..e82be01 100644 --- a/docs/index.md +++ b/docs/index.md @@ -194,5 +194,5 @@ Agent decides what to extract → MCP validates and writes → Human reviews → No AI markup in your code. No proprietary syntax. No vendor lock-in. If you stop using Contentrain, your content files are still plain JSON in your Git repo. ::: info Contentrain Studio -[Contentrain Studio](/studio) is the open-core team web layer for Contentrain: chat-first operations, role-based collaboration, branch review, media management, and CDN delivery on top of the same Git-native content model. Teams can self-host the AGPL core or use a managed Pro/Enterprise offering. +[Contentrain Studio](/studio) is the open-core team web layer for Contentrain: chat-first operations, role-based collaboration, branch review, media management, and CDN delivery on top of the same Git-native content model. Teams can self-host the AGPL core or use the managed plans (Starter/Pro/Enterprise). ::: diff --git a/docs/packages/cli.md b/docs/packages/cli.md index ad9ec47..8e3e1cf 100644 --- a/docs/packages/cli.md +++ b/docs/packages/cli.md @@ -15,7 +15,7 @@ The `contentrain` CLI is the human-facing companion to `@contentrain/mcp`. While You might wonder: if the AI agent handles everything through MCP, why do you need a CLI? -- **Bootstrapping** — `contentrain init` sets up `.contentrain/`, git hooks, and IDE rules before the agent is even connected +- **Bootstrapping** — `contentrain init` sets up `.contentrain/`, runs `git init` with an initial commit, and installs IDE rules and MCP config before the agent is even connected - **Visibility** — `contentrain status` and `contentrain doctor` give you instant project health without asking an agent - **SDK generation** — `contentrain generate` creates the typed client your application code imports - **Review workflow** — `contentrain diff` and `contentrain serve` let you review agent-created content in a proper UI @@ -102,7 +102,7 @@ contentrain init --root /path # Different project root | `--yes` | Skip prompts, use defaults | | `--root ` | Project root path | -Creates `.contentrain/config.json`, `.contentrain/models/`, `.contentrain/content/`, plus IDE rules and Agent Skills. Runs `git init` if not already a repo. +Creates `.contentrain/config.json`, the `models/`, `content/`, and `meta/` directories, `vocabulary.json`, and `context.json`, appends `.contentrain/.cache/` and `.contentrain/client/` to `.gitignore`, installs IDE rules, Agent Skills, and the MCP config (`.mcp.json` or IDE equivalent). Runs `git init` plus an initial commit if not already a repo. --- @@ -153,7 +153,7 @@ contentrain validate --json # CI-friendly JSON |------|-------------| | `--fix` | Auto-fix and create a `cr/fix/*` review branch | | `--interactive` | Choose which issues to fix interactively | -| `--watch` | Re-run when `.contentrain/` changes (read-only polling mode) | +| `--watch` | Re-run when `.contentrain/` content, models, or `config.json` change (read-only) | | `--model ` | Validate one model instead of all | | `--json` | CI-friendly JSON | @@ -291,7 +291,7 @@ contentrain serve --port 8080 --host 0.0.0.0 contentrain serve --demo # Temporary demo project, no setup required ``` -Serves REST endpoints (`/api/status`, `/api/content`, `/api/branches`, `/api/doctor`, `/api/describe-format`, `/api/preview/merge`, `/api/normalize/*`, `/api/capabilities`, `/api/branches/:name/sync-status`), a WebSocket stream (`meta:changed`, `file-watch:error`, `sync:warning`, `branch:*` events), and the bundled Vue UI. +Serves REST endpoints (`/api/status`, `/api/content`, `/api/branches`, `/api/doctor`, `/api/describe-format`, `/api/preview/merge`, `/api/normalize/*`, `/api/capabilities`, `/api/branches/:name/sync-status`, `/api/tree`), a WebSocket stream (`meta:changed`, `file-watch:error`, `sync:warning`, `branch:*` events), and the bundled Vue UI. #### MCP stdio @@ -313,9 +313,11 @@ contentrain serve --mcpHttp --port 3334 --host 0.0.0.0 --authToken $TOKEN | `--mcpHttp` | `CONTENTRAIN_MCP_HTTP=true` | Enable HTTP MCP at `POST /mcp` | | `--port ` | `CONTENTRAIN_PORT` | Port (default 3333) | | `--host ` | `CONTENTRAIN_HOST` | Bind address (default `localhost`) | -| `--authToken ` | `CONTENTRAIN_AUTH_TOKEN` | Required Bearer token | +| `--authToken ` | `CONTENTRAIN_AUTH_TOKEN` | Bearer token, enforced per-request when set | -**Secure-by-default:** non-localhost binds (`0.0.0.0` or specific IPs) **hard-error** without `--authToken` / `CONTENTRAIN_AUTH_TOKEN`. This is deliberate (OWASP Secure-by-Default) — the HTTP MCP endpoint exposes full project write access. +**Auth:** when a token is supplied, every request must send `Authorization: Bearer `. Binding MCP HTTP to `0.0.0.0` without a token logs a warning but the server still starts — the endpoint exposes full project write access, so always pass a token on non-localhost binds. + +**Web UI is stricter:** the default serve UI **hard-errors** on any non-localhost bind without `--authToken` / `CONTENTRAIN_AUTH_TOKEN` (OWASP Secure-by-Default) — it refuses to start. Note the UI has no per-request auth today; the token requirement is a start-up gate for exposing it beyond localhost. ## Studio Integration @@ -374,7 +376,7 @@ contentrain studio submissions --form contact-form contentrain studio submissions --form contact-form --status pending ``` -Each Studio command accepts `--workspace `, `--project `, and `--json` for scripting. +The monitoring and management commands (`status`, `activity`, `usage`, `branches`, `webhooks`, `submissions`, `cdn-build`, `cdn-init`) accept `--workspace `, `--project `, and `--json` for scripting. The auth commands differ: `login` takes `--url`/`--provider`, `whoami` only `--json`, `logout` none, and `connect` takes `--workspace`/`--json`. --- diff --git a/docs/packages/mcp.md b/docs/packages/mcp.md index c375ea2..4c0438f 100644 --- a/docs/packages/mcp.md +++ b/docs/packages/mcp.md @@ -98,14 +98,14 @@ The MCP server exposes **24 tools** — 19 core + 5 media — organized by funct | `contentrain_describe` | Model deep-dive | Full schema, sample data, field types for any model | | `contentrain_describe_format` | Format reference | File structure, JSON formats, markdown conventions, locale strategies | | `contentrain_doctor` | Health diagnostics | Setup validation, SDK freshness, orphan content, local branch limits, remote `cr/*` count, unused keys, missing translations | -| `contentrain_content_list` | Read content | List and filter content entries with optional relation resolution | +| `contentrain_content_list` | Read content | List entries with `filter`, optional relation resolution (`resolve`), and `limit`/`offset` pagination | ### Write Tools (Git-Backed, Branch-Isolated) | Tool | Purpose | Description | |------|---------|-------------| | `contentrain_init` | Bootstrap project | Creates `.contentrain/` structure, config, and git setup | -| `contentrain_scaffold` | Apply templates | Blog, docs, landing page, or SaaS starter templates | +| `contentrain_scaffold` | Apply templates | Starter templates: `blog`, `landing`, `docs`, `ecommerce`, `saas`, `i18n`, `mobile` | | `contentrain_model_save` | Define schemas | Create or update model definitions with field types and constraints | | `contentrain_model_delete` | Remove models | Delete a model definition and its content | | `contentrain_content_save` | Write content | Save entries for any model kind (collection, singleton, dictionary, document) | @@ -126,6 +126,9 @@ The MCP server exposes **24 tools** — 19 core + 5 media — organized by funct ### Field constraints +Fields draw from a **27-type catalog** — see [Field Types](/reference/field-types) +for the full list and per-type constraint enforcement. + `contentrain_content_save` **validates before it writes**. A `severity: error` issue on any entry in the call means nothing is committed — no branch, nothing to clean up. Fix the values and call again. Warnings pass and come back in the response. @@ -171,6 +174,9 @@ delivery: a collection entry is served only when its status is `published`. - **`contentrain_validate` flags publish-state drift** — drafts sitting beside published entries in one collection — as a notice. It never auto-fixes it: publishing is a content decision, and MCP does not make those. +- **Scheduled publishing lives in meta too.** `contentrain_content_save` accepts + optional `publish_at` / `expire_at` (ISO 8601) per entry; `expire_at` must be + after `publish_at`. ::: tip Non-i18n models A model with `i18n: false` keeps all content in one `data.json`, so it has @@ -212,16 +218,18 @@ MCP is infrastructure, not intelligence. It does not decide what content to writ - **Atomic git transactions** — every write is committed to a branch - **Schema enforcement** — content is validated against model definitions -### 2. Dry-Run First +### 2. Preview Where It Counts -Every write operation supports `dry_run: true`. The pattern is always: +`dry_run` belongs to `contentrain_apply` — the one tool that can modify your source files. The normalize pattern is always: -1. Run with `dry_run: true` to preview changes +1. Run `contentrain_apply` with `dry_run: true` (the default) to preview the plan 2. Review the output 3. Run with `dry_run: false` to commit -::: warning Never Skip Preview -Always call write tools with `dry_run: true` first. This is not optional — it prevents accidental schema changes, content overwrites, and branch pollution. +The other write tools do not take a `dry_run` parameter. They are made safe by different guarantees: every write lands on an isolated `cr/*` branch, `contentrain_content_save` validates before committing (an error means nothing is written), and destructive tools (`content_delete`, `model_delete`, `branch_delete`, `merge`, `bulk delete_entries`) require an explicit `confirm: true`. + +::: warning Never Skip the Apply Preview +Always call `contentrain_apply` with `dry_run: true` first and review the plan. It is the only tool that can touch source code — skipping the preview risks unreviewed source patches. ::: ### 3. Git-Native Workflow @@ -308,7 +316,7 @@ Ask your agent: *"Is my Contentrain setup healthy?"* — triggers `contentrain_d ### Create a Model ```ts -// Agent calls contentrain_model_save with dry_run: true first +// Agent calls contentrain_model_save { "id": "blog-post", "name": "Blog Posts", @@ -318,7 +326,7 @@ Ask your agent: *"Is my Contentrain setup healthy?"* — triggers `contentrain_d "fields": { "title": { "type": "string", "required": true }, "excerpt": { "type": "text" }, - "author": { "type": "relation", "relation": "team-members" }, + "author": { "type": "relation", "model": "team-members" }, "published": { "type": "boolean" } } } @@ -470,10 +478,11 @@ claude mcp add --transport http contentrain \ | Trust Level | Tools | Risk | Notes | |-------------|-------|------|-------| -| **HIGH** (read-only) | `status`, `describe`, `describe_format`, `doctor`, `content_list` | None | Safe to call anytime, no side effects | -| **MEDIUM** (git-isolated writes) | `model_save`, `content_save`, `content_delete`, `model_delete`, `validate`, `scaffold`, `bulk` | Low | Changes isolated to `cr/*` branches, reviewable | -| **LOW** (source modification) | `scan`, `apply` | Medium | Normalize touches source files — always use dry_run first | -| **MEDIUM** (remote push) | `submit`, `merge` | Medium | Pushes branches to remote or merges — requires network access and review | +| **HIGH** (read-only) | `status`, `describe`, `describe_format`, `doctor`, `content_list`, `branch_list`, `scan`, `media_list`, `media_get` | None | Safe to call anytime, no side effects — `scan` reads source but never writes | +| **MEDIUM** (git-isolated writes) | `init`, `scaffold`, `model_save`, `model_delete`, `content_save`, `content_delete`, `validate`, `bulk` | Low | Writes go through git transactions on isolated `cr/*` branches; destructive tools require `confirm: true` | +| **MEDIUM** (branch lifecycle) | `submit`, `merge`, `branch_delete` | Medium | `submit` pushes to the remote (requires network); `merge` and `branch_delete` are local git operations — network is used only for best-effort remote-copy cleanup | +| **MEDIUM** (provider media) | `media_ingest`, `media_update`, `media_delete` | Medium | Provider-side media stack; `media_delete` is destructive and does not rewrite content references | +| **LOW** (source modification) | `apply` | Medium | `mode: "reuse"` patches source files — always run `dry_run: true` first | ::: danger Source Modifications The `contentrain_apply` tool with `mode: "reuse"` modifies your source code files. Always run with `dry_run: true` first, review the patches carefully, and use the review workflow before merging. @@ -507,6 +516,7 @@ await server.connect(transport) Available subpath exports: +- `@contentrain/mcp` (root) — stdio server entrypoint (what the `contentrain-mcp` bin runs) - `@contentrain/mcp/server` — MCP server factory and stdio setup - `@contentrain/mcp/server/http` — HTTP transport server factory - `@contentrain/mcp/core/config` — Config manager @@ -526,6 +536,8 @@ Available subpath exports: - `@contentrain/mcp/git/branch-lifecycle` — Branch health tracking - `@contentrain/mcp/templates` — Scaffold templates - `@contentrain/mcp/tools/annotations` — Tool metadata (TOOL_NAMES, TOOL_ANNOTATIONS) +- `@contentrain/mcp/tools/availability` — Per-tool capability requirements (TOOL_REQUIREMENTS) +- `@contentrain/mcp/testing/conformance` — Provider conformance test harness - `@contentrain/mcp/util/detect` — Framework detection - `@contentrain/mcp/util/fs` — File system utilities - `@contentrain/mcp/providers/local` — LocalProvider implementation diff --git a/docs/packages/rules.md b/docs/packages/rules.md index 17c3b77..9c5bd8e 100644 --- a/docs/packages/rules.md +++ b/docs/packages/rules.md @@ -187,7 +187,7 @@ Works with Claude Code, Cursor, Windsurf, GitHub Copilot, OpenAI Codex, Gemini C ## IDE Integration -`contentrain init` installs a compact essential guardrails file (~86 lines, always-loaded) plus Agent Skills directories (on-demand) for detected IDEs: +`contentrain init` installs a compact essential guardrails file (always-loaded) plus Agent Skills directories (on-demand) for detected IDEs: | IDE | Rules Dir | Skills Dir | Format | |-----|-----------|------------|--------| @@ -206,7 +206,7 @@ Here is a real example of the agent workflow when a user says "Extract the hardc ### 1. Essential Rules Load First -The agent's essential guardrails (~86 lines) are always loaded from `.claude/rules/contentrain-essentials.md` (or equivalent IDE path). These cover MCP tool catalog, mandatory protocols, and security basics. +The agent's essential guardrails are always loaded from `.claude/rules/contentrain-essentials.md` (or equivalent IDE path). These cover MCP tool catalog, mandatory protocols, and security basics. ### 2. Skill Guides the Workflow diff --git a/docs/packages/sdk.md b/docs/packages/sdk.md index 0e77199..9431a57 100644 --- a/docs/packages/sdk.md +++ b/docs/packages/sdk.md @@ -56,7 +56,9 @@ This writes: index.cjs — CJS entry (NestJS, Express, legacy tooling) index.d.ts — Generated TypeScript types from model schemas data/ - {model}.{locale}.mjs — Static data modules per model/locale + {model}.{locale}.mjs — Static data modules per model/locale + {model}--{slug}.{locale}.mjs — Document entries (one module per slug) + {model}.mjs — Non-i18n models (no locale suffix) ``` It also updates your `package.json` with subpath imports: @@ -69,6 +71,12 @@ It also updates your `package.json` with subpath imports: "import": "./.contentrain/client/index.mjs", "require": "./.contentrain/client/index.cjs", "default": "./.contentrain/client/index.mjs" + }, + "#contentrain/*": { + "types": "./.contentrain/client/*.d.ts", + "import": "./.contentrain/client/*.mjs", + "require": "./.contentrain/client/*.cjs", + "default": "./.contentrain/client/*.mjs" } } } @@ -234,6 +242,7 @@ const latest = document('blog-article') | `locale` | `locale(lang: string)` | `this` | Set the content locale | | `where` | `where(field, value)` | `this` | Equality filter (shorthand) | | `where` | `where(field, op, value)` | `this` | Operator filter (same operators as QueryBuilder) | +| `sort` | `sort(field, order?)` | `this` | Sort by frontmatter field (`asc` default) | | `include` | `include(...fields)` | `this` | Resolve relation fields | | `bySlug` | `bySlug(slug)` | `T \| undefined` | Find document by slug | | `count` | `count()` | `number` | Return count of matching documents | @@ -349,7 +358,8 @@ For all bundler setups, also add a `paths` entry to `tsconfig.json` so the TypeS { "compilerOptions": { "paths": { - "#contentrain": ["./.contentrain/client/index.d.ts"] + "#contentrain": ["./.contentrain/client/index.d.ts"], + "#contentrain/*": ["./.contentrain/client/*.d.ts"] } } } diff --git a/docs/packages/types.md b/docs/packages/types.md index 5132920..37c2692 100644 --- a/docs/packages/types.md +++ b/docs/packages/types.md @@ -92,7 +92,7 @@ const result: ValidationResult = { | `ContentStatus` | `draft`, `in_review`, `published`, `rejected`, `archived` | | | `ContentSource` | `agent`, `human`, `import` | | | `WorkflowMode` | `auto-merge`, `review` | [Configuration](/reference/config) | -| `StackType` | `nuxt`, `next`, `astro`, `sveltekit`, `remix`, + 20 more | [Configuration](/reference/config) | +| `StackType` | `nuxt`, `next`, `astro`, `sveltekit`, `remix`, + 25 more | [Configuration](/reference/config) | | `Platform` | `web`, `mobile`, `api`, `desktop`, `static`, `other` | | | `ContextSource` | `mcp-local`, `mcp-studio`, `studio-ui` | | | `CollectionRuntimeFormat` | `map`, `array` | | @@ -109,7 +109,7 @@ const result: ValidationResult = { | `Vocabulary` | Shared terms for content consistency | | `EntryMeta` | Per-entry metadata (status, source, timestamps) | | `AssetEntry` | Asset registry entry (path, type, size, alt) | -| `ValidationError` | Structured validation error with severity and context | +| `ValidationError` | Structured validation issue — `severity` is `error`, `warning`, or `notice` (notices flag drift like drafts beside published entries) | | `ValidationResult` | Validation outcome (valid flag + error list) | | `ContextJson` | Last operation context written by MCP | | `ModelSummary` | Lightweight model info for listing operations | @@ -146,7 +146,7 @@ Media facet types (implemented by providers exposing a media stack — drives th Pre-built capability set: -- `LOCAL_CAPABILITIES` — Full capability set for LocalProvider (all seven capabilities enabled). Exported from `@contentrain/types` for custom providers that back onto the local filesystem. +- `LOCAL_CAPABILITIES` — Capability set for LocalProvider: `localWorktree`, `sourceRead`, `sourceWrite`, `pushRemote`, and `astScan` enabled; `branchProtection` and `pullRequestFallback` are `false` (a local worktree has no remote protection or PR flow). Exported from `@contentrain/types` for custom providers that back onto the local filesystem. See [RepoProvider Reference](/reference/providers) for the complete interface definitions and a minimum-viable provider recipe. @@ -195,10 +195,14 @@ Used by the normalize flow (scan, extract, reuse): | `ScanSummaryResult` | High-level scan summary (directory breakdown, top repeated) | | `StringContext` | Where a string appears (`jsx_text`, `template_attribute`, ...) | | `FileCategory` | File classification (`page`, `component`, `layout`, `other`) | +| `NormalizePlan` | Normalize plan exchanged between scan and apply | +| `NormalizePlanModel` | Model proposal inside a normalize plan | +| `NormalizePlanExtraction` | One extraction target (content entry to create) | +| `NormalizePlanPatch` | One source patch inside a normalize plan | ### Runtime Constants -These are the only non-type exports — used at runtime for path resolution and validation: +Beyond types, the package ships a small runtime surface: constants plus pure, dependency-free validate/serialize functions (browser-compatible — Studio shares the same validation contract through them). Constants first: ```ts import { @@ -221,6 +225,34 @@ import { | `ENTRY_ID_PATTERN` | RegExp | Validates entry IDs | | `LOCALE_PATTERN` | RegExp | Validates ISO locale codes | | `CANONICAL_JSON` | Object | Deterministic serialization rules | +| `SECRET_PATTERNS` | `ReadonlyArray` | Patterns behind `detectSecrets` — extend for custom secret detection | + +### Runtime Functions + +Validate functions (pure, dependency-free): + +| Function | Purpose | +|----------|---------| +| `validateSlug(slug)` | Kebab-case slug validation | +| `validateEntryId(id)` | Entry ID format validation | +| `validateLocale(locale, config)` | Locale format + config support check | +| `detectSecrets(value)` | Detect potential secrets in field values | +| `validateFieldValue(value, fieldDef)` | Full field schema validation (type, required, min/max, pattern, select) | +| `validateSemanticType(value, type)` | Semantic checks for typed values (integer, date, email, url, ...) | +| `validateAccept(value, accept)` | Extension-based `accept` constraint check for media paths | +| `isMediaType(type)` | Whether a field type is media-backed | + +Serialize functions (pure, dependency-free): + +| Function | Purpose | +|----------|---------| +| `sortKeys(obj, fieldOrder?)` | Recursive key sorting for canonical output | +| `canonicalStringify(data, fieldOrder?)` | Deterministic JSON serialization | +| `generateEntryId()` | 12-char hex entry ID generation | +| `parseMarkdownFrontmatter(content)` | Parse YAML frontmatter + body from markdown | +| `serializeMarkdownFrontmatter(data, body)` | Serialize data + body into markdown frontmatter | + +Unique constraints and relation references need external state (all entries / target existence), so they stay in MCP's validator — `validateFieldValue` covers everything schema-level. ### Git Transaction Types diff --git a/docs/public/llms.txt b/docs/public/llms.txt new file mode 100644 index 0000000..2f8c996 --- /dev/null +++ b/docs/public/llms.txt @@ -0,0 +1,48 @@ +# Contentrain AI Documentation + +> Contentrain AI is the MIT-licensed open-source toolkit for AI-generated content governance: a 24-tool MCP server (@contentrain/mcp), a CLI (contentrain), shared TypeScript types (@contentrain/types), agent rules and skills (@contentrain/rules, @contentrain/skills), and a generated typed query SDK (@contentrain/query). Content lives as canonical JSON/Markdown in your Git repo under .contentrain/ — agents write through deterministic MCP tools, humans review through git branches. + +Full documentation as a single file (for LLMs): https://ai.contentrain.io/llms-full.txt +Contentrain Studio (team web surface) documentation: https://docs.contentrain.io/llms.txt + +## Packages (npm) + +- @contentrain/mcp — MCP server: 24 deterministic tools (19 core + 5 media), stdio + HTTP transports, Local / GitHub / GitLab providers +- contentrain — CLI: init, serve (review UI + MCP transports), validate, generate, diff, merge, prune, studio commands +- @contentrain/query — universal query SDK; `contentrain generate` emits a typed client into .contentrain/client/ (#contentrain imports); optional CDN transport +- @contentrain/types — shared contract: model/field/config/provider types plus pure, browser-safe validate/serialize functions +- @contentrain/rules — always-loaded agent guardrails plus shared quality rules +- @contentrain/skills — 15 on-demand Agent Skills (Agent Skills standard) + +## Key Facts + +- 4 model kinds: collection (object-map by entry id), singleton, dictionary (flat string map), document (markdown + frontmatter) +- 27 field types; content is validated before every write — an error on any entry in a contentrain_content_save call means nothing is committed +- Every write lands on an isolated cr/* git branch; workflow is auto-merge or review (config.json) +- dry_run preview belongs to contentrain_apply (the normalize tool that can patch source); destructive tools require confirm: true +- Locale strategies: file / suffix / directory / none; entry status and scheduling live in .contentrain/meta/, not in content +- MIT licensed, local-first (no cloud dependency); GitHub/GitLab remote providers are optional peer dependencies + +## Key Pages + +- Getting Started: /getting-started +- 2-Minute Demo: /demo +- Ecosystem Map: /ecosystem +- Core Concepts: /concepts +- Contentrain Studio: /studio +- MCP Tools: /packages/mcp +- CLI: /packages/cli +- Query SDK: /packages/sdk +- Rules & Skills: /packages/rules +- Types: /packages/types +- Providers & Transports: /guides/providers +- HTTP Transport: /guides/http-transport +- Embedding MCP: /guides/embedding-mcp +- Normalize Flow: /guides/normalize +- Framework Integration: /guides/frameworks +- i18n Workflow: /guides/i18n +- Serve UI: /guides/serve-ui +- Model Kinds: /reference/model-kinds +- Field Types: /reference/field-types +- Configuration: /reference/config +- RepoProvider Reference: /reference/providers diff --git a/docs/public/og-image.png b/docs/public/og-image.png new file mode 100644 index 0000000..d7f04e5 Binary files /dev/null and b/docs/public/og-image.png differ diff --git a/docs/reference/config.md b/docs/reference/config.md index 2c3d270..ea11c81 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -119,7 +119,7 @@ interface ContentrainConfig { | Field | Type | Required | Description | |---|---|---|---| | `version` | `number` | Yes | Config schema version. Currently `1`. | -| `platform` | `Platform` | No | Target platform: `web`, `mobile`, `api`, `desktop`, `static`, `other` | +| `platform` | `Platform` | No | Target platform: `web`, `mobile`, `api`, `desktop`, `static`, `other`. Informational — MCP's config reader strips it; only the raw file (read by `generate` and tooling) retains it. | | `stack` | `StackType` | Yes | Framework identifier. See [Supported Stacks](#supported-stacks) below. | | `workflow` | `WorkflowMode` | Yes | `auto-merge` or `review`. See [Workflow Modes](#workflow-modes). | | `repository` | `object` | No | GitHub repository connection details. | @@ -127,7 +127,7 @@ interface ContentrainConfig { | `locales.supported` | `string[]` | Yes | All supported locale codes. Must include `default`. | | `domains` | `string[]` | Yes | Content domain names for organizing models. | | `assets_path` | `string` | No | Directory for media assets relative to project root. | -| `cdn.url` | `string` | No | Public media delivery base. When set, `contentrain generate` bakes a `media()` resolver into the client that turns relative `media/...` paths into absolute `{cdn.url}/{path}` URLs. | +| `cdn.url` | `string` | No | Public media delivery base. When set, `contentrain generate` bakes a `media()` resolver into the client that turns relative `media/...` paths into absolute `{cdn.url}/{path}` URLs. Like `platform`, it is not surfaced through MCP's config reader — only `generate` and tooling read it from the raw file. | | `branchRetention` | `number` | No | Days a merged `cr/*` branch is kept locally before lazy cleanup. Default: `30`. | | `branchWarnLimit` | `number` | No | Unmerged `cr/*` branch count that triggers a warning. Default: `50`. | | `branchBlockLimit` | `number` | No | Unmerged `cr/*` branch count that blocks new writes. Default: `80`. | @@ -312,7 +312,7 @@ interface EntryMeta { ### Content Status Lifecycle -| Status | Description | Transitions To | +| Status | Description | Typically Moves To | |---|---|---| | `draft` | Initial state, work in progress | `in_review`, `published` | | `in_review` | Submitted for review | `published`, `rejected` | @@ -320,6 +320,10 @@ interface EntryMeta { | `rejected` | Review rejected | `draft` | | `archived` | No longer active | `draft` | +::: info Conventions, not a state machine +The transitions above are the intended editorial flow — MCP does not enforce them. `contentrain_bulk update_status` can set any status directly; the lifecycle discipline lives in your workflow (and Studio's review UI), not in a validator gate. +::: + ### Content Sources | Source | Description | @@ -440,6 +444,8 @@ All content files follow predictable path conventions. These are exported as `PA ::: warning Document meta files include the `{slug}` segment in the path, unlike other kinds. This is because each document is a separate entity with its own lifecycle. + +On `i18n: false` models the `{locale}` in the meta path is pinned to the **default locale** — one `data.json` (or `{slug}.md`) means exactly one meta record, at `.contentrain/meta/{modelId}/{default-locale}.json`. ::: ### Other Paths @@ -472,14 +478,13 @@ All JSON files written by Contentrain follow strict serialization rules for dete | **Encoding** | UTF-8 | Universal character support | | **Trailing newline** | Yes (single `\n`) | POSIX compliance, clean git diffs | | **Null values** | Omitted entirely | Token efficiency — don't write what doesn't exist | -| **Default values** | Omitted entirely | Token efficiency — don't write what's implied | | **No trailing commas** | Standard JSON | Compatibility | ### Why Canonical Serialization Matters - **Collection entries** are sorted by entry ID → adding entries in parallel produces conflict-free merges -- **Entry fields** follow the schema definition order → consistent, predictable diffs -- **Omitting null/default** saves tokens when AI agents read content → lower cost, faster processing +- **Entry fields** are sorted alphabetically; only the model definition file keeps a curated key order (`id`, `name`, `kind`, `domain`, `i18n`, `description`, `content_path`, `locale_strategy`, `fields`) → consistent, predictable diffs +- **Omitting null/undefined** saves tokens when AI agents read content → lower cost, faster processing (values equal to a field's declared `default` are still written — the serializer does not compare against schema defaults) - **Deterministic output** means running the same operation twice produces identical files — no noise in git history All MCP tools, the CLI, and the SDK follow these rules. The `contentrain_validate` tool can detect and auto-fix serialization violations with `fix: true`. diff --git a/docs/reference/field-types.md b/docs/reference/field-types.md index 6f52215..ad64316 100644 --- a/docs/reference/field-types.md +++ b/docs/reference/field-types.md @@ -58,7 +58,7 @@ Field types for textual content of varying lengths and formats. | `url` | Web URL | Auto-validated format | `string` | | `slug` | URL-friendly identifier | Lowercase, hyphens, no spaces | `string` | | `code` | Source code or preformatted text | `min`, `max` (length) | `string` | -| `icon` | Icon identifier (e.g., icon library name) | `pattern`, `options` | `string` | +| `icon` | Icon identifier (e.g., icon library name) | `pattern` | `string` | #### Examples @@ -92,7 +92,7 @@ Field types for textual content of varying lengths and formats. }, "category_icon": { "type": "icon", - "options": ["home", "settings", "user", "star"] + "pattern": "^[a-z0-9-]+$" } } ``` @@ -386,7 +386,7 @@ Field types for structured and compound data. ``` ::: info -The `array` type can hold simple values (`items: "string"`) or complex objects (`items: { type: "object", fields: {...} }`). The `object` type always requires a `fields` definition. +The `array` type can hold simple values (`items: "string"`) or complex objects (`items: { type: "object", fields: {...} }`). Declare `fields` on an `object` to type its sub-fields — `fields` on any other type is rejected by `model_save`. Nesting is capped at 10 levels. ::: ## Complete Type Summary Table @@ -394,51 +394,66 @@ The `array` type can hold simple values (`items: "string"`) or complex objects ( | # | Type | Group | JS Storage | Supports `min`/`max` | Supports `pattern` | Supports `options` | Supports `model` | Supports `items`/`fields` | |---|---|---|---|---|---|---|---|---| | 1 | `string` | Text | `string` | Length | Yes | No | No | No | -| 2 | `text` | Text | `string` | Length | No | No | No | No | -| 3 | `email` | Text | `string` | No | Auto | No | No | No | -| 4 | `url` | Text | `string` | No | Auto | No | No | No | +| 2 | `text` | Text | `string` | Length | Yes | No | No | No | +| 3 | `email` | Text | `string` | Length | Auto | No | No | No | +| 4 | `url` | Text | `string` | Length | Auto | No | No | No | | 5 | `slug` | Text | `string` | Length | Auto | No | No | No | -| 6 | `code` | Text | `string` | Length | No | No | No | No | -| 7 | `icon` | Text | `string` | No | Yes | Yes | No | No | -| 8 | `markdown` | Rich Content | `string` | Length | No | No | No | No | -| 9 | `richtext` | Rich Content | `string` | Length | No | No | No | No | +| 6 | `code` | Text | `string` | Length | Yes | No | No | No | +| 7 | `icon` | Text | `string` | Length | Yes | No | No | No | +| 8 | `markdown` | Rich Content | `string` | Length | Yes | No | No | No | +| 9 | `richtext` | Rich Content | `string` | Length | Yes | No | No | No | | 10 | `number` | Numeric | `number` | Value | No | No | No | No | | 11 | `integer` | Numeric | `number` | Value | No | No | No | No | | 12 | `decimal` | Numeric | `number` | Value | No | No | No | No | | 13 | `percent` | Numeric | `number` | Value | No | No | No | No | | 14 | `rating` | Numeric | `number` | Value | No | No | No | No | | 15 | `boolean` | Boolean | `boolean` | No | No | No | No | No | -| 16 | `date` | Date/Time | `string` | Date range | No | No | No | No | -| 17 | `datetime` | Date/Time | `string` | Datetime range | No | No | No | No | +| 16 | `date` | Date/Time | `string` | Length | Yes | No | No | No | +| 17 | `datetime` | Date/Time | `string` | Length | Yes | No | No | No | | 18 | `image` | Media | `string` | No | No | No | No | No | | 19 | `video` | Media | `string` | No | No | No | No | No | | 20 | `file` | Media | `string` | No | No | No | No | No | -| 21 | `color` | Color | `string` | No | Yes | No | No | No | -| 22 | `phone` | Phone | `string` | No | Yes | No | No | No | +| 21 | `color` | Color | `string` | Length | Yes | No | No | No | +| 22 | `phone` | Phone | `string` | Length | Yes | No | No | No | | 23 | `relation` | Relation | `string` | No | No | No | Yes | No | -| 24 | `relations` | Relation | `string[]` | No | No | No | Yes | No | +| 24 | `relations` | Relation | `string[]` | Count | No | No | Yes | No | | 25 | `select` | Complex | `string` | No | No | Yes (required) | No | No | | 26 | `array` | Complex | `array` | Count | No | No | No | `items` | | 27 | `object` | Complex | `object` | No | No | No | No | `fields` | +::: info How to read `min`/`max` and `pattern` +Both are enforced generically at validation time: `min`/`max` compare **string length**, **numeric value**, or **array count** depending on the stored value, and `pattern` runs against any string value. The table marks the types where they are meaningful — a `pattern` on an `image` path is technically enforced too, it is just rarely what you want. There is no date-range semantics: on `date`/`datetime` fields `min`/`max` compare string length, not chronology. +::: + ## Constraint Properties Reference | Property | Applicable Types | Description | |---|---|---| | `required` | All | Field must have a non-null value | -| `unique` | All (collection only) | Value must be unique across all entries | -| `default` | All | Default value when field is not provided | -| `min` | string, text, code, slug, markdown, richtext, number, integer, decimal, percent, rating, array | Minimum length (strings) or value (numbers) or count (arrays) | -| `max` | string, text, code, slug, markdown, richtext, number, integer, decimal, percent, rating, array | Maximum length (strings) or value (numbers) or count (arrays) | -| `pattern` | string, icon, color, phone | Regular expression for validation | -| `options` | select, icon | Array of allowed string values | -| `model` | relation, relations | Target model ID(s) for the relation | -| `items` | array | Item type: a type name string or a nested `FieldDef` | +| `unique` | All (collection and document) | Value must be unique across all entries — rejected on singleton models (`model_save` errors), N/A for dictionary (no fields) | +| `default` | All | Default value declared on the schema (must be coherent with the type/options) | +| `min` | All string types, numeric types, `array`, `relations` | Minimum length (strings), value (numbers), or count (arrays/relations) | +| `max` | All string types, numeric types, `array`, `relations` | Maximum length (strings), value (numbers), or count (arrays/relations) | +| `pattern` | All string types (primary use: string, icon, color, phone) | Regular expression, enforced on every string value; must compile or `model_save` rejects it | +| `options` | select only | Array of allowed string values — declaring it on any other type is a `model_save` error | +| `model` | relation, relations | Target model ID(s) for the relation — required for these types | +| `items` | array | Item type: a type name string or a nested `FieldDef` — items are validated by the same rules as a scalar of that type | +| `accept` | image, video, file | Allowed formats — checked against the **file extension only**, as a warning that never blocks; unknown extensions stay silent | +| `maxSize` | image, video, file | Maximum file size in bytes — **accepted but never enforced by MCP** (it stores a path, not the file); your media provider enforces it at ingest, and `model_save` returns a `schema_warnings` entry saying so | | `fields` | object | Sub-field definitions as `Record` | -| `accept` | image, video, file | Comma-separated MIME types | -| `maxSize` | image, video, file | Maximum file size in bytes | | `description` | All | Human-readable description of the field | +## How Constraints Are Enforced + +Two distinct gates apply, at different moments: + +**At `model_save` (authoring time).** The schema itself is validated and a misplaced constraint is a blocking error, not a silent no-op: `options` on a non-select, `items` on a non-array, `fields` on a non-object, `accept`/`maxSize` on a non-media field, `unique` on a singleton, `min > max`, an uncompilable `pattern`, a `select` without non-empty `options`, a `relation`/`relations` without `model`, a `default` incoherent with the type. Model IDs must be kebab-case, field names snake_case (`^[a-z][a-z0-9_]*$`), and unknown keys are rejected — a typo'd `requird: true` is an error. + +**At `contentrain_content_save` (write time).** Values are validated *before* anything is committed — a `severity: error` on any entry in the call means nothing is written. Severities split by intent: + +- **Errors** (block the write): wrong type, `required` missing, `min`/`max` violations, `pattern` mismatch, `options` membership, and definitional semantic checks — an invalid `slug`, an unparseable `date`/`datetime`, a non-integer `integer`, an out-of-range `percent`. +- **Warnings** (pass through, reported in the response): heuristic semantic checks — `email`, `url`, `color`, `phone` formats — and the extension-based `accept` check. `rating` has no range enforcement by design. + ::: info Shared Across Contentrain AI and Studio -These 27 field types define content schemas used identically in both local workflows (`@contentrain/mcp`, CLI) and [Contentrain Studio](/studio). Content modeled locally works seamlessly in team workflows without changes. +These 27 field types define content schemas used identically in both local workflows (`@contentrain/mcp`, CLI) and [Contentrain Studio](/studio). The pure validators live in `@contentrain/types` (`validateFieldValue`, `validateSemanticType`), so Studio forms and MCP enforce the same contract. Content modeled locally works seamlessly in team workflows without changes. ::: diff --git a/docs/reference/model-kinds.md b/docs/reference/model-kinds.md index 16357a8..baa1c60 100644 --- a/docs/reference/model-kinds.md +++ b/docs/reference/model-kinds.md @@ -52,7 +52,7 @@ Collections store **multiple typed entries** as a JSON object-map keyed by auto- ### Key Characteristics - **Object-map storage** — entries keyed by ID, not an array -- **Auto-generated hex IDs** — alphanumeric, 1-40 characters, hyphens/underscores allowed +- **Auto-generated hex IDs** — 12 lowercase hex characters when generated; hand-written IDs may be any 1-40 alphanumeric characters with hyphens/underscores - **Typed fields** — each field has a defined `FieldType` with validation - **Sorted keys** — canonical serialization sorts entry IDs alphabetically - **Per-locale files** — each locale gets its own JSON file @@ -369,10 +369,10 @@ The `frontmatter` object contains all fields defined in the model schema. The `b | i18n: true | `.contentrain/content/{domain}/{slug}/{locale}.md` | | i18n: false | `.contentrain/content/{domain}/{slug}.md` | | Meta (i18n: true) | `.contentrain/meta/{model-id}/{slug}/{locale}.json` | -| Meta (i18n: false) | `.contentrain/meta/{model-id}/{slug}.json` | +| Meta (i18n: false) | `.contentrain/meta/{model-id}/{slug}/{default-locale}.json` | ::: warning -Document meta files have a **different path structure** than other kinds. They include the `{slug}` segment: `.contentrain/meta/{model-id}/{slug}/{locale}.json`. Other kinds use `.contentrain/meta/{model-id}/{locale}.json` directly. +Document meta files have a **different path structure** than other kinds. They include the `{slug}` segment: `.contentrain/meta/{model-id}/{slug}/{locale}.json`. Other kinds use `.contentrain/meta/{model-id}/{locale}.json` directly. On `i18n: false` models (any kind) the single meta record is pinned to the **default locale** — the filename is still `{default-locale}.json`, never `data.json` or a flat `{slug}.json`. ::: ### MCP content_save Call @@ -480,7 +480,6 @@ Every model has corresponding meta files in `.contentrain/meta/{model-id}/` that "status": "published", "source": "agent", "updated_by": "contentrain-mcp", - "approved_by": null, "version": "1.0.0", "publish_at": "2026-01-15T00:00:00Z", "expire_at": "2026-12-31T23:59:59Z" @@ -583,7 +582,7 @@ A model can reference itself for tree structures: |---|---| | **Referential integrity** | Referenced ID/slug must exist in the target model | | **Locale-agnostic** | IDs and slugs are the same across all locales | -| **Cascade warning** | Deleting a referenced entry triggers a validation warning | +| **No delete-time guard** | Deleting a referenced *entry* is not blocked and emits nothing at delete time; the broken reference surfaces as an **error** on the next `contentrain_validate`. (Deleting a whole *model* that others reference is blocked.) | | **Array ordering** | `relations` array order is preserved | | **Min/max** | `relations` supports `min` and `max` element count | diff --git a/docs/studio.md b/docs/studio.md index a1c7220..115336e 100644 --- a/docs/studio.md +++ b/docs/studio.md @@ -1,7 +1,7 @@ --- title: Contentrain Studio description: "Open-core team operations and delivery for the same Git-native content contract used by Contentrain AI" -order: 3 +order: 5 category: getting-started slug: studio --- @@ -14,7 +14,7 @@ In short: > Contentrain AI gives developers local-first packages and agent workflows. Studio gives teams an authenticated web surface for review, collaboration, media, APIs, and CDN delivery on top of the same `.contentrain/` contract. -Teams can self-host the AGPL core or use a managed Pro/Enterprise offering, depending on how much infrastructure and operations they want to own. +Teams can self-host the AGPL core or use the managed plans (Starter/Pro/Enterprise), depending on how much infrastructure and operations they want to own. It is not a separate content format and it is not a different storage engine. Studio sits on top of the same Contentrain model: