contentbase 0.0.2 → 0.0.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.
Files changed (130) hide show
  1. package/CLI.md +593 -0
  2. package/MCP-SERVER-SPEC.md +453 -0
  3. package/PRIMER.md +540 -0
  4. package/README.md +289 -13
  5. package/docs/README.md +110 -0
  6. package/docs/TABLE-OF-CONTENTS.md +7 -0
  7. package/docs/models.ts +38 -0
  8. package/models.ts +38 -0
  9. package/package.json +12 -3
  10. package/src/api/endpoints/actions.ts +34 -0
  11. package/src/api/endpoints/doc.ts +187 -0
  12. package/src/api/endpoints/docs-index.ts +26 -0
  13. package/src/api/endpoints/document.ts +171 -0
  14. package/src/api/endpoints/documents.ts +71 -0
  15. package/src/api/endpoints/inspect.ts +16 -0
  16. package/src/api/endpoints/models.ts +10 -0
  17. package/src/api/endpoints/query.ts +88 -0
  18. package/src/api/endpoints/root.ts +7 -0
  19. package/src/api/endpoints/search-reindex.ts +65 -0
  20. package/src/api/endpoints/search-status.ts +55 -0
  21. package/src/api/endpoints/search.ts +104 -0
  22. package/src/api/endpoints/semantic-search.ts +120 -0
  23. package/src/api/endpoints/text-search.ts +63 -0
  24. package/src/api/endpoints/validate.ts +34 -0
  25. package/src/api/helpers.ts +97 -0
  26. package/src/base-model.ts +12 -0
  27. package/src/cli/commands/action.ts +82 -44
  28. package/src/cli/commands/console.ts +124 -0
  29. package/src/cli/commands/create.ts +179 -53
  30. package/src/cli/commands/embed.ts +323 -0
  31. package/src/cli/commands/export.ts +58 -24
  32. package/src/cli/commands/extract.ts +174 -0
  33. package/src/cli/commands/help.ts +81 -0
  34. package/src/cli/commands/index.ts +17 -0
  35. package/src/cli/commands/init.ts +72 -48
  36. package/src/cli/commands/inspect.ts +56 -46
  37. package/src/cli/commands/mcp.ts +1219 -0
  38. package/src/cli/commands/search.ts +285 -0
  39. package/src/cli/commands/serve.ts +348 -0
  40. package/src/cli/commands/summary.ts +60 -0
  41. package/src/cli/commands/teach.ts +86 -0
  42. package/src/cli/commands/text-search.ts +134 -0
  43. package/src/cli/commands/validate.ts +126 -64
  44. package/src/cli/index.ts +88 -19
  45. package/src/cli/load-collection.ts +144 -17
  46. package/src/cli/registry.ts +28 -0
  47. package/src/collection.ts +361 -10
  48. package/src/define-model.ts +101 -4
  49. package/src/document.ts +47 -1
  50. package/src/extract-sections.ts +1 -1
  51. package/src/index.ts +14 -2
  52. package/src/model-instance.ts +35 -5
  53. package/src/node-shortcuts.ts +1 -1
  54. package/src/query/collection-query.ts +96 -9
  55. package/src/query/index.ts +7 -0
  56. package/src/query/query-dsl.ts +259 -0
  57. package/src/relationships/has-many.ts +39 -0
  58. package/src/relationships/index.ts +7 -2
  59. package/src/section.ts +2 -0
  60. package/src/types.ts +23 -1
  61. package/src/utils/index.ts +1 -0
  62. package/src/utils/match-pattern.ts +65 -0
  63. package/src/validator.ts +18 -1
  64. package/bun.lock +0 -473
  65. package/examples/sdlc-queries.ts +0 -161
  66. package/scripts/examples/01-collection-setup.ts +0 -46
  67. package/scripts/examples/02-querying.ts +0 -67
  68. package/scripts/examples/03-sections.ts +0 -36
  69. package/scripts/examples/04-relationships.ts +0 -54
  70. package/scripts/examples/05-document-api.ts +0 -54
  71. package/scripts/examples/06-extract-sections.ts +0 -55
  72. package/scripts/examples/07-validation.ts +0 -46
  73. package/scripts/examples/08-serialization.ts +0 -51
  74. package/scripts/examples/lib/format.ts +0 -87
  75. package/scripts/examples/lib/setup.ts +0 -21
  76. package/scripts/examples/run-all.ts +0 -43
  77. package/showcases/national-parks/models.ts +0 -74
  78. package/showcases/national-parks/parks/acadia.mdx +0 -40
  79. package/showcases/national-parks/parks/yosemite.mdx +0 -44
  80. package/showcases/national-parks/parks/zion.mdx +0 -44
  81. package/showcases/national-parks/queries.ts +0 -103
  82. package/showcases/national-parks/trails/angels-landing.mdx +0 -19
  83. package/showcases/national-parks/trails/cathedral-lakes.mdx +0 -19
  84. package/showcases/national-parks/trails/half-dome.mdx +0 -19
  85. package/showcases/national-parks/trails/jordan-pond-path.mdx +0 -19
  86. package/showcases/national-parks/trails/mist-trail.mdx +0 -19
  87. package/showcases/national-parks/trails/observation-point.mdx +0 -19
  88. package/showcases/national-parks/trails/precipice-trail.mdx +0 -19
  89. package/showcases/national-parks/trails/the-narrows.mdx +0 -19
  90. package/showcases/recipes/cuisines/chinese.mdx +0 -28
  91. package/showcases/recipes/cuisines/italian.mdx +0 -32
  92. package/showcases/recipes/cuisines/mexican.mdx +0 -28
  93. package/showcases/recipes/models.ts +0 -77
  94. package/showcases/recipes/queries.ts +0 -89
  95. package/showcases/recipes/recipes/chinese/egg-fried-rice.mdx +0 -43
  96. package/showcases/recipes/recipes/chinese/mapo-tofu.mdx +0 -47
  97. package/showcases/recipes/recipes/italian/bruschetta.mdx +0 -38
  98. package/showcases/recipes/recipes/italian/cacio-e-pepe.mdx +0 -39
  99. package/showcases/recipes/recipes/italian/tiramisu.mdx +0 -43
  100. package/showcases/recipes/recipes/mexican/chicken-tinga.mdx +0 -44
  101. package/showcases/recipes/recipes/mexican/guacamole.mdx +0 -39
  102. package/showcases/vinyl-collection/albums/bitches-brew.mdx +0 -36
  103. package/showcases/vinyl-collection/albums/i-put-a-spell-on-you.mdx +0 -35
  104. package/showcases/vinyl-collection/albums/in-rainbows.mdx +0 -35
  105. package/showcases/vinyl-collection/albums/kind-of-blue.mdx +0 -32
  106. package/showcases/vinyl-collection/albums/ok-computer.mdx +0 -37
  107. package/showcases/vinyl-collection/albums/wild-is-the-wind.mdx +0 -35
  108. package/showcases/vinyl-collection/artists/miles-davis.mdx +0 -27
  109. package/showcases/vinyl-collection/artists/nina-simone.mdx +0 -26
  110. package/showcases/vinyl-collection/artists/radiohead.mdx +0 -27
  111. package/showcases/vinyl-collection/models.ts +0 -73
  112. package/showcases/vinyl-collection/queries.ts +0 -87
  113. package/test/ast-query.test.ts +0 -128
  114. package/test/collection.test.ts +0 -99
  115. package/test/define-model.test.ts +0 -78
  116. package/test/document.test.ts +0 -225
  117. package/test/extract-sections.test.ts +0 -356
  118. package/test/fixtures/sdlc/epics/authentication.mdx +0 -42
  119. package/test/fixtures/sdlc/epics/searching-and-browsing.mdx +0 -21
  120. package/test/fixtures/sdlc/models.ts +0 -89
  121. package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-register.mdx +0 -20
  122. package/test/helpers.ts +0 -21
  123. package/test/model-instance.test.ts +0 -197
  124. package/test/query.test.ts +0 -167
  125. package/test/relationships.test.ts +0 -84
  126. package/test/section.test.ts +0 -99
  127. package/test/table-of-contents.test.ts +0 -91
  128. package/test/validator.test.ts +0 -62
  129. package/tsconfig.json +0 -18
  130. package/vitest.config.ts +0 -11
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({ rootPath: "./content" });
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
@@ -489,6 +606,32 @@ collection.tableOfContents({ basePath: "./content" });
489
606
  // links become: ./content/epics/authentication.mdx
