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,86 @@
1
+ import { z } from 'zod'
2
+ import fs from 'fs/promises'
3
+ import path from 'path'
4
+ import { commands } from '../registry.js'
5
+ import { loadCollection } from '../load-collection.js'
6
+
7
+ const argsSchema = z.object({
8
+ contentFolder: z.string().optional(),
9
+ })
10
+
11
+ async function handler(options: z.infer<typeof argsSchema>) {
12
+ const collection = await loadCollection({
13
+ contentFolder: options.contentFolder,
14
+ })
15
+
16
+ const modelsSummary = collection.generateModelSummary()
17
+ const toc = collection.tableOfContents({ title: 'Table of Contents' })
18
+
19
+ // Read the bundled static docs from the contentbase package
20
+ const packageRoot = path.resolve(import.meta.dir, '../../..')
21
+ const primer = await fs.readFile(path.join(packageRoot, 'PRIMER.md'), 'utf8')
22
+ const cli = await fs.readFile(path.join(packageRoot, 'CLI.md'), 'utf8')
23
+
24
+ const output = [
25
+ modelsSummary.trimEnd(),
26
+ '',
27
+ '---',
28
+ '',
29
+ toc.trimEnd(),
30
+ '',
31
+ '---',
32
+ '',
33
+ cli.trimEnd(),
34
+ '',
35
+ '---',
36
+ '',
37
+ primer.trimEnd(),
38
+ '',
39
+ ].join('\n')
40
+
41
+ console.log(output)
42
+ }
43
+
44
+ commands.register('teach', {
45
+ description: 'Output combined documentation (README.md + TOC + CLI.md + PRIMER.md) for LLM context',
46
+ help: `# cnotes teach
47
+
48
+ Output combined documentation suitable for feeding to an LLM as context. Concatenates the models summary, table of contents, CLI reference, and API primer into a single stream.
49
+
50
+ ## Usage
51
+
52
+ \`\`\`
53
+ cnotes teach [options]
54
+ \`\`\`
55
+
56
+ ## Options
57
+
58
+ | Option | Description |
59
+ |--------|-------------|
60
+ | \`--contentFolder\` | Path to content folder |
61
+
62
+ ## Output
63
+
64
+ Writes to stdout in this order:
65
+
66
+ 1. **README.md** — Model definitions summary
67
+ 2. **TABLE-OF-CONTENTS.md** — Document listing
68
+ 3. **CLI.md** — CLI reference
69
+ 4. **PRIMER.md** — API primer
70
+
71
+ ## Examples
72
+
73
+ \`\`\`bash
74
+ # Print full documentation
75
+ cnotes teach
76
+
77
+ # Save to a file for LLM context
78
+ cnotes teach > context.md
79
+
80
+ # Teach from a specific content folder
81
+ cnotes teach --contentFolder ./docs
82
+ \`\`\`
83
+ `,
84
+ argsSchema,
85
+ handler,
86
+ })
@@ -0,0 +1,134 @@
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
+ include: z.string().optional(),
7
+ exclude: z.string().optional(),
8
+ ignoreCase: z.boolean().default(false),
9
+ expanded: z.boolean().default(false),
10
+ maxResults: z.number().optional(),
11
+ contentFolder: z.string().optional(),
12
+ })
13
+
14
+ async function handler(options: z.infer<typeof argsSchema>, { container }: { container: any }) {
15
+ const pattern = container.argv._[1] as string | undefined
16
+
17
+ if (!pattern) {
18
+ console.error('Usage: cnotes text-search <pattern> [options]')
19
+ console.error(' --expanded Show line-level matches (default: files only)')
20
+ console.error(' --include Glob filter (e.g. "*.md")')
21
+ console.error(' --exclude Glob filter')
22
+ console.error(' --ignoreCase Case insensitive search')
23
+ console.error(' --maxResults Limit number of results')
24
+ process.exit(1)
25
+ }
26
+
27
+ const collection = await loadCollection({
28
+ contentFolder: options.contentFolder,
29
+ })
30
+
31
+ const grep = container.feature('grep')
32
+ const searchPath = collection.rootPath
33
+
34
+ const grepOptions: any = {
35
+ pattern,
36
+ path: searchPath,
37
+ ignoreCase: options.ignoreCase,
38
+ maxResults: options.maxResults,
39
+ include: options.include,
40
+ exclude: options.exclude,
41
+ }
42
+
43
+ if (!options.expanded) {
44
+ const files = await grep.filesContaining(pattern, {
45
+ path: searchPath,
46
+ ignoreCase: options.ignoreCase,
47
+ maxResults: options.maxResults,
48
+ include: options.include,
49
+ exclude: options.exclude,
50
+ })
51
+
52
+ if (files.length === 0) {
53
+ console.log('No matches found.')
54
+ return
55
+ }
56
+
57
+ console.log(`${files.length} file(s) match:\n`)
58
+ for (const file of files) {
59
+ console.log(` ${file}`)
60
+ }
61
+ } else {
62
+ const results = await grep.search(grepOptions)
63
+
64
+ if (results.length === 0) {
65
+ console.log('No matches found.')
66
+ return
67
+ }
68
+
69
+ // Group by file
70
+ const grouped = new Map<string, typeof results>()
71
+ for (const match of results) {
72
+ if (!grouped.has(match.file)) grouped.set(match.file, [])
73
+ grouped.get(match.file)!.push(match)
74
+ }
75
+
76
+ console.log(`${grouped.size} file(s), ${results.length} match(es):\n`)
77
+ for (const [file, matches] of grouped) {
78
+ console.log(` ${file}`)
79
+ for (const m of matches) {
80
+ console.log(` ${m.line}: ${m.content}`)
81
+ }
82
+ console.log()
83
+ }
84
+ }
85
+ }
86
+
87
+ commands.register('text-search', {
88
+ description: 'Search file contents with pattern matching',
89
+ help: `# cnotes text-search
90
+
91
+ Search file contents within the collection using ripgrep. Returns matching files by default, or line-level detail with \`--expanded\`.
92
+
93
+ ## Usage
94
+
95
+ \`\`\`
96
+ cnotes text-search <pattern> [options]
97
+ \`\`\`
98
+
99
+ ## Arguments
100
+
101
+ | Argument | Description |
102
+ |----------|-------------|
103
+ | \`pattern\` | Text or regex pattern to search for |
104
+
105
+ ## Options
106
+
107
+ | Option | Default | Description |
108
+ |--------|---------|-------------|
109
+ | \`--expanded\` | \`false\` | Show line-level matches instead of just file paths |
110
+ | \`--include\` | | Glob filter for file types (e.g. \`"*.md"\`) |
111
+ | \`--exclude\` | | Glob filter to exclude (e.g. \`"node_modules"\`) |
112
+ | \`--ignoreCase\` | \`false\` | Case-insensitive matching |
113
+ | \`--maxResults\` | | Limit number of results |
114
+ | \`--contentFolder\` | | Path to content folder |
115
+
116
+ ## Examples
117
+
118
+ \`\`\`bash
119
+ # Find files containing "authentication"
120
+ cnotes text-search authentication
121
+
122
+ # Case-insensitive search with line details
123
+ cnotes text-search "TODO" --ignoreCase --expanded
124
+
125
+ # Search only markdown files
126
+ cnotes text-search "status: draft" --include "*.md"
127
+
128
+ # Limit results
129
+ cnotes text-search "import" --maxResults 10 --expanded
130
+ \`\`\`
131
+ `,
132
+ argsSchema,
133
+ handler,
134
+ })
@@ -1,75 +1,137 @@
1
- import { defineCommand } from "citty";
2
- import { loadCollection } from "../load-collection";
3
- import { validateDocument } from "../../validator";
4
-
5
- export default defineCommand({
6
- meta: {
7
- name: "validate",
8
- description: "Validate documents against their model schemas",
9
- },
10
- args: {
11
- target: {
12
- type: "positional",
13
- description: "Path ID, model name, or 'all'",
14
- required: false,
15
- },
16
- rootPath: {
17
- type: "string",
18
- description: "Root path for the collection",
19
- alias: "r",
20
- },
21
- },
22
- async run({ args }) {
23
- const collection = await loadCollection({
24
- rootPath: args.rootPath as string | undefined,
25
- });
26
-
27
- const target = (args.target as string) || "all";
28
- let pathIds: string[];
29
-
30
- if (target === "all") {
31
- pathIds = collection.available;
32
- } else if (collection.items.has(target)) {
33
- pathIds = [target];
1
+ import { z } from 'zod'
2
+ import { commands } from '../registry.js'
3
+ import { loadCollection } from '../load-collection.js'
4
+ import { validateDocument } from '../../validator.js'
5
+
6
+ const argsSchema = z.object({
7
+ contentFolder: z.string().optional(),
8
+ setDefaultMeta: z.boolean().default(false),
9
+ })
10
+
11
+ async function handler(options: z.infer<typeof argsSchema>, context: { container: any }) {
12
+ const collection = await loadCollection({
13
+ contentFolder: options.contentFolder,
14
+ })
15
+
16
+ const setDefaultMeta = options.setDefaultMeta
17
+ const target = (context.container.argv._[1] as string) || 'all'
18
+ let pathIds: string[]
19
+
20
+ if (target === 'all') {
21
+ pathIds = collection.available
22
+ } else if (collection.items.has(target)) {
23
+ pathIds = [target]
24
+ } else {
25
+ const def = collection.getModelDefinition(target)
26
+ if (def) {
27
+ pathIds = collection.available.filter((id) =>
28
+ id.startsWith(def.prefix)
29
+ )
34
30
  } else {
35
- // Try to match as model name
36
- const def = collection.getModelDefinition(target);
37
- if (def) {
38
- pathIds = collection.available.filter((id) =>
39
- id.startsWith(def.prefix)
40
- );
41
- } else {
42
- console.error(`Not found: "${target}"`);
43
- process.exit(1);
44
- }
31
+ console.error(`Not found: "${target}"`)
32
+ process.exit(1)
45
33
  }
34
+ }
46
35
 
47
- let valid = 0;
48
- let invalid = 0;
36
+ let valid = 0
37
+ let invalid = 0
38
+ let updated = 0
49
39
 
50
- for (const pathId of pathIds) {
51
- const def = collection.findModelDefinition(pathId);
52
- if (!def) continue;
40
+ for (const pathId of pathIds) {
41
+ const def = collection.findModelDefinition(pathId)
42
+ if (!def) continue
53
43
 
54
- const doc = collection.document(pathId);
55
- const result = validateDocument(doc, def);
44
+ const doc = collection.document(pathId)
56
45
 
57
- if (result.valid) {
58
- valid++;
59
- } else {
60
- invalid++;
61
- console.log(`INVALID: ${pathId}`);
62
- for (const error of result.errors) {
63
- console.log(` ${error.path.join(".")}: ${error.message}`);
46
+ if (setDefaultMeta && Object.keys(doc.meta).length === 0) {
47
+ const defaults = def.meta.parse({})
48
+ if (Object.keys(defaults).length > 0) {
49
+ const cleanDefaults: Record<string, unknown> = {}
50
+ for (const [k, v] of Object.entries(defaults)) {
51
+ if (v !== undefined) cleanDefaults[k] = v
52
+ }
53
+ if (Object.keys(cleanDefaults).length > 0) {
54
+ Object.assign(doc.meta, cleanDefaults)
55
+ await doc.save({ normalize: false })
56
+ updated++
57
+ console.log(`SET DEFAULTS: ${pathId}`)
64
58
  }
65
59
  }
66
60
  }
67
61
 
68
- console.log();
69
- console.log(
70
- `Validated ${valid + invalid} documents: ${valid} valid, ${invalid} invalid`
71
- );
62
+ const result = validateDocument(doc, def)
63
+
64
+ if (result.valid) {
65
+ valid++
66
+ } else {
67
+ invalid++
68
+ console.log(`INVALID: ${pathId}`)
69
+ for (const error of result.errors) {
70
+ console.log(` ${error.path.join('.')}: ${error.message}`)
71
+ }
72
+ }
73
+ }
74
+
75
+ console.log()
76
+ if (updated > 0) {
77
+ console.log(`Updated ${updated} document(s) with default meta.`)
78
+ }
79
+ console.log(
80
+ `Validated ${valid + invalid} documents: ${valid} valid, ${invalid} invalid`
81
+ )
82
+
83
+ if (invalid > 0) process.exit(1)
84
+ }
85
+
86
+ commands.register('validate', {
87
+ description: 'Validate documents against their model schemas',
88
+ help: `# cnotes validate
89
+
90
+ Validate documents against their model schemas. Check frontmatter types, required fields, and optionally fill in missing defaults.
91
+
92
+ ## Usage
93
+
94
+ \`\`\`
95
+ cnotes validate [target] [options]
96
+ \`\`\`
97
+
98
+ ## Arguments
99
+
100
+ | Argument | Description | Default |
101
+ |----------|-------------|---------|
102
+ | \`target\` | A path ID, model name, or \`all\` | \`all\` |
103
+
104
+ ## Options
105
+
106
+ | Option | Description |
107
+ |--------|-------------|
108
+ | \`--setDefaultMeta\` | Write Zod schema defaults to documents with empty frontmatter |
109
+ | \`--contentFolder\` | Path to content folder |
110
+
111
+ ## Exit Codes
112
+
113
+ - \`0\` — All documents valid
114
+ - \`1\` — One or more validation errors
115
+
116
+ ## Examples
117
+
118
+ \`\`\`bash
119
+ # Validate everything
120
+ cnotes validate
121
+
122
+ # Validate a single document
123
+ cnotes validate epics/auth-system
124
+
125
+ # Validate all documents of a model
126
+ cnotes validate Epic
127
+
128
+ # Fill in missing defaults and validate
129
+ cnotes validate all --setDefaultMeta
72
130
 
73
- if (invalid > 0) process.exit(1);
74
- },
75
- });
131
+ # Validate a different content folder
132
+ cnotes validate --contentFolder ./docs
133
+ \`\`\`
134
+ `,
135
+ argsSchema,
136
+ handler,
137
+ })
package/src/cli/index.ts CHANGED
@@ -1,20 +1,89 @@
1
1
  #!/usr/bin/env bun
2
- import { defineCommand, runMain } from "citty";
3
-
4
- const main = defineCommand({
5
- meta: {
6
- name: "contentbase",
7
- version: "0.1.0",
8
- description: "An ORM for Markdown/MDX files",
9
- },
10
- subCommands: {
11
- init: () => import("./commands/init.js").then((m) => m.default),
12
- create: () => import("./commands/create.js").then((m) => m.default),
13
- inspect: () => import("./commands/inspect.js").then((m) => m.default),
14
- validate: () => import("./commands/validate.js").then((m) => m.default),
15
- export: () => import("./commands/export.js").then((m) => m.default),
16
- action: () => import("./commands/action.js").then((m) => m.default),
17
- },
18
- });
19
-
20
- runMain(main);
2
+ import { commands } from './registry.js'
3
+
4
+ // Side-effect imports register all commands
5
+ import './commands/index.js'
6
+
7
+ async function renderMarkdown(container: any, text: string) {
8
+ const ui = container.feature('ui')
9
+ console.log(ui.markdown(text))
10
+ }
11
+
12
+ async function main() {
13
+ // Dynamic import so the library stays luca-free; only the CLI pulls it in
14
+ const luca = await import('@soederpop/luca/node')
15
+ const container = luca.default
16
+
17
+ const commandName = container.argv._[0] as string | undefined
18
+ const wantsHelp = container.argv.help || container.argv.h
19
+
20
+ // Bare invocation or explicit "help" with no subcommand
21
+ if (!commandName || (commandName === 'help' && !container.argv._[1])) {
22
+ const help = commands.get('help')!
23
+ await help.handler({}, { container })
24
+ return
25
+ }
26
+
27
+ // `cnotes help <command>` — show that command's help
28
+ if (commandName === 'help' && container.argv._[1]) {
29
+ const target = container.argv._[1] as string
30
+ if (!commands.has(target)) {
31
+ console.error(`Unknown command: ${target}`)
32
+ console.error(`Run "cnotes help" to see available commands.\n`)
33
+ process.exit(1)
34
+ }
35
+ const def = commands.get(target)!
36
+ if (def.help) {
37
+ await renderMarkdown(container, def.help)
38
+ } else {
39
+ console.log(`${target} — ${def.description}\n`)
40
+ console.log(`No detailed help available for this command.`)
41
+ }
42
+ return
43
+ }
44
+
45
+ if (!commands.has(commandName)) {
46
+ console.error(`Unknown command: ${commandName}`)
47
+ console.error(`Run "cnotes help" to see available commands.\n`)
48
+ process.exit(1)
49
+ }
50
+
51
+ const def = commands.get(commandName)!
52
+
53
+ // `cnotes <command> --help` — show that command's help
54
+ if (wantsHelp) {
55
+ if (def.help) {
56
+ await renderMarkdown(container, def.help)
57
+ } else {
58
+ console.log(`${commandName} — ${def.description}\n`)
59
+ console.log(`No detailed help available for this command.`)
60
+ }
61
+ return
62
+ }
63
+
64
+ // Parse args from container.argv (minimist-parsed)
65
+ const { _: _positional, ...flags } = container.argv
66
+ let options = flags
67
+
68
+ if (def.argsSchema) {
69
+ const result = def.argsSchema.safeParse(flags)
70
+ if (!result.success) {
71
+ console.error(`Invalid options for "${commandName}":`)
72
+ for (const issue of result.error.issues) {
73
+ console.error(` ${issue.path.join('.')}: ${issue.message}`)
74
+ }
75
+ if (def.help) {
76
+ console.error(`\nRun "cnotes ${commandName} --help" for usage information.`)
77
+ }
78
+ process.exit(1)
79
+ }
80
+ options = result.data
81
+ }
82
+
83
+ await def.handler(options, { container })
84
+ }
85
+
86
+ main().catch((err) => {
87
+ console.error(err)
88
+ process.exit(1)
89
+ })