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 +21 -0
- package/PROMPT.md +55 -0
- package/README.md +220 -0
- package/dist/handler-ClOW1ldA.mjs +5703 -0
- package/dist/http-transport-DbFCI6Cs.mjs +993 -0
- package/dist/index.d.mts +213 -0
- package/dist/index.mjs +1170 -0
- package/dist/structured-text-service-B4xSlUg_.mjs +1952 -0
- package/dist/token-service-BDjccMmz.mjs +3820 -0
- package/migrations/0000_genesis.sql +118 -0
- package/package.json +98 -0
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
|