490
607
  ```
491
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
+
492
635
  ---
493
636
 
494
637
  ## Computed Properties
@@ -537,16 +680,141 @@ collection.use(timestampPlugin, { format: "iso" });
537
680
 
538
681
  ## CLI
539
682
 
540
- Contentbase ships with a CLI for common operations:
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.
541
760
 
542
761
  ```bash
543
- contentbase inspect # show collection info
544
- contentbase validate # validate all documents
545
- contentbase export # export collection as JSON
546
- contentbase create Story # scaffold a new document
547
- 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
548
764
  ```
549
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
+
550
818
  ---
551
819
 
552
820
  ## API Reference
@@ -557,18 +825,26 @@ contentbase action publish # run a named action
557
825
  | --- | --- |
558
826
  | `Collection` | Loads and manages a directory of documents |
559
827
  | `Document` | A single Markdown/MDX file with AST operations |
560
- | `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 |
561
830
  | `section()` | Declare a section extraction |
562
831
  | `hasMany()` | Declare a one-to-many relationship |
563
832
  | `belongsTo()` | Declare a many-to-one relationship |
564
833
  | `parse()` | Parse a file path or markdown string into a queryable `ParsedDocument` |
565
834
  | `extractSections()` | Combine sections from multiple documents into one |
566
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 |
567
839
  | `AstQuery` | MDAST query wrapper (select, visit, find) |
