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.
Files changed (97) hide show
  1. package/.mcp.json +9 -0
  2. package/CLI.md +593 -0
  3. package/MCP-DESCRIPTIONS-REVIEW.md +122 -0
  4. package/MCP-SERVER-SPEC.md +453 -0
  5. package/PRIMER.md +540 -0
  6. package/README.md +406 -13
  7. package/bun.lock +45 -6
  8. package/dist/cnotes +0 -0
  9. package/docs/README.md +110 -0
  10. package/docs/TABLE-OF-CONTENTS.md +7 -0
  11. package/docs/models.ts +38 -0
  12. package/models.ts +38 -0
  13. package/package.json +14 -4
  14. package/public/web-demo/index.html +813 -0
  15. package/scripts/examples/01-collection-setup.ts +46 -0
  16. package/scripts/examples/02-querying.ts +67 -0
  17. package/scripts/examples/03-sections.ts +36 -0
  18. package/scripts/examples/04-relationships.ts +54 -0
  19. package/scripts/examples/05-document-api.ts +54 -0
  20. package/scripts/examples/06-extract-sections.ts +55 -0
  21. package/scripts/examples/07-validation.ts +46 -0
  22. package/scripts/examples/08-serialization.ts +51 -0
  23. package/scripts/examples/lib/format.ts +87 -0
  24. package/scripts/examples/lib/setup.ts +21 -0
  25. package/scripts/examples/run-all.ts +43 -0
  26. package/showcases/node_modules/.cache/.repl_history +3 -0
  27. package/showcases/vinyl-collection/artists/nina-simone.mdx +1 -1
  28. package/src/__tests__/semantic-search.integration.test.ts +284 -0
  29. package/src/api/endpoints/actions.ts +34 -0
  30. package/src/api/endpoints/doc.ts +187 -0
  31. package/src/api/endpoints/docs-index.ts +26 -0
  32. package/src/api/endpoints/document.ts +171 -0
  33. package/src/api/endpoints/documents.ts +71 -0
  34. package/src/api/endpoints/inspect.ts +16 -0
  35. package/src/api/endpoints/models.ts +10 -0
  36. package/src/api/endpoints/query.ts +88 -0
  37. package/src/api/endpoints/root.ts +7 -0
  38. package/src/api/endpoints/search-reindex.ts +65 -0
  39. package/src/api/endpoints/search-status.ts +55 -0
  40. package/src/api/endpoints/search.ts +104 -0
  41. package/src/api/endpoints/semantic-search.ts +120 -0
  42. package/src/api/endpoints/text-search.ts +63 -0
  43. package/src/api/endpoints/validate.ts +34 -0
  44. package/src/api/helpers.ts +97 -0
  45. package/src/base-model.ts +12 -0
  46. package/src/cli/commands/action.ts +82 -44
  47. package/src/cli/commands/console.ts +124 -0
  48. package/src/cli/commands/create.ts +179 -53
  49. package/src/cli/commands/embed.ts +323 -0
  50. package/src/cli/commands/export.ts +58 -24
  51. package/src/cli/commands/extract.ts +174 -0
  52. package/src/cli/commands/help.ts +81 -0
  53. package/src/cli/commands/index.ts +17 -0
  54. package/src/cli/commands/init.ts +72 -48
  55. package/src/cli/commands/inspect.ts +56 -46
  56. package/src/cli/commands/mcp.ts +1219 -0
  57. package/src/cli/commands/search.ts +285 -0
  58. package/src/cli/commands/serve.ts +348 -0
  59. package/src/cli/commands/summary.ts +60 -0
  60. package/src/cli/commands/teach.ts +86 -0
  61. package/src/cli/commands/text-search.ts +134 -0
  62. package/src/cli/commands/validate.ts +126 -64
  63. package/src/cli/index.ts +88 -19
  64. package/src/cli/load-collection.ts +144 -17
  65. package/src/cli/registry.ts +28 -0
  66. package/src/collection.ts +455 -6
  67. package/src/define-model.ts +104 -7
  68. package/src/document.ts +47 -1
  69. package/src/extract-sections.ts +222 -0
  70. package/src/index.ts +20 -2
  71. package/src/model-instance.ts +35 -5
  72. package/src/node-shortcuts.ts +1 -1
  73. package/src/query/collection-query.ts +96 -9
  74. package/src/query/index.ts +7 -0
  75. package/src/query/query-dsl.ts +259 -0
  76. package/src/relationships/has-many.ts +39 -0
  77. package/src/relationships/index.ts +7 -2
  78. package/src/section.ts +2 -0
  79. package/src/types.ts +24 -2
  80. package/src/utils/index.ts +1 -0
  81. package/src/utils/match-pattern.ts +65 -0
  82. package/src/validator.ts +18 -1
  83. package/test/collection.test.ts +118 -2
  84. package/test/extract-sections.test.ts +356 -0
  85. package/test/fixtures/sdlc/MODELS.md +106 -0
  86. package/test/fixtures/sdlc/SKILL.md +404 -0
  87. package/test/fixtures/sdlc/index.ts +9 -0
  88. package/test/fixtures/sdlc/models.ts +8 -6
  89. package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +20 -0
  90. package/test/fixtures/sdlc/templates/epic.md +23 -0
  91. package/test/fixtures/sdlc/templates/story.md +19 -0
  92. package/test/pattern.test.ts +191 -0
  93. package/test/query-dsl.test.ts +431 -0
  94. package/test/query.test.ts +29 -0
  95. package/test/relationships.test.ts +130 -0
  96. package/test/section.test.ts +65 -4
  97. package/test/table-of-contents.test.ts +135 -0
