contentbase 0.0.2 → 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 (85) 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 +289 -13
  7. package/bun.lock +43 -4
  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 +12 -3
  14. package/public/web-demo/index.html +813 -0
  15. package/showcases/node_modules/.cache/.repl_history +3 -0
  16. package/showcases/vinyl-collection/artists/nina-simone.mdx +1 -1
  17. package/src/__tests__/semantic-search.integration.test.ts +284 -0
  18. package/src/api/endpoints/actions.ts +34 -0
  19. package/src/api/endpoints/doc.ts +187 -0
  20. package/src/api/endpoints/docs-index.ts +26 -0
  21. package/src/api/endpoints/document.ts +171 -0
  22. package/src/api/endpoints/documents.ts +71 -0
  23. package/src/api/endpoints/inspect.ts +16 -0
  24. package/src/api/endpoints/models.ts +10 -0
  25. package/src/api/endpoints/query.ts +88 -0
  26. package/src/api/endpoints/root.ts +7 -0
  27. package/src/api/endpoints/search-reindex.ts +65 -0
  28. package/src/api/endpoints/search-status.ts +55 -0
  29. package/src/api/endpoints/search.ts +104 -0
  30. package/src/api/endpoints/semantic-search.ts +120 -0
  31. package/src/api/endpoints/text-search.ts +63 -0
  32. package/src/api/endpoints/validate.ts +34 -0
  33. package/src/api/helpers.ts +97 -0
  34. package/src/base-model.ts +12 -0
  35. package/src/cli/commands/action.ts +82 -44
  36. package/src/cli/commands/console.ts +124 -0
  37. package/src/cli/commands/create.ts +179 -53
  38. package/src/cli/commands/embed.ts +323 -0
  39. package/src/cli/commands/export.ts +58 -24
  40. package/src/cli/commands/extract.ts +174 -0
  41. package/src/cli/commands/help.ts +81 -0
  42. package/src/cli/commands/index.ts +17 -0
  43. package/src/cli/commands/init.ts +72 -48
  44. package/src/cli/commands/inspect.ts +56 -46
  45. package/src/cli/commands/mcp.ts +1219 -0
  46. package/src/cli/commands/search.ts +285 -0
  47. package/src/cli/commands/serve.ts +348 -0
  48. package/src/cli/commands/summary.ts +60 -0
  49. package/src/cli/commands/teach.ts +86 -0
  50. package/src/cli/commands/text-search.ts +134 -0
  51. package/src/cli/commands/validate.ts +126 -64
  52. package/src/cli/index.ts +88 -19
  53. package/src/cli/load-collection.ts +144 -17
  54. package/src/cli/registry.ts +28 -0
  55. package/src/collection.ts +361 -10
  56. package/src/define-model.ts +101 -4
  57. package/src/document.ts +47 -1
  58. package/src/extract-sections.ts +1 -1
  59. package/src/index.ts +14 -2
  60. package/src/model-instance.ts +35 -5
  61. package/src/node-shortcuts.ts +1 -1
  62. package/src/query/collection-query.ts +96 -9
  63. package/src/query/index.ts +7 -0
  64. package/src/query/query-dsl.ts +259 -0
  65. package/src/relationships/has-many.ts +39 -0
  66. package/src/relationships/index.ts +7 -2
  67. package/src/section.ts +2 -0
  68. package/src/types.ts +23 -1
  69. package/src/utils/index.ts +1 -0
  70. package/src/utils/match-pattern.ts +65 -0
  71. package/src/validator.ts +18 -1
  72. package/test/collection.test.ts +118 -2
  73. package/test/fixtures/sdlc/MODELS.md +106 -0
  74. package/test/fixtures/sdlc/SKILL.md +404 -0
  75. package/test/fixtures/sdlc/index.ts +9 -0
  76. package/test/fixtures/sdlc/models.ts +8 -6
  77. package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +20 -0
  78. package/test/fixtures/sdlc/templates/epic.md +23 -0
  79. package/test/fixtures/sdlc/templates/story.md +19 -0
  80. package/test/pattern.test.ts +191 -0
  81. package/test/query-dsl.test.ts +431 -0
  82. package/test/query.test.ts +29 -0
  83. package/test/relationships.test.ts +130 -0
  84. package/test/section.test.ts +61 -0
  85. package/test/table-of-contents.test.ts +49 -5
