agent-cms 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Jokull Solberg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PROMPT.md ADDED
@@ -0,0 +1,55 @@
1
+ You are an agent-cms setup guide. Your job is to help the user add a headless CMS to their project backed by Cloudflare D1 and R2.
2
+
3
+ Reference the agent-cms repo for details as you work:
4
+ - `README.md` — feature overview, bindings, field types, interfaces, examples
5
+ - `examples/blog/` — complete CMS + Astro site with structured text, responsive images, service bindings
6
+ - `examples/editor-mcp/` — editor onboarding with OAuth gateway and scoped tokens
7
+
8
+ Clone or fetch the repo if you need specifics: `https://github.com/jokull/agent-cms`
9
+
10
+ ## Before Starting
11
+
12
+ 1. Assess the repository:
13
+ `ls -la wrangler.jsonc wrangler.json wrangler.toml package.json tsconfig.json bun.lock pnpm-lock.yaml package-lock.json 2>/dev/null`
14
+ Determine package manager from lock file. If multiple, ask.
15
+ 2. Determine integration mode. Ask the user:
16
+ - **Standalone Worker** — separate CMS Worker, site fetches via HTTP or service binding. Best when the CMS and site are separate projects.
17
+ - **Service binding** — separate CMS Worker, site calls `env.CMS.fetch()` with zero latency. Best for Cloudflare-to-Cloudflare.
18
+ - **Mounted** — mount agent-cms at `/cms` inside an existing Worker. Best when you want one Worker and one D1 database for everything.
19
+
20
+ ## Standalone Worker
21
+
22
+ 1. Run `pnpm create agent-cms <name>` (or npm/bun equivalent)
23
+ 2. `cd <name> && pnpm install && pnpm dev`
24
+ 3. `pnpm run setup -- http://127.0.0.1:8787`
25
+ 4. Register the MCP server:
26
+ `claude mcp add --transport http <name> http://127.0.0.1:8787/mcp`
27
+ 5. If the user wants a service binding from their site Worker, add to the site's `wrangler.jsonc`:
28
+ ```jsonc
29
+ { "services": [{ "binding": "CMS", "service": "<name>" }] }
30
+ ```
31
+ Then fetch from the site: `env.CMS.fetch(new Request("http://cms/graphql", { method: "POST", body: ... }))`
32
+
33
+ ## Mounted in Existing Worker
34
+
35
+ 1. Install: `pnpm add agent-cms`
36
+ 2. Check the user's existing `wrangler.jsonc` for D1 and R2 bindings. If D1 exists, ask whether to reuse it or create a new one for the CMS. If R2 exists, same question.
37
+ 3. Create the handler in the user's existing Worker entry point:
38
+ ```ts
39
+ import { createCMSHandler } from "agent-cms";
40
+ ```
41
+ Mount it at `/cms` using whatever router the project uses (Hono, itty-router, or raw URL matching).
42
+ 4. Run setup: `curl -X POST http://localhost:8787/cms/api/setup`
43
+ 5. Register MCP: `claude mcp add --transport http cms http://127.0.0.1:8787/cms/mcp`
44
+
45
+ ## After Setup
46
+
47
+ 1. Confirm the MCP server is responding: call `schema_info`
48
+ 2. Ask the user what content they need — blog posts, products, pages, events — and create the schema conversationally
49
+ 3. If the user has an existing site, help them query content via GraphQL. Check if they use a typed GraphQL client (gql.tada, graphql-codegen) and offer to introspect the schema for types.
50
+
51
+ ## What NOT to Do
52
+
53
+ - Do not create content models without asking the user what they need
54
+ - Do not deploy to production without explicit confirmation
55
+ - Do not touch existing wrangler bindings without asking
package/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # agent-cms
2
+
3
+ Agent-first headless CMS. Runs as a Cloudflare Worker backed by D1 and R2 in your own account. No hosted service, no admin UI. Agents define schemas, manage content, and publish — via MCP.
4
+
5
+ ## What you get
6
+
7
+ - **Structured text with typed blocks** — a document tree where rich components (code blocks, media, custom types) are embedded inline. One GraphQL query returns the full tree with discriminated block unions. Map directly to React/Svelte/Vue components in a single server hop. Render with [`react-datocms`](https://github.com/datocms/react-datocms), `vue-datocms`, or `datocms-svelte` — the structured text format is [DAST](https://www.datocms.com/docs/structured-text/dast), an open standard.
8
+ - **Hybrid search** — FTS5 for keyword matching, Cloudflare Vectorize for semantic similarity, combined with reciprocal rank fusion. All on D1.
9
+ - **Draft/publish with scheduling** — records start as drafts. Publishing captures a version snapshot. Schedule publish/unpublish at future datetimes. Full version history — restore to any snapshot, and the restore itself is reversible.
10
+ - **Geospatial filtering** — `lat_lon` field type with `near(latitude, longitude, radius)` queries in GraphQL.
11
+ - **Automatic reverse references** — link model A to model B, and B gets a query field for all records in A that reference it, with full filtering, ordering, and pagination.
12
+ - **Two MCP servers** — admin MCP (`/mcp`) for schema and content, editor MCP (`/mcp/editor`) scoped to content operations only. Create editor tokens with optional expiry.
13
+ - **Multi-locale with fallback chains** — per-field opt-in. Locale A falls back to B falls back to C. The GraphQL resolver walks the chain.
14
+ - **24 field types** — string, text, boolean, integer, float, date, date_time, slug (auto-generated), media (with focal point + blurhash), media_gallery, link, links, structured_text, seo (title + description + image + twitter card), json, color (RGBA), lat_lon, video. All validated with Effect schemas.
15
+ - **Tree hierarchies and sortable collections** — parent-child nesting and explicit position ordering as first-class model properties.
16
+ - **Dynamic SQL builder** — the query engine builds SQL at runtime from the content schema. No ORM, no generated client. The content schema is decoupled from your application schema — run this on the same D1 database as your site.
17
+ - **Responsive images** — Cloudflare Image Resizing with focal points, blurhash for progressive loading, color palette extraction. R2 storage, no external service.
18
+ - **Bulk operations** — create up to 1000 records in a single call.
19
+ - **Schema portability** — export the full schema as JSON (no IDs, just api_keys), import it on a fresh instance.
20
+ - **Three interfaces** — REST API, GraphQL, and MCP, all auto-generated from the content schema.
21
+ - **Effect-TS throughout** — typed errors, dependency injection via services and layers, no try/catch. The whole CMS is a single Worker.
22
+
23
+ ## Quick start
24
+
25
+ Copy the prompt from [`PROMPT.md`](./PROMPT.md) into Claude Code. It assesses your project, asks how you want to integrate (standalone Worker, service binding, or mounted in an existing Worker), and wires everything up — including D1 database, wrangler config, and MCP server connection.
26
+
27
+ ## Interfaces
28
+
29
+ ### `/mcp` — Admin agent interface
30
+
31
+ MCP server with tools for schema management, content operations (CRUD, bulk insert, publish/unpublish, reorder), asset management, search, and schema import/export. Requires `writeKey`.
32
+
33
+ ### `/mcp/editor` — Editorial agent interface
34
+
35
+ Reduced MCP server for content-authoring agents. Accepts either an editor token or `writeKey`. Exposes schema introspection, record CRUD, drafts, publish/unpublish, version restore, assets, site settings, and search. Does not expose schema mutation, token management, or admin operations.
36
+
37
+ ### `/graphql` — Content delivery
38
+
39
+ Read-only GraphQL API. Supports filtering, ordering, pagination, locale fallback, and draft previews via `X-Include-Drafts`.
40
+
41
+ ```graphql
42
+ {
43
+ all_posts(
44
+ filter: { _status: { eq: "published" } }
45
+ order_by: [_created_at_DESC]
46
+ first: 10
47
+ ) {
48
+ id
49
+ title
50
+ slug
51
+ cover_image {
52
+ url
53
+ width
54
+ height
55
+ alt
56
+ }
57
+ body {
58
+ value
59
+ blocks {
60
+ ... on CodeBlockRecord {
61
+ id
62
+ code
63
+ language
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ #### Naming conventions
72
+
73
+ Model `api_key` values (snake_case) map to GraphQL names:
74
+
75
+ | api_key | GraphQL type | Single query | List query | Meta query |
76
+ |---------|-------------|-------------|------------|------------|
77
+ | `blog_post` | `BlogPost` | `blog_post` | `all_blog_posts` | `_all_blog_posts_meta` |
78
+ | `category` | `Category` | `category` | `all_categories` | `_all_categories_meta` |
79
+
80
+ Block types get a `Record` suffix: `code_block` → `CodeBlockRecord`.
81
+
82
+ Field `api_key` values stay snake_case in queries: `cover_image`, `published_at`.
83
+
84
+ #### Performance model
85
+
86
+ GraphQL nesting is not compiled into one giant SQL join. The server fetches root records, batches linked records and StructuredText work into set-oriented SQL, then assembles the nested shape in memory. See [`PERFORMANCE.md`](./PERFORMANCE.md).
87
+
88
+ #### MCP resources and prompts
89
+
90
+ Agents connecting via MCP get two resources:
91
+ - **`agent-cms://guide`** — workflow order, naming conventions, field value formats
92
+ - **`agent-cms://schema`** — current schema as JSON
93
+
94
+ Two prompts for common workflows:
95
+ - **`setup-content-model`** — design and create content models from a description
96
+ - **`generate-graphql-queries`** — generate typed GraphQL queries for a model
97
+
98
+ ### `/api` — REST
99
+
100
+ JSON REST API for programmatic access. Models, fields, records, assets, locales, publish/unpublish, scheduling, bulk operations, schema import/export.
101
+
102
+ ### `/api/search` — Search
103
+
104
+ FTS5 keyword search with BM25 ranking and snippets, scoped to all models or a single model. When `AI` + `VECTORIZE` bindings are configured:
105
+
106
+ - **`keyword`** — FTS5. Phrases (`"exact match"`), prefix (`word*`), boolean (`AND`/`OR`).
107
+ - **`semantic`** — Vectorize cosine similarity.
108
+ - **`hybrid`** (default) — Reciprocal rank fusion of keyword + semantic results.
109
+
110
+ ## Editor tokens
111
+
112
+ Editor tokens are the credential for non-admin editing flows. Create them via `POST /api/tokens` or the `editor_tokens` MCP tool (with `action: "create"`). The raw token is shown once; the server stores a hash.
113
+
114
+ Editor tokens can access `/mcp/editor`, REST content/asset operations, and draft GraphQL previews. They cannot mutate schema, manage tokens, or run admin operations.
115
+
116
+ For editor onboarding with OAuth, the package exports `createCmsAdminClient` and `createEditorMcpProxy` to stand up an app-land MCP gateway. See [`examples/editor-mcp/`](./examples/editor-mcp/).
117
+
118
+ ## Scheduling
119
+
120
+ Schedule publish/unpublish at future datetimes via REST or MCP. To execute schedules automatically, add a cron trigger:
121
+
122
+ ```ts
123
+ import { createCMSHandler } from "agent-cms";
124
+
125
+ let cachedHandler: ReturnType<typeof createCMSHandler> | null = null;
126
+
127
+ function getHandler(env: Env) {
128
+ if (!cachedHandler) {
129
+ cachedHandler = createCMSHandler({
130
+ bindings: { db: env.DB, assets: env.ASSETS, writeKey: env.CMS_WRITE_KEY },
131
+ });
132
+ }
133
+ return cachedHandler;
134
+ }
135
+
136
+ export default {
137
+ fetch(request: Request, env: Env) {
138
+ return getHandler(env).fetch(request);
139
+ },
140
+ scheduled(_controller: ScheduledController, env: Env) {
141
+ return getHandler(env).runScheduledTransitions();
142
+ },
143
+ };
144
+ ```
145
+
146
+ ```json
147
+ { "triggers": { "crons": ["* * * * *"] } }
148
+ ```
149
+
150
+ Without a cron trigger, schedules are stored and queryable but do not execute.
151
+
152
+ ## Lifecycle hooks
153
+
154
+ React to content events with hooks passed to `createCMSHandler`:
155
+
156
+ ```ts
157
+ createCMSHandler({
158
+ bindings: { db: env.DB, assets: env.ASSETS, writeKey: env.CMS_WRITE_KEY },
159
+ hooks: {
160
+ onPublish: ({ modelApiKey, recordId }) => fetch(env.DEPLOY_HOOK_URL, { method: "POST" }),
161
+ onRecordCreate: ({ modelApiKey, recordId }) => { /* notify, sync, etc. */ },
162
+ },
163
+ });
164
+ ```
165
+
166
+ Available: `onRecordCreate`, `onRecordUpdate`, `onRecordDelete`, `onPublish`, `onUnpublish`. All receive `{ modelApiKey, recordId }`. Fire-and-forget.
167
+
168
+ ## Bindings
169
+
170
+ Only `DB` is required. Everything else is optional and degrades gracefully.
171
+
172
+ | Binding | Type | What it enables |
173
+ |---------|------|-----------------|
174
+ | `DB` | D1 | **Required.** Content storage, schema, FTS5 search. |
175
+ | `ASSETS` | R2 | Asset file storage and serving via `/assets/`. |
176
+ | `AI` | Workers AI | Embedding generation for semantic search. |
177
+ | `VECTORIZE` | Vectorize | Semantic vector search. Requires `AI`. |
178
+ | `CMS_WRITE_KEY` | Secret | Auth for writes, MCP, and publish. Without it, writes are open. |
179
+ | `ASSET_BASE_URL` | Variable | Public URL prefix for assets and Image Resizing. Must be a custom domain for transforms. |
180
+
181
+ ```jsonc
182
+ {
183
+ "d1_databases": [{ "binding": "DB", "database_name": "my-cms-db", "database_id": "..." }],
184
+ "r2_buckets": [{ "binding": "ASSETS", "bucket_name": "my-cms-assets" }],
185
+ "vectorize": [{ "binding": "VECTORIZE", "index_name": "my-cms-content" }],
186
+ "ai": { "binding": "AI" },
187
+ "vars": { "ASSET_BASE_URL": "https://cms.example.com" }
188
+ }
189
+ ```
190
+
191
+ To create the Vectorize index: `npx wrangler vectorize create my-cms-content --dimensions=384 --metric=cosine`
192
+
193
+ ## Assets
194
+
195
+ Asset binaries live in R2. Metadata in D1. Served from `/assets/:id/:filename`.
196
+
197
+ - **MCP/editor**: `import_asset_from_url` — download, store, register in one step
198
+ - **Browser**: `PUT /api/assets/:id/file` then register metadata
199
+ - **Server**: upload to R2, then `POST /api/assets`
200
+
201
+ Focal points, blurhash, and color palette are stored per-asset. Cloudflare Image Resizing generates responsive variants at the edge.
202
+
203
+ ## Stack
204
+
205
+ - **Runtime**: Cloudflare Workers
206
+ - **Database**: D1 (managed SQLite)
207
+ - **Assets**: R2 + Cloudflare Image Resizing
208
+ - **Search**: SQLite FTS5 + Cloudflare Vectorize
209
+ - **Application**: [Effect](https://effect.website)
210
+ - **GraphQL**: [graphql-yoga](https://the-guild.dev/graphql/yoga-server) with generated SDL
211
+ - **Testing**: [Vitest](https://vitest.dev) (`pnpm test`)
212
+
213
+ ## Examples
214
+
215
+ - [`examples/blog/`](./examples/blog/) — CMS Worker + Astro SSR site with typed GraphQL (gql.tada), structured text rendering, responsive images, service bindings
216
+ - [`examples/editor-mcp/`](./examples/editor-mcp/) — editor onboarding: app-land OAuth gateway, scoped editor tokens, separate MCP URLs for developers and editors
217
+
218
+ ## License
219
+
220
+ MIT