contentbase 0.0.1 → 0.0.4
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/.mcp.json +9 -0
- package/CLI.md +593 -0
- package/MCP-DESCRIPTIONS-REVIEW.md +122 -0
- package/MCP-SERVER-SPEC.md +453 -0
- package/PRIMER.md +540 -0
- package/README.md +406 -13
- package/bun.lock +45 -6
- package/dist/cnotes +0 -0
- package/docs/README.md +110 -0
- package/docs/TABLE-OF-CONTENTS.md +7 -0
- package/docs/models.ts +38 -0
- package/models.ts +38 -0
- package/package.json +14 -4
- package/public/web-demo/index.html +813 -0
- package/scripts/examples/01-collection-setup.ts +46 -0
- package/scripts/examples/02-querying.ts +67 -0
- package/scripts/examples/03-sections.ts +36 -0
- package/scripts/examples/04-relationships.ts +54 -0
- package/scripts/examples/05-document-api.ts +54 -0
- package/scripts/examples/06-extract-sections.ts +55 -0
- package/scripts/examples/07-validation.ts +46 -0
- package/scripts/examples/08-serialization.ts +51 -0
- package/scripts/examples/lib/format.ts +87 -0
- package/scripts/examples/lib/setup.ts +21 -0
- package/scripts/examples/run-all.ts +43 -0
- package/showcases/node_modules/.cache/.repl_history +3 -0
- package/showcases/vinyl-collection/artists/nina-simone.mdx +1 -1
- package/src/__tests__/semantic-search.integration.test.ts +284 -0
- package/src/api/endpoints/actions.ts +34 -0
- package/src/api/endpoints/doc.ts +187 -0
- package/src/api/endpoints/docs-index.ts +26 -0
- package/src/api/endpoints/document.ts +171 -0
- package/src/api/endpoints/documents.ts +71 -0
- package/src/api/endpoints/inspect.ts +16 -0
- package/src/api/endpoints/models.ts +10 -0
- package/src/api/endpoints/query.ts +88 -0
- package/src/api/endpoints/root.ts +7 -0
- package/src/api/endpoints/search-reindex.ts +65 -0
- package/src/api/endpoints/search-status.ts +55 -0
- package/src/api/endpoints/search.ts +104 -0
- package/src/api/endpoints/semantic-search.ts +120 -0
- package/src/api/endpoints/text-search.ts +63 -0
- package/src/api/endpoints/validate.ts +34 -0
- package/src/api/helpers.ts +97 -0
- package/src/base-model.ts +12 -0
- package/src/cli/commands/action.ts +82 -44
- package/src/cli/commands/console.ts +124 -0
- package/src/cli/commands/create.ts +179 -53
- package/src/cli/commands/embed.ts +323 -0
- package/src/cli/commands/export.ts +58 -24
- package/src/cli/commands/extract.ts +174 -0
- package/src/cli/commands/help.ts +81 -0
- package/src/cli/commands/index.ts +17 -0
- package/src/cli/commands/init.ts +72 -48
- package/src/cli/commands/inspect.ts +56 -46
- package/src/cli/commands/mcp.ts +1219 -0
- package/src/cli/commands/search.ts +285 -0
- package/src/cli/commands/serve.ts +348 -0
- package/src/cli/commands/summary.ts +60 -0
- package/src/cli/commands/teach.ts +86 -0
- package/src/cli/commands/text-search.ts +134 -0
- package/src/cli/commands/validate.ts +126 -64
- package/src/cli/index.ts +88 -19
- package/src/cli/load-collection.ts +144 -17
- package/src/cli/registry.ts +28 -0
- package/src/collection.ts +455 -6
- package/src/define-model.ts +104 -7
- package/src/document.ts +47 -1
- package/src/extract-sections.ts +222 -0
- package/src/index.ts +20 -2
- package/src/model-instance.ts +35 -5
- package/src/node-shortcuts.ts +1 -1
- package/src/query/collection-query.ts +96 -9
- package/src/query/index.ts +7 -0
- package/src/query/query-dsl.ts +259 -0
- package/src/relationships/has-many.ts +39 -0
- package/src/relationships/index.ts +7 -2
- package/src/section.ts +2 -0
- package/src/types.ts +24 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/match-pattern.ts +65 -0
- package/src/validator.ts +18 -1
- package/test/collection.test.ts +118 -2
- package/test/extract-sections.test.ts +356 -0
- package/test/fixtures/sdlc/MODELS.md +106 -0
- package/test/fixtures/sdlc/SKILL.md +404 -0
- package/test/fixtures/sdlc/index.ts +9 -0
- package/test/fixtures/sdlc/models.ts +8 -6
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +20 -0
- package/test/fixtures/sdlc/templates/epic.md +23 -0
- package/test/fixtures/sdlc/templates/story.md +19 -0
- package/test/pattern.test.ts +191 -0
- package/test/query-dsl.test.ts +431 -0
- package/test/query.test.ts +29 -0
- package/test/relationships.test.ts +130 -0
- package/test/section.test.ts +65 -4
- package/test/table-of-contents.test.ts +135 -0
package/README.md
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
Contentbase treats a folder of Markdown and MDX files as a typed, queryable database. Define models with Zod schemas, extract structured data from headings and lists, traverse parent/child relationships across documents, validate everything, and query it all with a fluent API.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
-
import { Collection, defineModel, section, hasMany, z } from "contentbase";
|
|
9
|
-
import { toString } from "mdast-util-to-string";
|
|
8
|
+
import { Collection, defineModel, section, hasMany, z, toString } from "contentbase";
|
|
10
9
|
|
|
11
10
|
const Story = defineModel("Story", {
|
|
12
11
|
meta: z.object({
|
|
@@ -79,14 +78,18 @@ content/
|
|
|
79
78
|
|
|
80
79
|
A model is a config object that describes one type of document. It declares:
|
|
81
80
|
|
|
81
|
+
- **description** -- human-readable summary (auto-generated from schema if omitted)
|
|
82
82
|
- **meta** -- a Zod schema for frontmatter
|
|
83
83
|
- **sections** -- named extractions from heading-based sections
|
|
84
84
|
- **relationships** -- `hasMany` / `belongsTo` links between models
|
|
85
85
|
- **computed** -- derived values calculated from instance data
|
|
86
|
+
- **defaults** -- static default values for frontmatter fields
|
|
87
|
+
- **pattern** -- Express-style path patterns for inferring meta from file paths
|
|
86
88
|
|
|
87
89
|
```ts
|
|
88
90
|
const Epic = defineModel("Epic", {
|
|
89
91
|
prefix: "epics",
|
|
92
|
+
description: "A project epic that groups related user stories.",
|
|
90
93
|
meta: z.object({
|
|
91
94
|
priority: z.enum(["low", "medium", "high"]).optional(),
|
|
92
95
|
status: z.enum(["created", "in-progress", "complete"]).default("created"),
|
|
@@ -103,14 +106,71 @@ const Epic = defineModel("Epic", {
|
|
|
103
106
|
});
|
|
104
107
|
```
|
|
105
108
|
|
|
109
|
+
If `description` is omitted, one is generated on first access from the model's schema: *"An Epic has metadata (priority, status), relationship (stories → Story), and computed property (isComplete)."*
|
|
110
|
+
|
|
106
111
|
The `prefix` determines which files match this model. Files whose path starts with `"epics"` are Epics. If omitted, the prefix is auto-pluralized from the name (`"Epic"` -> `"epics"`).
|
|
107
112
|
|
|
113
|
+
#### Explicit Model Assignment with `_model`
|
|
114
|
+
|
|
115
|
+
Documents at the root of a collection (not in a subfolder) can't be matched by prefix. You can explicitly assign a model by adding `_model` to the frontmatter:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
---
|
|
119
|
+
_model: Epic
|
|
120
|
+
title: Platform Migration
|
|
121
|
+
status: created
|
|
122
|
+
---
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The `_model` key takes priority over prefix matching, so this works even if the file lives outside the model's prefix folder.
|
|
126
|
+
|
|
127
|
+
#### The Base Model
|
|
128
|
+
|
|
129
|
+
Every collection automatically registers a built-in `Base` model as a catch-all. Documents that don't match any other model (by `_model` or prefix) are assigned to Base. You can query these unmatched documents:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { Base } from "contentbase";
|
|
133
|
+
|
|
134
|
+
const misc = await collection.query(Base).fetchAll();
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
If you want to customize the Base model (e.g. add a meta schema), register your own before calling `load()`:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const MyBase = defineModel("Base", {
|
|
141
|
+
meta: z.object({ tags: z.array(z.string()).optional() }),
|
|
142
|
+
});
|
|
143
|
+
collection.register(MyBase);
|
|
144
|
+
await collection.load(); // won't auto-register the built-in Base
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Path Patterns
|
|
148
|
+
|
|
149
|
+
Models can declare Express-style path patterns to automatically infer meta values from the document's file path:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const Story = defineModel("Story", {
|
|
153
|
+
prefix: "stories",
|
|
154
|
+
pattern: "stories/:epic/:slug",
|
|
155
|
+
meta: z.object({
|
|
156
|
+
epic: z.string(),
|
|
157
|
+
slug: z.string(),
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
A file at `stories/authentication/user-can-register.mdx` will automatically have `{ epic: "authentication", slug: "user-can-register" }` inferred into its meta. Explicit frontmatter values always take precedence over pattern-inferred values. You can also supply an array of patterns -- the first match wins.
|
|
163
|
+
|
|
108
164
|
### Collections
|
|
109
165
|
|
|
110
166
|
A `Collection` loads a directory tree and gives you access to documents and typed model instances.
|
|
111
167
|
|
|
112
168
|
```ts
|
|
113
|
-
const collection = new Collection({
|
|
169
|
+
const collection = new Collection({
|
|
170
|
+
rootPath: "./content",
|
|
171
|
+
extensions: ["mdx", "md"], // default
|
|
172
|
+
autoDiscover: true, // auto-load models.ts if no models registered
|
|
173
|
+
});
|
|
114
174
|
await collection.load();
|
|
115
175
|
|
|
116
176
|
// Register models for prefix-based matching
|
|
@@ -141,8 +201,7 @@ Given this Markdown:
|
|
|
141
201
|
Define a section to extract the list items:
|
|
142
202
|
|
|
143
203
|
```ts
|
|
144
|
-
import { section } from "contentbase";
|
|
145
|
-
import { toString } from "mdast-util-to-string";
|
|
204
|
+
import { section, toString } from "contentbase";
|
|
146
205
|
|
|
147
206
|
const Story = defineModel("Story", {
|
|
148
207
|
sections: {
|
|
@@ -150,12 +209,13 @@ const Story = defineModel("Story", {
|
|
|
150
209
|
extract: (query) =>
|
|
151
210
|
query.selectAll("listItem").map((node) => toString(node)),
|
|
152
211
|
schema: z.array(z.string()),
|
|
212
|
+
alternatives: ["Requirements"], // fallback heading names
|
|
153
213
|
}),
|
|
154
214
|
},
|
|
155
215
|
});
|
|
156
216
|
```
|
|
157
217
|
|
|
158
|
-
The `extract` function receives an `AstQuery` scoped to the content under that heading. The `schema` is optional and used during validation.
|
|
218
|
+
The `extract` function receives an `AstQuery` scoped to the content under that heading. The `schema` is optional and used during validation. The `alternatives` array provides fallback heading names -- if "Acceptance Criteria" isn't found, it tries "Requirements" next.
|
|
159
219
|
|
|
160
220
|
Section data is **lazily computed and cached** -- the extract function only runs the first time you access the property.
|
|
161
221
|
|
|
@@ -272,6 +332,9 @@ const results = await collection
|
|
|
272
332
|
.query(Story)
|
|
273
333
|
.whereIn("meta.status", ["created", "in-progress"])
|
|
274
334
|
.whereExists("meta.epic")
|
|
335
|
+
.sort("meta.status", "asc")
|
|
336
|
+
.limit(10)
|
|
337
|
+
.offset(0)
|
|
275
338
|
.fetchAll();
|
|
276
339
|
|
|
277
340
|
// Convenience accessors
|
|
@@ -283,6 +346,60 @@ Available operators: `eq`, `neq`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`, `cont
|
|
|
283
346
|
|
|
284
347
|
Queries filter by model type **before** creating instances, so you only pay the parsing cost for matching documents.
|
|
285
348
|
|
|
349
|
+
### JSON Query DSL
|
|
350
|
+
|
|
351
|
+
For querying over the wire (REST API, MCP server) without executing arbitrary code, Contentbase provides a MongoDB-style JSON query DSL:
|
|
352
|
+
|
|
353
|
+
```json
|
|
354
|
+
{
|
|
355
|
+
"model": "Epic",
|
|
356
|
+
"where": {
|
|
357
|
+
"meta.status": "created",
|
|
358
|
+
"meta.priority": { "$gt": 3 }
|
|
359
|
+
},
|
|
360
|
+
"sort": { "meta.priority": "desc" },
|
|
361
|
+
"select": ["id", "title", "meta.status"],
|
|
362
|
+
"limit": 10,
|
|
363
|
+
"offset": 0
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Where value shortcuts:**
|
|
368
|
+
|
|
369
|
+
| Value type | Interpretation | Example |
|
|
370
|
+
|------------|---------------|---------|
|
|
371
|
+
| Literal (string, number, boolean, null) | Implicit `$eq` | `"meta.status": "active"` |
|
|
372
|
+
| Array | Implicit `$in` | `"meta.tags": ["a", "b"]` |
|
|
373
|
+
| Operator object | Explicit operator | `"meta.priority": { "$gt": 5 }` |
|
|
374
|
+
| Multiple operators | AND on same field | `"meta.priority": { "$gte": 3, "$lte": 8 }` |
|
|
375
|
+
|
|
376
|
+
**Available operators:** `$eq`, `$neq`, `$in`, `$notIn`, `$gt`, `$lt`, `$gte`, `$lte`, `$contains`, `$startsWith`, `$endsWith`, `$regex`, `$exists`
|
|
377
|
+
|
|
378
|
+
**Options:**
|
|
379
|
+
|
|
380
|
+
| Field | Type | Default | Description |
|
|
381
|
+
|-------|------|---------|-------------|
|
|
382
|
+
| `model` | string | *(required)* | Model name or prefix |
|
|
383
|
+
| `where` | object | -- | MongoDB-style filter conditions |
|
|
384
|
+
| `sort` | object or array | -- | `{ "field": "asc"\|"desc" }` or `[{ "path": "...", "direction": "asc" }]` |
|
|
385
|
+
| `select` | string[] | -- | Fields to include in output |
|
|
386
|
+
| `limit` | number | -- | Maximum results to return |
|
|
387
|
+
| `offset` | number | -- | Number of results to skip |
|
|
388
|
+
| `method` | string | `"fetchAll"` | `"fetchAll"`, `"first"`, `"last"`, or `"count"` |
|
|
389
|
+
|
|
390
|
+
Use via `POST /api/query` with a JSON body, or through the MCP `query` tool. The DSL parser is also available programmatically:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { executeQueryDSL, queryDSLSchema } from "contentbase";
|
|
394
|
+
|
|
395
|
+
const result = await executeQueryDSL(collection, {
|
|
396
|
+
model: "Epic",
|
|
397
|
+
where: { "meta.status": "created" },
|
|
398
|
+
sort: { title: "asc" },
|
|
399
|
+
limit: 5,
|
|
400
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
286
403
|
---
|
|
287
404
|
|
|
288
405
|
## Validation
|
|
@@ -376,6 +493,147 @@ await doc.reload();
|
|
|
376
493
|
|
|
377
494
|
---
|
|
378
495
|
|
|
496
|
+
## Standalone Parsing
|
|
497
|
+
|
|
498
|
+
The `parse()` function gives you a queryable document from a file path or raw markdown string, without needing a Collection:
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
import { parse } from "contentbase";
|
|
502
|
+
|
|
503
|
+
const doc = await parse("./content/my-post.mdx");
|
|
504
|
+
doc.title; // first heading text
|
|
505
|
+
doc.meta; // frontmatter
|
|
506
|
+
doc.astQuery.selectAll("heading"); // AST querying
|
|
507
|
+
doc.nodes.links; // node shortcuts
|
|
508
|
+
doc.querySection("Introduction").selectAll("paragraph");
|
|
509
|
+
|
|
510
|
+
// Also works with raw markdown
|
|
511
|
+
const doc2 = await parse("# Hello\n\nWorld");
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Extracting Sections Across Documents
|
|
517
|
+
|
|
518
|
+
`extractSections()` pulls named sections from multiple documents into a single combined document, with heading depths adjusted automatically:
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
import { extractSections } from "contentbase";
|
|
522
|
+
|
|
523
|
+
const combined = extractSections([
|
|
524
|
+
{ source: doc1, sections: "Acceptance Criteria" },
|
|
525
|
+
{ source: doc2, sections: ["Acceptance Criteria", "Mockups"] },
|
|
526
|
+
], {
|
|
527
|
+
title: "All Acceptance Criteria",
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
This produces:
|
|
532
|
+
|
|
533
|
+
```md
|
|
534
|
+
# All Acceptance Criteria
|
|
535
|
+
|
|
536
|
+
## Authentication
|
|
537
|
+
### Acceptance Criteria
|
|
538
|
+
- Users can sign up with email and password
|
|
539
|
+
- ...
|
|
540
|
+
|
|
541
|
+
## Searching And Browsing
|
|
542
|
+
### Acceptance Criteria
|
|
543
|
+
- Users can search by category
|
|
544
|
+
- ...
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Modes
|
|
548
|
+
|
|
549
|
+
**Grouped** (default) -- each source document gets a heading (its title), with extracted sections nested underneath:
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
extractSections(entries, { mode: "grouped" });
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Flat** -- sections are placed sequentially with no source grouping:
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
extractSections(entries, { mode: "flat" });
|
|
559
|
+
// ## Acceptance Criteria <- from doc1
|
|
560
|
+
// - ...
|
|
561
|
+
// ## Acceptance Criteria <- from doc2
|
|
562
|
+
// - ...
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Options
|
|
566
|
+
|
|
567
|
+
| Option | Default | Description |
|
|
568
|
+
| --- | --- | --- |
|
|
569
|
+
| `title` | -- | Optional h1 title for the combined document |
|
|
570
|
+
| `mode` | `"grouped"` | `"grouped"` nests under source titles, `"flat"` places sections sequentially |
|
|
571
|
+
| `onMissing` | `"skip"` | `"skip"` silently omits missing sections, `"throw"` raises an error |
|
|
572
|
+
|
|
573
|
+
The return value is a `ParsedDocument` -- fully queryable with `astQuery`, `nodes`, `extractSection()`, `querySection()`, and `stringify()`.
|
|
574
|
+
|
|
575
|
+
Sources can be any mix of `Document` and `ParsedDocument` instances.
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## Table of Contents Generation
|
|
580
|
+
|
|
581
|
+
Generate a markdown table of contents for a collection with links that work on GitHub:
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
const toc = collection.tableOfContents({ title: "Project Docs" });
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Output:
|
|
588
|
+
|
|
589
|
+
```md
|
|
590
|
+
# Project Docs
|
|
591
|
+
|
|
592
|
+
## Epic
|
|
593
|
+
|
|
594
|
+
- [Authentication](./epics/authentication.mdx)
|
|
595
|
+
- [Searching And Browsing](./epics/searching-and-browsing.mdx)
|
|
596
|
+
|
|
597
|
+
## Story
|
|
598
|
+
|
|
599
|
+
- [A User should be able to register.](./stories/authentication/a-user-should-be-able-to-register.mdx)
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
If models are registered, documents are grouped by model. Without models, a flat list is produced. Use `basePath` to control the link prefix:
|
|
603
|
+
|
|
604
|
+
```ts
|
|
605
|
+
collection.tableOfContents({ basePath: "./content" });
|
|
606
|
+
// links become: ./content/epics/authentication.mdx
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### File Tree
|
|
610
|
+
|
|
611
|
+
Render an ASCII file tree of all documents in the collection:
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
const tree = collection.renderFileTree();
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
```
|
|
618
|
+
epics/
|
|
619
|
+
├── authentication.mdx
|
|
620
|
+
└── searching-and-browsing.mdx
|
|
621
|
+
stories/
|
|
622
|
+
└── authentication/
|
|
623
|
+
└── a-user-should-be-able-to-register.mdx
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Model Summary
|
|
627
|
+
|
|
628
|
+
Generate comprehensive documentation of all registered models, including schema fields, sections, relationships, and defaults:
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
const summary = await collection.generateModelSummary();
|
|
632
|
+
// Returns markdown documenting each model's schema, sections, relationships
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
379
637
|
## Computed Properties
|
|
380
638
|
|
|
381
639
|
Derived values that are lazily evaluated from instance data:
|
|
@@ -422,16 +680,141 @@ collection.use(timestampPlugin, { format: "iso" });
|
|
|
422
680
|
|
|
423
681
|
## CLI
|
|
424
682
|
|
|
425
|
-
Contentbase ships with a CLI for
|
|
683
|
+
Contentbase ships with a CLI available as both `cnotes` and `contentbase`. See [CLI.md](./CLI.md) for the full reference with examples for every command.
|
|
684
|
+
|
|
685
|
+
```bash
|
|
686
|
+
bun add contentbase
|
|
687
|
+
|
|
688
|
+
# Then use it via bunx, or in package.json scripts
|
|
689
|
+
bunx cnotes inspect
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Commands
|
|
693
|
+
|
|
694
|
+
```bash
|
|
695
|
+
cnotes init [name] # scaffold a new project
|
|
696
|
+
cnotes create <Model> --title "..." # scaffold a new document (uses templates if available)
|
|
697
|
+
cnotes inspect # show models, sections, relationships, doc counts
|
|
698
|
+
cnotes validate [target] # validate documents ('all', a model name, or a path ID)
|
|
699
|
+
cnotes export # export collection as JSON
|
|
700
|
+
cnotes extract <glob> --sections "A, B" # extract specific sections from matching documents
|
|
701
|
+
cnotes summary # generate MODELS.md and TABLE-OF-CONTENTS.md
|
|
702
|
+
cnotes teach # output combined documentation for LLM context
|
|
703
|
+
cnotes action <name> # run a named action
|
|
704
|
+
cnotes text-search <pattern> # search file contents with pattern matching
|
|
705
|
+
cnotes serve # start HTTP server with REST API and doc serving
|
|
706
|
+
cnotes mcp # start MCP server for AI agent integration
|
|
707
|
+
cnotes console # interactive REPL with collection in scope
|
|
708
|
+
cnotes help # list available commands
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
All commands accept `--contentFolder` to specify which folder contains your content. Defaults to `./docs`. You can also set it in `package.json`:
|
|
712
|
+
|
|
713
|
+
```json
|
|
714
|
+
{
|
|
715
|
+
"contentbase": {
|
|
716
|
+
"contentFolder": "content"
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### serve
|
|
722
|
+
|
|
723
|
+
Start an HTTP server that exposes a full REST API for the collection. Documents are available as JSON, rendered HTML, or raw markdown.
|
|
724
|
+
|
|
725
|
+
```bash
|
|
726
|
+
# Start on default port 8000
|
|
727
|
+
cnotes serve
|
|
728
|
+
|
|
729
|
+
# Custom port, specific content folder
|
|
730
|
+
cnotes serve --port 9000 --contentFolder ./sdlc
|
|
731
|
+
|
|
732
|
+
# Read-only mode: disables all write endpoints (POST/PUT/PATCH/DELETE)
|
|
733
|
+
cnotes serve --read-only
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**Built-in endpoints:**
|
|
737
|
+
|
|
738
|
+
| Path | Description |
|
|
739
|
+
|------|-------------|
|
|
740
|
+
| `GET /api/inspect` | Collection overview |
|
|
741
|
+
| `GET /api/models` | All model definitions |
|
|
742
|
+
| `GET /api/documents` | List documents (filter with `?model=`) |
|
|
743
|
+
| `GET/POST/PUT/PATCH/DELETE /api/documents/:pathId` | Document CRUD |
|
|
744
|
+
| `GET /api/query?model=&where=&select=` | Query model instances (flat condition format) |
|
|
745
|
+
| `POST /api/query` | Query with JSON DSL body (MongoDB-style) |
|
|
746
|
+
| `GET /api/search?pattern=` | Full-text regex search across documents |
|
|
747
|
+
| `GET /api/text-search?pattern=` | File-level text search (`expanded=true` for line detail) |
|
|
748
|
+
| `GET /api/validate?pathId=` | Validate against schema |
|
|
749
|
+
| `GET/POST /api/actions` | List or execute actions |
|
|
750
|
+
| `GET /docs/:path.json\|.md\|.html` | Content-negotiated doc serving |
|
|
751
|
+
| `GET /openapi.json` | Auto-generated OpenAPI 3.1 spec |
|
|
752
|
+
|
|
753
|
+
When `--read-only` is passed, all mutating endpoints return `403 Forbidden`. This includes `POST /api/documents`, `PUT/PATCH/DELETE /api/documents/:pathId`, and `POST /api/actions`. Read endpoints are unaffected.
|
|
754
|
+
|
|
755
|
+
You can also add your own endpoints by placing files in an `endpoints/` directory. See [CLI.md](./CLI.md#user-defined-endpoints) for details.
|
|
756
|
+
|
|
757
|
+
### mcp
|
|
758
|
+
|
|
759
|
+
Start a Model Context Protocol server for AI agent integration. Exposes tools, resources, and prompts for the collection.
|
|
426
760
|
|
|
427
761
|
```bash
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
contentbase export # export collection as JSON
|
|
431
|
-
contentbase create Story # scaffold a new document
|
|
432
|
-
contentbase action publish # run a named action
|
|
762
|
+
cnotes mcp # stdio transport (for Claude Desktop, etc.)
|
|
763
|
+
cnotes mcp --transport http --port 3003 # HTTP transport
|
|
433
764
|
```
|
|
434
765
|
|
|
766
|
+
### extract
|
|
767
|
+
|
|
768
|
+
The `extract` command outputs document titles, leading content, and only the requested sections -- combined into a single document suitable for creating new content:
|
|
769
|
+
|
|
770
|
+
```bash
|
|
771
|
+
# Extract Acceptance Criteria from all stories
|
|
772
|
+
cnotes extract "stories/**/*" --sections "Acceptance Criteria"
|
|
773
|
+
|
|
774
|
+
# Combine epics into a single document with a title
|
|
775
|
+
cnotes extract "epics/*" -s "Stories" --title "All Stories"
|
|
776
|
+
|
|
777
|
+
# Multiple sections, include frontmatter, raw heading depths
|
|
778
|
+
cnotes extract "epics/*" -s "Stories, Notes" --frontmatter --no-normalize-headings
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
Glob patterns are matched against document path IDs using [picomatch](https://github.com/micromatch/picomatch). Sections that don't exist in a document are silently skipped.
|
|
782
|
+
|
|
783
|
+
By default, heading depths are normalized so each document's content nests properly in the combined output. When `--title` is provided, it becomes the h1 and document titles shift to h2. Use `--no-normalize-headings` to preserve original heading depths.
|
|
784
|
+
|
|
785
|
+
### create
|
|
786
|
+
|
|
787
|
+
The `create` command scaffolds new documents with smart defaults:
|
|
788
|
+
|
|
789
|
+
```bash
|
|
790
|
+
cnotes create Story --title "User can logout"
|
|
791
|
+
cnotes create Epic --title "Payments" --meta.priority high
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
If a template exists at `templates/<model>.md` (or `.mdx`) in your content directory, it's used as the base. Meta values are merged with this priority: Zod defaults < model defaults < template frontmatter < CLI `--meta.*` flags.
|
|
795
|
+
|
|
796
|
+
### Model Discovery
|
|
797
|
+
|
|
798
|
+
The CLI uses a 3-tier system to find your models:
|
|
799
|
+
|
|
800
|
+
**Tier 1 — `index.ts`** (recommended): If your content directory has an `index.ts` that exports a `Collection` with models registered, the CLI uses it directly. This is what `contentbase init` scaffolds.
|
|
801
|
+
|
|
802
|
+
```ts
|
|
803
|
+
// docs/index.ts
|
|
804
|
+
import { Collection, defineModel, z } from "contentbase";
|
|
805
|
+
|
|
806
|
+
const Post = defineModel("Post", {
|
|
807
|
+
meta: z.object({ draft: z.boolean().default(false) }),
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
export const collection = new Collection({ rootPath: import.meta.dir });
|
|
811
|
+
collection.register(Post);
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
**Tier 2 — `models.ts`**: If no `index.ts` exists but a `models.ts` is found, the CLI imports it, detects model definitions from exports, and auto-registers them on a new Collection.
|
|
815
|
+
|
|
816
|
+
**Tier 3 — Auto-discovery**: If neither file exists, the CLI scans top-level subdirectories for markdown files and generates bare models from folder names (`epics/` → `Epic`). These models have no schema validation — useful for quick inspection, but you'll want a `models.ts` or `index.ts` for real use.
|
|
817
|
+
|
|
435
818
|
---
|
|
436
819
|
|
|
437
820
|
## API Reference
|
|
@@ -442,16 +825,26 @@ contentbase action publish # run a named action
|
|
|
442
825
|
| --- | --- |
|
|
443
826
|
| `Collection` | Loads and manages a directory of documents |
|
|
444
827
|
| `Document` | A single Markdown/MDX file with AST operations |
|
|
445
|
-
| `defineModel()` | Create a typed model definition |
|
|
828
|
+
| `defineModel()` | Create a typed model definition (accepts optional `description`, auto-generated if omitted) |
|
|
829
|
+
| `generateDescription()` | Generate a human-readable model description from its schema |
|
|
446
830
|
| `section()` | Declare a section extraction |
|
|
447
831
|
| `hasMany()` | Declare a one-to-many relationship |
|
|
448
832
|
| `belongsTo()` | Declare a many-to-one relationship |
|
|
833
|
+
| `parse()` | Parse a file path or markdown string into a queryable `ParsedDocument` |
|
|
834
|
+
| `extractSections()` | Combine sections from multiple documents into one |
|
|
449
835
|
| `CollectionQuery` | Fluent query builder for model instances |
|
|
836
|
+
| `queryDSLSchema` | Zod schema for validating JSON query DSL input |
|
|
837
|
+
| `parseWhereClause()` | Parse MongoDB-style where object into internal conditions |
|
|
838
|
+
| `executeQueryDSL()` | Execute a JSON query DSL against a collection |
|
|
450
839
|
| `AstQuery` | MDAST query wrapper (select, visit, find) |
|
|
451
840
|
| `NodeShortcuts` | Convenience getters for common AST nodes |
|
|
452
841
|
| `createModelInstance()` | Low-level factory for model instances |
|
|
453
842
|
| `validateDocument()` | Standalone validation function |
|
|
843
|
+
| `matchPattern()` | Express-style path pattern matching (`:param` syntax) |
|
|
844
|
+
| `matchPatterns()` | Try multiple patterns against a path, first match wins |
|
|
845
|
+
| `introspectMetaSchema()` | Extract field info (name, type, required, default) from a Zod schema |
|
|
454
846
|
| `z` | Re-exported from Zod (no extra dependency needed) |
|
|
847
|
+
| `toString` | Re-exported from `mdast-util-to-string` |
|
|
455
848
|
|
|
456
849
|
---
|
|
457
850
|
|