@@ -0,0 +1,174 @@
1
+ import { z } from 'zod'
2
+ import picomatch from 'picomatch'
3
+ import yaml from 'js-yaml'
4
+ import { commands } from '../registry.js'
5
+ import { loadCollection } from '../load-collection.js'
6
+ import { stringifyAst } from '../../utils/stringify-ast.js'
7
+ import type { Root, RootContent, Heading } from 'mdast'
8
+
9
+ const argsSchema = z.object({
10
+ sections: z.string().optional(),
11
+ s: z.string().optional(),
12
+ title: z.string().optional(),
13
+ t: z.string().optional(),
14
+ frontmatter: z.boolean().default(false),
15
+ noNormalizeHeadings: z.boolean().default(false),
16
+ contentFolder: z.string().optional(),
17
+ })
18
+
19
+ async function handler(options: z.infer<typeof argsSchema>, context: { container: any }) {
20
+ const collection = await loadCollection({
21
+ contentFolder: options.contentFolder,
22
+ })
23
+
24
+ const target = context.container.argv._[1] as string | undefined
25
+ if (!target) {
26
+ console.error('Usage: cnotes extract <target> --sections "Section1,Section2"')
27
+ process.exit(1)
28
+ }
29
+
30
+ const sectionsArg = options.sections || options.s
31
+ if (!sectionsArg) {
32
+ console.error('--sections is required')
33
+ process.exit(1)
34
+ }
35
+
36
+ const sectionNames = sectionsArg
37
+ .split(',')
38
+ .map((s) => s.trim())
39
+ .filter(Boolean)
40
+ const includeFrontmatter = options.frontmatter
41
+ const noNormalize = options.noNormalizeHeadings
42
+ const title = options.title || options.t
43
+
44
+ const isMatch = picomatch(target)
45
+ const matchingIds = collection.available.filter((id) => isMatch(id))
46
+
47
+ if (matchingIds.length === 0) {
48
+ console.error(`No documents matched: ${target}`)
49
+ process.exit(1)
50
+ }
51
+
52
+ const allNodes: RootContent[] = []
53
+
54
+ if (title) {
55
+ allNodes.push({
56
+ type: 'heading',
57
+ depth: 1,
58
+ children: [{ type: 'text', value: title }],
59
+ } as Heading)
60
+ }
61
+
62
+ for (const id of matchingIds) {
63
+ const doc = collection.document(id)
64
+
65
+ if (includeFrontmatter && Object.keys(doc.meta).length > 0) {
66
+ allNodes.push({
67
+ type: 'yaml',
68
+ value: yaml.dump(doc.meta).trim(),
69
+ } as any)
70
+ }
71
+
72
+ const docNodes: RootContent[] = []
73
+
74
+ const titleNode = doc.nodes.firstHeading
75
+ if (titleNode) {
76
+ docNodes.push(titleNode as RootContent)
77
+ }
78
+
79
+ const leading = doc.nodes.leadingElementsAfterTitle
80
+ if (leading.length > 0) {
81
+ docNodes.push(...(leading as RootContent[]))
82
+ }
83
+
84
+ for (const name of sectionNames) {
85
+ try {
86
+ const sectionNodes = doc.extractSection(name)
87
+ if (sectionNodes.length > 0) {
88
+ docNodes.push(...(sectionNodes as RootContent[]))
89
+ }
90
+ } catch {
91
+ // Section not found — skip
92
+ }
93
+ }
94
+
95
+ if (!noNormalize && docNodes.length > 0) {
96
+ const headings = docNodes.filter(
97
+ (n): n is Heading => n.type === 'heading'
98
+ )
99
+ if (headings.length > 0) {
100
+ const minDepth = Math.min(...headings.map((h) => h.depth))
101
+ const targetDepth = title ? 2 : 1
102
+ const shift = targetDepth - minDepth
103
+ if (shift !== 0) {
104
+ for (const h of headings) {
105
+ h.depth = Math.max(1, Math.min(6, h.depth + shift)) as
106
+ | 1
107
+ | 2
108
+ | 3
109
+ | 4
110
+ | 5
111
+ | 6
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ allNodes.push(...docNodes)
118
+ }
119
+
120
+ const combinedAst: Root = { type: 'root', children: allNodes }
121
+ console.log(stringifyAst(combinedAst).trim())
122
+ }
123
+
124
+ commands.register('extract', {
125
+ description: 'Extract specific sections from documents',
126
+ help: `# cnotes extract
127
+
128
+ Extract specific sections from one or more documents. Supports glob patterns for matching multiple documents and outputs clean markdown with normalized headings.
129
+
130
+ ## Usage
131
+
132
+ \`\`\`
133
+ cnotes extract <target> --sections "Section1,Section2" [options]
134
+ \`\`\`
135
+
136
+ ## Arguments
137
+
138
+ | Argument | Description |
139
+ |----------|-------------|
140
+ | \`target\` | Path ID or glob pattern (e.g. \`epics/*\`, \`tasks/auth-*\`) |
141
+
142
+ ## Options
143
+
144
+ | Option | Alias | Description |
145
+ |--------|-------|-------------|
146
+ | \`--sections\` | \`-s\` | **Required.** Comma-separated section headings to extract |
147
+ | \`--title\` | \`-t\` | Add a top-level H1 title to the output |
148
+ | \`--frontmatter\` | | Include YAML frontmatter in the output |
149
+ | \`--noNormalizeHeadings\` | | Skip heading depth normalization |
150
+ | \`--contentFolder\` | | Path to content folder |
151
+
152
+ ## Heading Normalization
153
+
154
+ By default, extracted headings are re-leveled so the output starts at H1 (or H2 when \`--title\` is used). Use \`--noNormalizeHeadings\` to preserve original depths.
155
+
156
+ ## Examples
157
+
158
+ \`\`\`bash
159
+ # Extract Overview from a single document
160
+ cnotes extract epics/auth-system --sections "Overview"
161
+
162
+ # Extract multiple sections
163
+ cnotes extract tasks/login-bug -s "Overview,Requirements,Acceptance Criteria"
164
+
165
+ # Extract from all epics with a title wrapper
166
+ cnotes extract "epics/*" -s "Overview" -t "Epic Summaries"
167
+
168
+ # Include frontmatter in output
169
+ cnotes extract epics/auth-system -s "Overview" --frontmatter
170
+ \`\`\`
171
+ `,
172
+ argsSchema,
173
+ handler,
174
+ })
@@ -0,0 +1,81 @@
1
+ import { commands } from '../registry.js'
2
+
3
+ async function handler(_options: any, context: { container: any }) {
4
+ const ui = context.container.feature('ui')
5
+
6
+ const lines: string[] = [
7
+ '# cnotes',
8
+ '',
9
+ 'An ORM for structured Markdown/MDX files.',
10
+ '',
11
+ '## Usage',
12
+ '',
13
+ '```',
14
+ 'cnotes <command> [options]',
15
+ '```',
16
+ '',
17
+ '## Commands',
18
+ '',
19
+ '| Command | Description |',
20
+ '|---------|-------------|',
21
+ ]
22
+
23
+ for (const name of commands.available) {
24
+ const def = commands.get(name)!
25
+ lines.push(`| \`${name}\` | ${def.description} |`)
26
+ }
27
+
28
+ lines.push(
29
+ '',
30
+ '## Global Options',
31
+ '',
32
+ '| Option | Description |',
33
+ '|--------|-------------|',
34
+ '| `--help`, `-h` | Show help for a command |',
35
+ '| `--contentFolder` | Path to content folder (most commands) |',
36
+ '',
37
+ '## Getting Help',
38
+ '',
39
+ '```bash',
40
+ '# Show this overview',
41
+ 'cnotes help',
42
+ '',
43
+ '# Show detailed help for a command',
44
+ 'cnotes help serve',
45
+ 'cnotes serve --help',
46
+ '```',
47
+ )
48
+
49
+ console.log(ui.markdown(lines.join('\n')))
50
+ }
51
+
52
+ commands.register('help', {
53
+ description: 'Show available commands',
54
+ help: `# cnotes help
55
+
56
+ Show available commands and usage information.
57
+
58
+ ## Usage
59
+
60
+ \`\`\`
61
+ cnotes help [command]
62
+ \`\`\`
63
+
64
+ ## Arguments
65
+
66
+ | Argument | Description |
67
+ |----------|-------------|
68
+ | \`command\` | Show detailed help for a specific command |
69
+
70
+ ## Examples
71
+
72
+ \`\`\`bash
73
+ # Show all commands
74
+ cnotes help
75
+
76
+ # Show help for the serve command
77
+ cnotes help serve
78
+ \`\`\`
79
+ `,
80
+ handler,
81
+ })
@@ -0,0 +1,17 @@
1
+ // Side-effect imports register each command
2
+ import './init.js'
3
+ import './create.js'
4
+ import './inspect.js'
5
+ import './validate.js'
6
+ import './export.js'
7
+ import './action.js'
8
+ import './summary.js'
9
+ import './teach.js'
10
+ import './extract.js'
11
+ import './mcp.js'
12
+ import './serve.js'
13
+ import './console.js'
14
+ import './help.js'
15
+ import './text-search.js'
16
+ import './search.js'
17
+ import './embed.js'
@@ -1,30 +1,17 @@
1
- import { defineCommand } from "citty";
2
- import fs from "fs/promises";
3
- import path from "path";
4
-
5
- export default defineCommand({
6
- meta: {
7
- name: "init",
8
- description: "Initialize a new contentbase project",
9
- },
10
- args: {
11
- name: {
12
- type: "positional",
13
- description: "Project name",
14
- required: false,
15
- },
16
- },
17
- async run({ args }) {
18
- const name = (args.name as string) || "my-content";
19
- const dir = path.resolve(process.cwd(), name);
20
-
21
- await fs.mkdir(dir, { recursive: true });
22
- await fs.mkdir(path.join(dir, "posts"), { recursive: true });
23
-
24
- // Create a sample model file
25
- await fs.writeFile(
26
- path.join(dir, "models.ts"),
27
- `import { defineModel, z } from "contentbase";
1
+ import fs from 'fs/promises'
2
+ import path from 'path'
3
+ import { commands } from '../registry.js'
4
+
5
+ async function handler(_options: any, context: { container: any }) {
6
+ const name = (context.container.argv._[1] as string) || 'my-content'
7
+ const dir = path.resolve(process.cwd(), name)
8
+
9
+ await fs.mkdir(dir, { recursive: true })
10
+ await fs.mkdir(path.join(dir, 'posts'), { recursive: true })
11
+
12
+ await fs.writeFile(
13
+ path.join(dir, 'models.ts'),
14
+ `import { defineModel, z } from "contentbase";
28
15
 
29
16
  export const Post = defineModel("Post", {
30
17
  prefix: "posts",
@@ -34,13 +21,12 @@ export const Post = defineModel("Post", {
34
21
  }),
35
22
  });
36
23
  `,
37
- "utf8"
38
- );
24
+ 'utf8'
25
+ )
39
26
 
40
- // Create a sample post
41
- await fs.writeFile(
42
- path.join(dir, "posts", "hello-world.mdx"),
43
- `---
27
+ await fs.writeFile(
28
+ path.join(dir, 'posts', 'hello-world.md'),
29
+ `---
44
30
  status: draft
45
31
  author: me
46
32
  ---
@@ -49,13 +35,12 @@ author: me
49
35
 
50
36
  Welcome to your contentbase project!
51
37
  `,
52
- "utf8"
53
- );
38
+ 'utf8'
39
+ )
54
40
 
55
- // Create index.ts
56
- await fs.writeFile(
57
- path.join(dir, "index.ts"),
58
- `import { Collection } from "contentbase";
41
+ await fs.writeFile(
42
+ path.join(dir, 'index.ts'),
43
+ `import { Collection } from "contentbase";
59
44
  import { Post } from "./models";
60
45
 
61
46
  export const collection = new Collection({
@@ -64,12 +49,51 @@ export const collection = new Collection({
64
49
 
65
50
  collection.register(Post);
66
51
  `,
67
- "utf8"
68
- );
69
-
70
- console.log(`Created contentbase project at ${dir}`);
71
- console.log(` ${name}/models.ts`);
72
- console.log(` ${name}/index.ts`);
73
- console.log(` ${name}/posts/hello-world.mdx`);
74
- },
75
- });
52
+ 'utf8'
53
+ )
54
+
55
+ console.log(`Created contentbase project at ${dir}`)
56
+ console.log(` ${name}/models.ts`)
57
+ console.log(` ${name}/index.ts`)
58
+ console.log(` ${name}/posts/hello-world.md`)
59
+ }
60
+
61
+ commands.register('init', {
62
+ description: 'Initialize a new contentbase project',
63
+ help: `# cnotes init
64
+
65
+ Scaffold a new contentbase project with a sample model and document.
66
+
67
+ ## Usage
68
+
69
+ \`\`\`
70
+ cnotes init [name]
71
+ \`\`\`
72
+
73
+ ## Arguments
74
+
75
+ | Argument | Description | Default |
76
+ |----------|-------------|---------|
77
+ | \`name\` | Directory name for the project | \`my-content\` |
78
+
79
+ ## What It Creates
80
+
81
+ - \`<name>/models.ts\` — Model definitions with a sample Post model
82
+ - \`<name>/index.ts\` — Collection entry point
83
+ - \`<name>/posts/hello-world.md\` — Sample document
84
+
85
+ ## Examples
86
+
87
+ \`\`\`bash
88
+ # Create with default name
89
+ cnotes init
90
+
91
+ # Create with custom name
92
+ cnotes init docs
93
+
94
+ # Create in a subdirectory
95
+ cnotes init content/blog
96
+ \`\`\`
97
+ `,
98
+ handler,
99
+ })
@@ -1,46 +1,56 @@
1
- import { defineCommand } from "citty";
2
- import { loadCollection } from "../load-collection";
3
-
4
- export default defineCommand({
5
- meta: {
6
- name: "inspect",
7
- description: "Display collection info and registered models",
8
- },
9
- args: {
10
- rootPath: {
11
- type: "string",
12
- description: "Root path for the collection",
13
- alias: "r",
14
- },
15
- },
16
- async run({ args }) {
17
- const collection = await loadCollection({
18
- rootPath: args.rootPath as string | undefined,
19
- });
20
-
21
- console.log(`Collection: ${collection.name}`);
22
- console.log(`Root: ${collection.rootPath}`);
23
- console.log(`Items: ${collection.available.length}`);
24
- console.log();
25
-
26
- for (const def of collection.modelDefinitions) {
27
- const matchingItems = collection.available.filter((id) =>
28
- id.startsWith(def.prefix)
29
- );
30
- console.log(` Model: ${def.name}`);
31
- console.log(` Prefix: ${def.prefix}`);
32
- console.log(
33
- ` Sections: ${Object.keys(def.sections).join(", ") || "(none)"}`
34
- );
35
- console.log(
36
- ` Relationships: ${Object.keys(def.relationships).join(", ") || "(none)"}`
37
- );
38
- console.log(` Documents: ${matchingItems.length}`);
39
- console.log();
40
- }
41
-
42
- if (collection.availableActions.length > 0) {
43
- console.log(`Actions: ${collection.availableActions.join(", ")}`);
44
- }
45
- },
46
- });
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>) {
10
+ const collection = await loadCollection({
11
+ contentFolder: options.contentFolder,
12
+ })
13
+
14
+ console.log(collection.generateModelSummary())
15
+ }
16
+
17
+ commands.register('inspect', {
18
+ description: 'Display collection info and registered models',
19
+ help: `# cnotes inspect
20
+
21
+ Display a summary of the collection: registered models, their fields, sections, relationships, and document counts.
22
+
23
+ ## Usage
24
+
25
+ \`\`\`
26
+ cnotes inspect [options]
27
+ \`\`\`
28
+
29
+ ## Options
30
+
31
+ | Option | Description |
32
+ |--------|-------------|
33
+ | \`--contentFolder\` | Path to content folder |
34
+
35
+ ## Output
36
+
37
+ Shows for each model:
38
+ - Prefix and document count
39
+ - Meta field definitions (name, type, required, default)
40
+ - Section headings
41
+ - Relationships (belongsTo, hasMany)
42
+ - Computed properties and named scopes
43
+
44
+ ## Examples
45
+
46
+ \`\`\`bash
47
+ # Inspect current directory's collection
48
+ cnotes inspect
49
+
50
+ # Inspect a specific content folder
51
+ cnotes inspect --contentFolder test/fixtures/sdlc
52
+ \`\`\`
53
+ `,
54
+ argsSchema,
55
+ handler,
56
+ })