@contentrain/skills 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # `@contentrain/skills`
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/%40contentrain%2Fskills?label=%40contentrain%2Fskills)](https://www.npmjs.com/package/@contentrain/skills)
4
- [![Agent Skills](https://img.shields.io/badge/Agent_Skills-15_skills-8B5CF6)](https://agentskills.io)
4
+ [![Agent Skills](https://img.shields.io/badge/Agent_Skills-16_skills-8B5CF6)](https://agentskills.io)
5
5
  [![GitHub source](https://img.shields.io/badge/source-Contentrain%2Fai-181717?logo=github)](https://github.com/Contentrain/ai/tree/main/packages/skills)
6
6
  [![Docs](https://img.shields.io/badge/docs-ai.contentrain.io-0f172a)](https://ai.contentrain.io/packages/skills)
7
7
 
@@ -45,7 +45,7 @@ Works with Claude Code, Cursor, Windsurf, GitHub Copilot, OpenAI Codex, Gemini C
45
45
 
46
46
  ### Agent Skills (standard format)
47
47
 
48
- Published under `skills/` — 15 production skills:
48
+ Published under `skills/` — 16 production skills:
49
49
 
50
50
  | Skill | Description |
51
51
  |-------|-------------|
@@ -128,7 +128,7 @@ This reduces always-loaded context from thousands of lines to just the essential
128
128
 
129
129
  `@contentrain/skills` is kept in lockstep with the MCP tool registry via cross-package parity tests (`tests/mcp-parity.test.ts`):
130
130
 
131
- - `skills/contentrain/references/mcp-tools.md` must have an `### <tool>` heading for every tool in the MCP `TOOL_NAMES` registry (currently 26: 21 core + 5 media).
131
+ - `skills/contentrain/references/mcp-tools.md` must have an `### <tool>` heading for every tool in the MCP `TOOL_NAMES` registry (currently 27: 22 core + 5 media).
132
132
  - Key skills (normalize, translate) must not reference legacy `contentrain/{operation}/...` branch prefixes — MCP now emits `cr/*`.
133
133
 
134
134
  When MCP's surface changes, these tests fail until the skill docs catch up.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/skills",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "license": "MIT",
5
5
  "description": "AI agent skills for Contentrain — workflow procedures, framework integration guides",
6
6
  "type": "module",
@@ -61,9 +61,9 @@
61
61
  "tsdown": "^0.21.0",
62
62
  "typescript": "^5.7.0",
63
63
  "vitest": "^3.0.0",
64
- "@contentrain/mcp": "3.3.3",
65
- "@contentrain/rules": "0.7.2",
66
- "@contentrain/wp-import": "0.4.1"
64
+ "@contentrain/mcp": "3.9.0",
65
+ "@contentrain/rules": "0.7.3",
66
+ "@contentrain/wp-import": "0.9.4"
67
67
  },
68
68
  "scripts": {
69
69
  "build": "tsdown src/index.ts --format esm,cjs --dts",
@@ -12,16 +12,19 @@ Contentrain is a Git-based, AI-first content management system. Content lives in
12
12
 
13
13
  ## Ecosystem Overview
14
14
 
15
- Contentrain consists of 6 packages that work together:
15
+ Contentrain consists of 9 packages that work together:
16
16
 
17
17
  | Package | Role | How agent uses it |
18
18
  |---|---|---|
19
- | @contentrain/mcp | 22 MCP tools (scan, apply, validate, merge, reconcile, doctor...) | MCP tool calls |
20
- | contentrain (CLI) | init, serve, generate, doctor, diff, status | Shell commands |
19
+ | @contentrain/mcp | 27 MCP tools (scan, apply, validate, merge, reconcile, doctor...) | MCP tool calls |
20
+ | contentrain (CLI) | init, serve, generate, doctor, diff, status, import | Shell commands |
21
21
  | @contentrain/types | Shared TypeScript contracts | Type safety |
22
22
  | @contentrain/query | Generated SDK client (Prisma-pattern) | `import from '#contentrain'` |
23
- | @contentrain/rules | Behavioral guardrails (~86 lines, always-loaded) | Auto-loaded by IDE |
23
+ | @contentrain/rules | Behavioral guardrails (~145 lines, always-loaded) | Auto-loaded by IDE |
24
24
  | @contentrain/skills | Procedural workflows (this file) | On-demand by agent |
25
+ | @contentrain/wp-import | WordPress WXR/REST → `RawIR` → a `.contentrain` store | `contentrain import wordpress` |
26
+ | @contentrain/emitter-astro | `ProjectIR` → a complete Astro project | Called by the migration engine |
27
+ | @contentrain/verify | Offline SEO, indexing, link and asset gates for a built site | `npx @contentrain/verify dist` |
25
28
 
26
29
  ### CLI Serve — Review & Approval Bridge
27
30
 
@@ -196,8 +196,9 @@ Several fields are managed by the platform. Agents MUST NOT set or modify them.
196
196
  | `i18n: false` on model | Single file: `data.json` (or `{slug}.md` for documents) |
197
197
  | Locale code | ISO 639-1: `en`, `tr`, `de`, `fr`, `ja`, `ar` |
198
198
  | ID/slug are locale-agnostic | Same entry ID or document slug across all locales |
199
- | Entry parity (collections) | All locales MUST have the same set of entry IDs |
200
- | Key parity (dictionaries) | All locales MUST have the same set of keys |
199
+ | Entry parity (collections) | All covered locales MUST have the same set of entry IDs |
200
+ | Key parity (dictionaries) | All covered locales MUST have the same set of keys |
201
+ | `locales` on model (optional) | The project locales this model covers — a subset of `locales.supported`. Absent means all of them. Parity is checked against it |
201
202
 
202
203
  ### Validation Checks
203
204
 
@@ -15,7 +15,8 @@ Every rule applies whenever the project has more than one locale configured.
15
15
  ### 1.1 Coverage Requirement
16
16
 
17
17
  - Read `.contentrain/config.json` → `locales.supported` to determine the full list of supported locales.
18
- - EVERY entry that exists in the source locale MUST have a corresponding entry in ALL supported target locales.
18
+ - A model MAY narrow that list with an optional `locales` array (a subset of `locales.supported`). When present, it — not the project list — is the model's coverage. Absent means every supported locale.
19
+ - EVERY entry that exists in the source locale MUST have a corresponding entry in ALL of the model's covered target locales.
19
20
  - Missing translations are deployment blockers. Treat a missing locale file as an error, not a warning.
20
21
 
21
22
  ### 1.2 Dictionary Entries
@@ -88,6 +88,7 @@ Create or update a model definition (upsert).
88
88
  | `kind` | string | Yes | `singleton`, `collection`, `document`, `dictionary` |
89
89
  | `domain` | string | Yes | Organizational group |
90
90
  | `i18n` | boolean | Yes | Whether model supports localization |
91
+ | `locales` | string[] | No | The project locales this model covers — a subset of `config.locales.supported`. Absent means all of them, which is what parity is validated against. Declare it only for a genuinely partial model (a partially-translated site) |
91
92
  | `title_field` | string | Yes | Field shown as an entry's title. Must name a field on this model typed `string`, `text`, `slug`, `email`, `url`, `code`, `markdown` or `richtext`. Dictionaries have no fields — use `"key"` |
92
93
  | `fields` | object | No | Field definitions (not for dictionary) |
93
94
  | `description` | string | No | Model description |
@@ -97,8 +97,8 @@ In v1, media fields store URL/path strings only. Upload and processing are out o
97
97
 
98
98
  | Type | Cardinality | Storage |
99
99
  |------|-------------|---------|
100
- | `relation` | One-to-one | `"entry-id"` (string) |
101
- | `relations` | One-to-many | `["id-1", "id-2"]` (string array) |
100
+ | `relation` | One-to-one | `"entry-id"` (string); `{ "model": "page", "ref": "entry-id" }` when `model` lists several targets |
101
+ | `relations` | One-to-many | `["id-1", "id-2"]` (string array); `[{ "model", "ref" }, …]` when `model` lists several targets |
102
102
 
103
103
  ### Relation Examples
104
104
 
@@ -14,7 +14,7 @@ Follow these guidelines when populating image, video, or file fields in any cont
14
14
 
15
15
  Media references are stored in one of two models. Identify which one the project uses before populating image, video, or file fields. The asset rules below (dimensions, formats, size limits, naming) apply to **both**; only the **Asset Organization** section (paths under `assets_path`, relative references) is specific to the local-file model.
16
16
 
17
- 1. **Local file model (default / OSS).** Media lives on disk under `config.json > assets_path` (default `.contentrain/assets/`) and is referenced by a **relative path from the project root** (e.g. `.contentrain/assets/blog/hero.webp`). To turn relative paths into URLs at read time, set `config.json > cdn.url` (or `contentrain generate --cdnBaseUrl <base>`) and use the `media()` resolver baked into the `@contentrain/query` generated client — `media('media/...') → {cdn.url}/{path}`.
17
+ 1. **Local file model (default / OSS).** Media lives on disk under `config.json > assets_path` (default `.contentrain/assets/`) and is referenced by a **relative path from the project root** (e.g. `.contentrain/assets/blog/hero.webp`). To turn relative paths into URLs at read time, set `config.json > cdn.url` (or `contentrain generate --mediaBaseUrl <base>`) and use the `media()`/`mediaBody()` resolvers baked into the `@contentrain/query` generated client — `media('media/...') → {mediaBaseUrl}/{path}`, `mediaBody(markdown)` for `](media/...)` references inside markdown. The Astro loader (`contentrainLoader({ mediaBaseUrl })`) applies the same resolver automatically to media fields and markdown bodies.
18
18
 
19
19
  2. **Studio CDN model.** Media lives in the Studio CDN/object store and is referenced by a relative storage path (`media/...`) returned by the media library. Pass either that `media/...` path or its URL to `contentrain_content_save`: in **cloud mode** the value is **automatically normalized to an absolute public delivery URL** on save (`{base}/{path}`), in both media fields and markdown bodies, so the committed content renders in a browser anywhere with no SDK. In **local mode** the relative path is kept verbatim. The rewrite is idempotent and never touches external URLs (`http(s)://`, `//`, `data:`) or already-absolute delivery URLs — so for external images (a CDN or Unsplash URL), just save the URL directly.
20
20
 
@@ -83,16 +83,19 @@ const doc = document('blog-post').bySlug('getting-started')
83
83
  Media / image / file field values are plain strings. How you turn them into a URL depends on the storage model:
84
84
 
85
85
  - **Studio-CDN content** already carries absolute delivery URLs — the write path normalizes `media/...` references on save — so use the field value directly, no resolution needed.
86
- - **Relative-path content** (OSS / local-file model, values like `media/...`) resolves through an optional `media()` helper baked into the generated client. Set `config.json > cdn.url` (or run `contentrain generate --cdnBaseUrl <base>`) and the client exports it:
86
+ - **Relative-path content** (OSS / local-file model, values like `media/...`) resolves through optional `media()`/`mediaBody()` helpers baked into the generated client. Set `config.json > cdn.url` (or run `contentrain generate --mediaBaseUrl <base>`) and the client exports them:
87
87
 
88
88
  ```typescript
89
- import { media } from '#contentrain'
89
+ import { media, mediaBody } from '#contentrain'
90
90
 
91
- media('media/original/hero.webp') // → '{cdn.url}/media/original/hero.webp'
91
+ media('media/original/hero.webp') // → '{mediaBaseUrl}/media/original/hero.webp'
92
92
  media('https://images.unsplash.com/x.jpg') // → unchanged (external pass-through)
93
+ mediaBody('See ![cover](media/hero.webp)') // → resolves `](media/...)` refs inside markdown
93
94
  ```
94
95
 
95
- `media()` is omitted when no base is configured, and is idempotent — external URLs (`http(s)://`, `//`, `data:`) and already-absolute delivery URLs pass through untouched. It is the **local-mode** counterpart of CDN mode's `client.media()` (which returns a `MediaAccessor` over the media manifest, below).
96
+ Both take an optional second argument overriding the baked base for that call — a Nuxt app passes its own `useRuntimeConfig()` value here instead of rebaking per environment. The Astro loader (`contentrainLoader({ mediaBaseUrl })`) applies the same resolver automatically.
97
+
98
+ `media()`/`mediaBody()` do not exist on the client at all when no base is configured at generate time — not a pass-through, an absent export. A host that never bakes a base in (reading it purely from its own runtime config or env, e.g. Nuxt) must import `resolveMediaUrl(value, base)` / `resolveMediaRefsInBody(markdown, base)` from `@contentrain/query` directly instead of going through the generated client. Both take a nullable base and no-op when it is absent, so calling them unconditionally is safe. `media()`, when present, is idempotent — external URLs (`http(s)://`, `//`, `data:`) and already-absolute delivery URLs pass through untouched. It is the **local-mode** counterpart of CDN mode's `client.media()` (which returns a `MediaAccessor` over the media manifest, below).
96
99
 
97
100
  ## CDN Mode (Remote Data)
98
101
 
@@ -189,7 +192,7 @@ const history = await conv.history(response.conversationId)
189
192
  - Locale fallback chain: explicit → config default → first available
190
193
  - Generated files in `.contentrain/client/` are **immutable** — always regenerate, never edit
191
194
  - Run `contentrain generate` after any model change
192
- - **Media**: Studio-CDN fields already carry absolute URLs; for relative `media/...` values set `config.cdn.url` (or `generate --cdnBaseUrl`) and resolve with the generated `media()` helper
195
+ - **Media**: Studio-CDN fields already carry absolute URLs; for relative `media/...` values set `config.cdn.url` (or `generate --mediaBaseUrl`) and resolve with the generated `media()`/`mediaBody()` helpers
193
196
 
194
197
  ## Framework Integration
195
198
 
@@ -120,7 +120,9 @@ Example:
120
120
  listings, pickers and relation references, and must be a field on this model
121
121
  typed `string`, `text`, `slug`, `email`, `url`, `code`, `markdown` or
122
122
  `richtext`. Dictionary models have no fields, so they use the reserved value
123
- `"key"` — the entry key is the title.
123
+ `"key"` — the entry key is the title. A singleton made of section objects
124
+ (`hero`, `services`) names a section's heading with a dotted path,
125
+ `"hero.heading"` — one level into an `object` field, never an array.
124
126
 
125
127
  Choose what a reader uses to tell one entry from another: the headline, the
126
128
  name, the question. Not the slug, not the icon, not a relation ID. Consumers