@@ -0,0 +1,97 @@
1
+ import { unified } from 'unified'
2
+ import remarkParse from 'remark-parse'
3
+ import remarkGfm from 'remark-gfm'
4
+ import remarkRehype from 'remark-rehype'
5
+ import rehypeStringify from 'rehype-stringify'
6
+ import type { Collection } from '../collection.js'
7
+ import { introspectMetaSchema } from '../collection.js'
8
+
9
+ export async function renderMarkdownToHtml(markdown: string): Promise<string> {
10
+ const result = await unified()
11
+ .use(remarkParse)
12
+ .use(remarkGfm)
13
+ .use(remarkRehype)
14
+ .use(rehypeStringify)
15
+ .process(markdown)
16
+ return String(result)
17
+ }
18
+
19
+ export function rewriteDocLinks(html: string): string {
20
+ return html.replace(/href="([^"]*\.(?:md|mdx))"/g, (match, href) => {
21
+ if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {
22
+ return match
23
+ }
24
+ return `href="${href.replace(/\.mdx?$/, '')}"`
25
+ })
26
+ }
27
+
28
+ export const tocPageStyle = `<style>
29
+ *, *::before, *::after { box-sizing: border-box; }
30
+ body {
31
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
32
+ max-width: 52rem;
33
+ margin: 0 auto;
34
+ padding: 2rem 1.5rem;
35
+ line-height: 1.7;
36
+ color: #1a1a2e;
37
+ background: #fafafa;
38
+ }
39
+ h1 { font-size: 2rem; font-weight: 600; margin: 2rem 0 1rem; color: #0f0f23; }
40
+ h2 { font-size: 1.5rem; font-weight: 600; margin: 2.5rem 0 0.75rem; color: #16163a; border-bottom: 1px solid #e2e2e8; padding-bottom: 0.4rem; }
41
+ a { color: #2563eb; text-decoration: none; }
42
+ a:hover { text-decoration: underline; }
43
+ ul { padding-left: 1.5rem; margin: 0.5rem 0 1rem; }
44
+ li { margin: 0.3rem 0; }
45
+ </style>`
46
+
47
+ export async function renderTocPage(collection: Collection): Promise<string> {
48
+ const tocMarkdown = collection.tableOfContents({ title: 'Table of Contents', basePath: '.' })
49
+ const tocHtml = rewriteDocLinks(await renderMarkdownToHtml(tocMarkdown))
50
+ return `<!DOCTYPE html>
51
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
52
+ <title>Table of Contents</title>
53
+ <link rel="preconnect" href="https://fonts.googleapis.com">
54
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
55
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
56
+ ${tocPageStyle}
57
+ </head>
58
+ <body>
59
+ ${tocHtml}
60
+ </body></html>`
61
+ }
62
+
63
+ export function resolveModelDef(collection: Collection, name: string) {
64
+ const lower = name.toLowerCase()
65
+ return collection.modelDefinitions.find(
66
+ (d: any) => d.name.toLowerCase() === lower || d.prefix.toLowerCase() === lower,
67
+ )
68
+ }
69
+
70
+ export function buildSchemaJSON(collection: Collection) {
71
+ const models: Record<string, any> = {}
72
+ for (const def of collection.modelDefinitions as any[]) {
73
+ const fields = introspectMetaSchema(def.meta)
74
+ const sections = Object.entries(def.sections || {}).map(([key, sec]: [string, any]) => ({
75
+ key,
76
+ heading: sec.heading,
77
+ alternatives: sec.alternatives || [],
78
+ hasSchema: !!sec.schema,
79
+ }))
80
+ const relationships = Object.entries(def.relationships || {}).map(([key, rel]: [string, any]) => ({
81
+ key,
82
+ type: rel.type,
83
+ model: rel.model,
84
+ }))
85
+ models[def.name] = {
86
+ name: def.name,
87
+ prefix: def.prefix,
88
+ description: def.description,
89
+ fields,
90
+ sections,
91
+ relationships,
92
+ computed: Object.keys(def.computed || {}),
93
+ scopes: Object.keys(def.scopes || {}),
94
+ }
95
+ }
96
+ return models
97
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ import { defineModel } from "./define-model";
3
+
4
+ /**
5
+ * Built-in Base model that serves as a catch-all for documents
6
+ * that don't match any other registered model.
7
+ * Auto-registered by Collection.load() unless overridden.
8
+ */
9
+ export const Base = defineModel("Base", {
10
+ prefix: "",
11
+ meta: z.looseObject({}),
12
+ });
@@ -1,44 +1,82 @@
1
- import { defineCommand } from "citty";
2
- import { loadCollection } from "../load-collection";
3
-
4
- export default defineCommand({
5
- meta: {
6
- name: "action",
7
- description: "Run a named action on the collection",
8
- },
9
- args: {
10
- name: {
11
- type: "positional",
12
- description: "Action name",
13
- required: true,
14
- },
15
- rootPath: {
16
- type: "string",
17
- description: "Root path for the collection",
18
- alias: "r",
19
- },
20
- },
21
- async run({ args }) {
22
- const collection = await loadCollection({
23
- rootPath: args.rootPath as string | undefined,
24
- });
25
-
26
- const actionName = args.name as string;
27
-
28
- if (!collection.actions.has(actionName)) {
29
- console.error(
30
- `Action "${actionName}" not found. Available: ${collection.availableActions.join(", ") || "(none)"}`
31
- );
32
- process.exit(1);
33
- }
34
-
35
- const result = await collection.runAction(actionName);
36
- if (result !== undefined) {
37
- console.log(
38
- typeof result === "string"
39
- ? result
40
- : JSON.stringify(result, null, 2)
41
- );
42
- }
43
- },
44
- });
1
+ import { z } from 'zod'
2
+ import { commands } from '../registry.js'
3
+ import { loadCollection } from '../load-collection.js'
4
+
5
+ const argsSchema = z.object({
6
+ contentFolder: z.string().optional(),
7
+ })
8
+
9
+ async function handler(options: z.infer<typeof argsSchema>, context: { container: any }) {
10
+ const collection = await loadCollection({
11
+ contentFolder: options.contentFolder,
12
+ })
13
+
14
+ const actionName = context.container.argv._[1] as string | undefined
15
+
16
+ if (!actionName) {
17
+ console.error('Usage: cnotes action <name>')
18
+ console.error(`Available: ${collection.availableActions.join(', ') || '(none)'}`)
19
+ process.exit(1)
20
+ }
21
+
22
+ if (!collection.actions.has(actionName)) {
23
+ console.error(
24
+ `Action "${actionName}" not found. Available: ${collection.availableActions.join(', ') || '(none)'}`
25
+ )
26
+ process.exit(1)
27
+ }
28
+
29
+ const result = await collection.runAction(actionName)
30
+ if (result !== undefined) {
31
+ console.log(
32
+ typeof result === 'string'
33
+ ? result
34
+ : JSON.stringify(result, null, 2)
35
+ )
36
+ }
37
+ }
38
+
39
+ commands.register('action', {
40
+ description: 'Run a named action on the collection',
41
+ help: `# cnotes action
42
+
43
+ Run a named action registered on the collection. Actions are custom functions defined in your collection's entry point.
44
+
45
+ ## Usage
46
+
47
+ \`\`\`
48
+ cnotes action <name> [options]
49
+ \`\`\`
50
+
51
+ ## Arguments
52
+
53
+ | Argument | Description |
54
+ |----------|-------------|
55
+ | \`name\` | The registered action name |
56
+
57
+ ## Options
58
+
59
+ | Option | Description |
60
+ |--------|-------------|
61
+ | \`--contentFolder\` | Path to content folder |
62
+
63
+ ## Output
64
+
65
+ If the action returns a value, it is printed to stdout (strings directly, objects as JSON).
66
+
67
+ ## Examples
68
+
69
+ \`\`\`bash
70
+ # Run an action
71
+ cnotes action generate-report
72
+
73
+ # Run an action on a specific content folder
74
+ cnotes action sync --contentFolder ./docs
75
+
76
+ # List available actions (shows error with available names)
77
+ cnotes action
78
+ \`\`\`
79
+ `,
80
+ argsSchema,
81
+ handler,
82
+ })
@@ -0,0 +1,124 @@
1
+ import { z } from 'zod'
2
+ import { commands } from '../registry.js'
3
+ import { loadCollection } from '../load-collection.js'
4
+
5
+ const argsSchema = z.object({
6
+ contentFolder: z.string().optional(),
7
+ })
8
+
9
+ async function handler(options: z.infer<typeof argsSchema>, context: { container: any }) {
10
+ const container = context.container
11
+ const ui = container.feature('ui')
12
+
13
+ const collection = await loadCollection({
14
+ contentFolder: options.contentFolder,
15
+ })
16
+
17
+ const featureContext: Record<string, any> = {
18
+ collection,
19
+ }
20
+
21
+ // Expose all container features
22
+ for (const name of container.features.available) {
23
+ try {
24
+ featureContext[name] = container.feature(name)
25
+ } catch {}
26
+ }
27
+
28
+ // Load user console module if present
29
+ const consoleModulePath = container.paths.resolve('cnotes.console.ts')
30
+ let consoleModuleLoaded = false
31
+ let consoleModuleError: Error | null = null
32
+
33
+ if (container.fs.exists(consoleModulePath)) {
34
+ try {
35
+ const vmFeature = container.feature('vm')
36
+ const userExports = vmFeature.loadModule(consoleModulePath, { container, console, collection })
37
+ Object.assign(featureContext, userExports)
38
+ consoleModuleLoaded = true
39
+ } catch (err: any) {
40
+ consoleModuleError = err
41
+ }
42
+ }
43
+
44
+ const prompt = ui.colors.cyan('cnotes') + ui.colors.dim(' > ')
45
+
46
+ console.log()
47
+ console.log(ui.colors.dim(' Contentbase REPL — collection and container features in scope. Tab to autocomplete.'))
48
+ if (consoleModuleLoaded) {
49
+ console.log(ui.colors.dim(' Loaded cnotes.console.ts exports into scope.'))
50
+ } else if (consoleModuleError) {
51
+ console.log(ui.colors.yellow(' Warning: Failed to load cnotes.console.ts:'))
52
+ console.log(ui.colors.yellow(` ${consoleModuleError.message}`))
53
+ console.log(ui.colors.dim(' The REPL will start without your custom exports.'))
54
+ }
55
+ console.log(ui.colors.dim(' Type .exit to quit.'))
56
+ console.log()
57
+
58
+ const repl = container.feature('repl', { prompt })
59
+ await repl.start({
60
+ context: {
61
+ ...featureContext,
62
+ console,
63
+ setTimeout,
64
+ setInterval,
65
+ clearTimeout,
66
+ clearInterval,
67
+ fetch,
68
+ Bun,
69
+ },
70
+ })
71
+ }
72
+
73
+ commands.register('console', {
74
+ description: 'Start an interactive REPL with collection and container features in scope',
75
+ help: `# cnotes console
76
+
77
+ Start an interactive REPL with the loaded collection, all container features, and optional user-defined exports in scope. Useful for exploring and debugging your content.
78
+
79
+ ## Usage
80
+
81
+ \`\`\`
82
+ cnotes console [options]
83
+ \`\`\`
84
+
85
+ ## Options
86
+
87
+ | Option | Description |
88
+ |--------|-------------|
89
+ | \`--contentFolder\` | Path to content folder |
90
+
91
+ ## Scope
92
+
93
+ The REPL has these variables available:
94
+
95
+ - \`collection\` — The loaded Collection instance
96
+ - All luca container features (fs, ui, grep, etc.)
97
+ - Exports from \`cnotes.console.ts\` if present in the project root
98
+
99
+ ## Custom Console Module
100
+
101
+ Create a \`cnotes.console.ts\` file in your project root to add custom helpers to the REPL scope:
102
+
103
+ \`\`\`typescript
104
+ export const myHelper = () => "hello from console"
105
+ \`\`\`
106
+
107
+ ## Examples
108
+
109
+ \`\`\`bash
110
+ # Start the REPL
111
+ cnotes console
112
+
113
+ # Start with a specific content folder
114
+ cnotes console --contentFolder ./docs
115
+
116
+ # Once inside the REPL:
117
+ # collection.available — list all document IDs
118
+ # collection.document('posts/hello') — load a document
119
+ # .exit — quit
120
+ \`\`\`
121
+ `,
122
+ argsSchema,
123
+ handler,
124
+ })
@@ -1,59 +1,185 @@
1
- import { defineCommand } from "citty";
2
- import fs from "fs/promises";
3
- import path from "path";
4
- import { loadCollection } from "../load-collection";
5
- import { kebabCase } from "../../utils/inflect";
6
-
7
- export default defineCommand({
8
- meta: {
9
- name: "create",
10
- description: "Create a new document for a model type",
11
- },
12
- args: {
13
- model: {
14
- type: "positional",
15
- description: "Model name",
16
- required: true,
17
- },
18
- title: {
19
- type: "string",
20
- description: "Document title",
21
- required: true,
22
- },
23
- rootPath: {
24
- type: "string",
25
- description: "Root path for the collection",
26
- alias: "r",
27
- },
28
- },
29
- async run({ args }) {
30
- const collection = await loadCollection({
31
- rootPath: args.rootPath as string | undefined,
32
- });
33
-
34
- const modelName = args.model as string;
35
- const title = args.title as string;
36
- const def = collection.getModelDefinition(modelName);
37
-
38
- if (!def) {
39
- console.error(
40
- `Model "${modelName}" not found. Available: ${collection.modelDefinitions.map((d) => d.name).join(", ")}`
41
- );
42
- process.exit(1);
1
+ import { z } from 'zod'
2
+ import fs from 'fs/promises'
3
+ import path from 'path'
4
+ import matter from 'gray-matter'
5
+ import { commands } from '../registry.js'
6
+ import { loadCollection } from '../load-collection.js'
7
+ import { kebabCase } from '../../utils/inflect.js'
8
+ import { introspectMetaSchema } from '../../collection.js'
9
+
10
+ const argsSchema = z.object({
11
+ title: z.string().optional(),
12
+ contentFolder: z.string().optional(),
13
+ })
14
+
15
+ async function handler(options: z.infer<typeof argsSchema>, context: { container: any }) {
16
+ const collection = await loadCollection({
17
+ contentFolder: options.contentFolder,
18
+ })
19
+
20
+ const modelName = context.container.argv._[1] as string | undefined
21
+ if (!modelName) {
22
+ console.error('Usage: cnotes create <model> --title "Document Title"')
23
+ process.exit(1)
24
+ }
25
+
26
+ const title = options.title
27
+ if (!title) {
28
+ console.error('--title is required')
29
+ process.exit(1)
30
+ }
31
+
32
+ const def =
33
+ collection.getModelDefinition(modelName) ??
34
+ collection.modelDefinitions.find(
35
+ (d) => d.name.toLowerCase() === modelName.toLowerCase()
36
+ )
37
+
38
+ if (!def) {
39
+ console.error(
40
+ `Model "${modelName}" not found. Available: ${collection.modelDefinitions.map((d) => d.name).join(', ')}`
41
+ )
42
+ process.exit(1)
43
+ }
44
+
45
+ // Parse --meta.* flags from raw argv
46
+ const metaOverrides: Record<string, unknown> = {}
47
+ const rawArgs = context.container.argv
48
+ for (const key of Object.keys(rawArgs)) {
49
+ if (key.startsWith('meta.')) {
50
+ metaOverrides[key.slice(5)] = rawArgs[key]
43
51
  }
52
+ }
53
+
54
+ // Build meta from priority layers: zod defaults < definition.defaults < template frontmatter < CLI overrides
55
+ const zodDefaults: Record<string, unknown> = {}
56
+ for (const field of introspectMetaSchema(def.meta)) {
57
+ if (field.defaultValue !== undefined) {
58
+ zodDefaults[field.name] = field.defaultValue
59
+ }
60
+ }
61
+
62
+ const definitionDefaults: Record<string, unknown> = def.defaults ?? {}
44
63
 
45
- const slug = kebabCase(title.toLowerCase());
46
- const pathId = `${def.prefix}/${slug}`;
47
- const filePath = path.resolve(
64
+ // Template lookup
65
+ const templateExtensions = ['md', 'mdx']
66
+ let templateContent: string | null = null
67
+
68
+ for (const ext of templateExtensions) {
69
+ const templatePath = path.resolve(
48
70
  collection.rootPath,
49
- `${pathId}.mdx`
50
- );
71
+ 'templates',
72
+ `${modelName.toLowerCase()}.${ext}`
73
+ )
74
+ try {
75
+ templateContent = await fs.readFile(templatePath, 'utf8')
76
+ break
77
+ } catch {
78
+ // template not found, try next extension
79
+ }
80
+ }
81
+
82
+ let content: string
83
+
84
+ if (templateContent) {
85
+ const parsed = matter(templateContent)
86
+
87
+ const mergedMeta = {
88
+ ...zodDefaults,
89
+ ...definitionDefaults,
90
+ ...parsed.data,
91
+ ...metaOverrides,
92
+ }
93
+
94
+ const body = parsed.content.replace(/^# .+$/m, `# ${title}`)
95
+ content = matter.stringify(body, mergedMeta)
96
+ } else {
97
+ const mergedMeta = {
98
+ ...zodDefaults,
99
+ ...definitionDefaults,
100
+ ...metaOverrides,
101
+ }
102
+
103
+ const lines: string[] = []
104
+ lines.push(`# ${title}`)
105
+ lines.push('')
106
+
107
+ const sections = def.sections ?? {}
108
+ for (const [, sec] of Object.entries(sections)) {
109
+ const s = sec as any
110
+ lines.push(`## ${s.heading}`)
111
+ lines.push('')
112
+ if (s.schema?.description) {
113
+ lines.push(s.schema.description)
114
+ lines.push('')
115
+ }
116
+ }
117
+
118
+ content = matter.stringify(lines.join('\n'), mergedMeta)
119
+ }
120
+
121
+ const slug = kebabCase(title.toLowerCase())
122
+ const pathId = `${def.prefix}/${slug}`
123
+ const filePath = path.resolve(collection.rootPath, `${pathId}.md`)
124
+
125
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
126
+ await fs.writeFile(filePath, content, 'utf8')
127
+
128
+ console.log(`Created ${filePath}`)
129
+ }
130
+
131
+ commands.register('create', {
132
+ description: 'Create a new document for a model type',
133
+ help: `# cnotes create
134
+
135
+ Create a new document for a registered model, with proper frontmatter defaults and section scaffolding.
136
+
137
+ ## Usage
138
+
139
+ \`\`\`
140
+ cnotes create <model> --title "Document Title" [options]
141
+ \`\`\`
142
+
143
+ ## Arguments
144
+
145
+ | Argument | Description |
146
+ |----------|-------------|
147
+ | \`model\` | Model name (e.g. \`Post\`, \`Epic\`, \`Task\`) |
148
+
149
+ ## Options
150
+
151
+ | Option | Description |
152
+ |--------|-------------|
153
+ | \`--title\` | **Required.** Title for the new document (used as H1 and slug) |
154
+ | \`--meta.*\` | Set frontmatter fields (e.g. \`--meta.status active\`) |
155
+ | \`--contentFolder\` | Path to content folder |
156
+
157
+ ## Template Lookup
158
+
159
+ If \`templates/<model>.md\` (or \`.mdx\`) exists in the content root, it will be used as the document scaffold. Template frontmatter is merged with schema defaults and CLI overrides.
160
+
161
+ ## Meta Priority
162
+
163
+ Frontmatter values are merged in this order (last wins):
164
+
165
+ 1. Zod schema defaults
166
+ 2. Model definition defaults
167
+ 3. Template frontmatter
168
+ 4. CLI \`--meta.*\` overrides
169
+
170
+ ## Examples
51
171
 
52
- const content = `---\n---\n\n# ${title}\n`;
172
+ \`\`\`bash
173
+ # Create a new post
174
+ cnotes create Post --title "My First Post"
53
175
 
54
- await fs.mkdir(path.dirname(filePath), { recursive: true });
55
- await fs.writeFile(filePath, content, "utf8");
176
+ # Create with meta overrides
177
+ cnotes create Task --title "Fix login bug" --meta.status active --meta.priority high
56
178
 
57
- console.log(`Created ${filePath}`);
58
- },
59
- });
179
+ # Create from a different content folder
180
+ cnotes create Epic --title "Auth System" --contentFolder ./docs
181
+ \`\`\`
182
+ `,
183
+ argsSchema,
184
+ handler,
185
+ })