568
840
  | `NodeShortcuts` | Convenience getters for common AST nodes |
569
841
  | `createModelInstance()` | Low-level factory for model instances |
570
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 |
571
846
  | `z` | Re-exported from Zod (no extra dependency needed) |
847
+ | `toString` | Re-exported from `mdast-util-to-string` |
572
848
 
573
849
  ---
574
850
 
package/docs/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # Models
2
+
3
+ Models define the structure of markdown documents in this collection. Each document is a markdown file with YAML frontmatter (metadata attributes) and a heading-based structure (sections). Models specify the expected frontmatter fields via a schema, named sections that map to `##` headings in the document body, relationships to other models, and computed properties derived at query time.
4
+
5
+ ## Examples
6
+
7
+ An Example has metadata (tags).
8
+
9
+ **Prefix:** `examples`
10
+
11
+ ### Attributes
12
+
13
+ | Field | Type | Required | Default | Description |
14
+ |-------|------|----------|---------|-------------|
15
+ | tags | string[] | optional | `[]` | Arbitrary tags for categorizing the example |
16
+
17
+ ### Example
18
+
19
+ ```markdown
20
+ ---
21
+ tags: []
22
+ ---
23
+ # Example Title
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Ideas
29
+
30
+ An Idea has metadata (goal, tags, status).
31
+
32
+ **Prefix:** `ideas`
33
+
34
+ ### Attributes
35
+
36
+ | Field | Type | Required | Default | Description |
37
+ |-------|------|----------|---------|-------------|
38
+ | goal | string | optional | — | Slug of the goal this idea is aligned to |
39
+ | tags | string[] | optional | `[]` | Arbitrary tags for categorizing the idea |
40
+ | status | enum(`spark`, `exploring`, `parked`, `promoted`) | optional | `"spark"` | spark is a new raw idea, exploring means actively thinking about it, parked means on hold, promoted means it became a plan |
41
+
42
+ ### Example
43
+
44
+ ```markdown
45
+ ---
46
+ tags: []
47
+ status: spark
48
+ ---
49
+ # Idea Title
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Reports
55
+
56
+ A Report has metadata (tags).
57
+
58
+ **Prefix:** `reports`
59
+
60
+ ### Attributes
61
+
62
+ | Field | Type | Required | Default | Description |
63
+ |-------|------|----------|---------|-------------|
64
+ | tags | string[] | optional | `[]` | Arbitrary tags for categorizing the report |
65
+
66
+ ### Example
67
+
68
+ ```markdown
69
+ ---
70
+ tags: []
71
+ ---
72
+ # Report Title
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Tutorials
78
+
79
+ A Tutorial has metadata (tags).
80
+
81
+ **Prefix:** `tutorials`
82
+
83
+ ### Attributes
84
+
85
+ | Field | Type | Required | Default | Description |
86
+ |-------|------|----------|---------|-------------|
87
+ | tags | string[] | optional | `[]` | Arbitrary tags for categorizing the tutorial |
88
+
89
+ ### Example
90
+
91
+ ```markdown
92
+ ---
93
+ tags: []
94
+ ---
95
+ # Tutorial Title
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Bases
101
+
102
+ A Base document.
103
+
104
+ **Prefix:** ``
105
+
106
+ ### Example
107
+
108
+ ```markdown
109
+ # Base Title
110
+ ```
@@ -0,0 +1,7 @@
1
+ # Table of Contents
2
+
3
+ ## Ideas
4
+
5
+ - [Changelogs](./ideas/changelogs.md)
6
+ - [Content model collections](./ideas/content-model-collections.md)
7
+ - [Semantic Search Integration](./ideas/semantic-search.md)
package/docs/models.ts ADDED
@@ -0,0 +1,38 @@
1
+ import {
2
+ defineModel,
3
+ section,
4
+ hasMany,
5
+ belongsTo,
6
+ AstQuery,
7
+ z,
8
+ } from "contentbase";
9
+
10
+ export const Idea = defineModel("Idea", {
11
+ prefix: "ideas",
12
+ meta: z.object({
13
+ goal: z.string().optional().describe("Slug of the goal this idea is aligned to"),
14
+ tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the idea"),
15
+ status: z.enum(["spark", "exploring", "parked", "promoted"]).default("spark").describe("spark is a new raw idea, exploring means actively thinking about it, parked means on hold, promoted means it became a plan"),
16
+ }),
17
+ });
18
+
19
+ export const Tutorial = defineModel("Tutorial", {
20
+ prefix: "tutorials",
21
+ meta: z.object({
22
+ tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the tutorial"),
23
+ }),
24
+ })
25
+
26
+ export const Report = defineModel("Report", {
27
+ prefix: "reports",
28
+ meta: z.object({
29
+ tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the report"),
30
+ }),
31
+ })
32
+
33
+ export const Example = defineModel("Example", {
34
+ prefix: "examples",
35
+ meta: z.object({
36
+ tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the example"),
37
+ }),
38
+ })
package/models.ts ADDED
@@ -0,0 +1,38 @@
1
+ import {
2
+ defineModel,
3
+ section,
4
+ hasMany,
5
+ belongsTo,
6
+ AstQuery,
7
+ z,
8
+ } from "contentbase";
9
+
10
+ export const Idea = defineModel("Idea", {
11
+ prefix: "ideas",
12
+ meta: z.object({
13
+ goal: z.string().optional().describe("Slug of the goal this idea is aligned to"),
14
+ tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the idea"),
15
+ status: z.enum(["spark", "exploring", "parked", "promoted"]).default("spark").describe("spark is a new raw idea, exploring means actively thinking about it, parked means on hold, promoted means it became a plan"),
16
+ }),
17
+ });
18
+
19
+ export const Tutorial = defineModel("Tutorial", {
20
+ prefix: "tutorials",
21
+ meta: z.object({
22
+ tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the tutorial"),
23
+ }),
24
+ })
25
+
26
+ export const Report = defineModel("Report", {
27
+ prefix: "reports",
28
+ meta: z.object({
29
+ tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the report"),
30
+ }),
31
+ })
32
+
33
+ export const Example = defineModel("Example", {
34
+ prefix: "examples",
35
+ meta: z.object({
36
+ tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the example"),
37
+ }),
38
+ })
package/package.json CHANGED
@@ -1,27 +1,32 @@
1
1
  {
2
2
  "name": "contentbase",
3
- "version": "0.0.2",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
7
7
  "bin": {
8
+ "cnotes": "./src/cli/index.ts",
8
9
  "contentbase": "./src/cli/index.ts"
9
10
  },
10
11
  "scripts": {
11
12
  "test": "vitest run",
12
13
  "test:watch": "vitest",
13
14
  "typecheck": "tsc --noEmit",
14
- "examples": "bun run scripts/examples/run-all.ts"
15
+ "examples": "bun run scripts/examples/run-all.ts",
16
+ "compile": "bun build ./src/cli/index.ts --compile --outfile dist/cnotes --external @node-llama-cpp/*"
15
17
  },
16
18
  "dependencies": {
17
- "citty": "^0.1.6",
19
+ "@soederpop/luca": "^0.0.2",
18
20
  "gray-matter": "^4.0.3",
19
21
  "js-yaml": "^4.1.0",
20
22
  "mdast-util-mdxjs-esm": "^2.0.1",
21
23
  "mdast-util-to-markdown": "^2.1.2",
22
24
  "mdast-util-to-string": "^4.0.0",
25
+ "picomatch": "^4.0.3",
26
+ "rehype-stringify": "^10.0.1",
23
27
  "remark-gfm": "^4.0.0",
24
28
  "remark-parse": "^11.0.0",
29
+ "remark-rehype": "^11.1.2",
25
30
  "remark-stringify": "^11.0.0",
26
31
  "unified": "^11.0.5",
27
32
  "unist-util-find-after": "^5.0.0",
@@ -35,8 +40,12 @@
35
40
  "devDependencies": {
36
41
  "@types/js-yaml": "^4.0.9",
37
42
  "@types/mdast": "^4.0.4",
43
+ "@types/picomatch": "^4.0.2",
38
44
  "bun-types": "^1.1.0",
39
45
  "typescript": "^5.4.0",
40
46
  "vitest": "^1.6.0"
47
+ },
48
+ "luca": {
49
+ "aliases": ["contentbase", "content base", "content", "the orm"]
41
50
  }
42
51
  }
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod'
2
+
3
+ export const path = '/api/actions'
4
+ export const description = 'List or execute collection actions'
5
+ export const tags = ['actions']
6
+
7
+ export async function get(_params: any, ctx: any) {
8
+ const collection = ctx.container._contentbaseCollection
9
+ return { actions: collection.availableActions }
10
+ }
11
+
12
+ export const postSchema = z.object({
13
+ name: z.string(),
14
+ args: z.array(z.any()).optional(),
15
+ })
16
+
17
+ export async function post(params: any, ctx: any) {
18
+ if (ctx.container._contentbaseReadOnly) {
19
+ ctx.response.status(403)
20
+ return { error: 'Server is running in read-only mode' }
21
+ }
22
+
23
+ const collection = ctx.container._contentbaseCollection
24
+
25
+ if (!collection.availableActions.includes(params.name)) {
26
+ ctx.response.status(400)
27
+ return {
28
+ error: `Unknown action: ${params.name}. Available: ${collection.availableActions.join(', ') || '(none)'}`,
29
+ }
30
+ }
31
+
32
+ const result = await collection.runAction(params.name, ...(params.args || []))
33
+ return typeof result === 'string' ? { result } : result
34
+ }