contentbase 0.1.5 → 0.1.8

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 (33) hide show
  1. package/docs/models.ts +4 -1
  2. package/package.json +8 -3
  3. package/src/cli/commands/action.ts +1 -0
  4. package/src/cli/commands/api/endpoints/index.ts +38 -0
  5. package/src/{api → cli/commands/api}/endpoints/query.ts +1 -1
  6. package/src/{api → cli/commands/api}/endpoints/validate.ts +1 -1
  7. package/src/{api → cli/commands/api}/helpers.ts +2 -2
  8. package/src/cli/commands/create.ts +30 -0
  9. package/src/cli/commands/extract.ts +1 -0
  10. package/src/cli/commands/help.ts +2 -1
  11. package/src/cli/commands/init.ts +1 -0
  12. package/src/cli/commands/mcp.ts +1 -1
  13. package/src/cli/commands/search.ts +1 -0
  14. package/src/cli/commands/serve.ts +4 -4
  15. package/src/cli/commands/summary.ts +3 -1
  16. package/src/cli/commands/text-search.ts +1 -0
  17. package/src/cli/commands/validate.ts +1 -0
  18. package/src/cli/registry.ts +1 -0
  19. package/src/collection.ts +20 -10
  20. package/src/query/query-dsl.ts +1 -1
  21. /package/src/{api → cli/commands/api}/endpoints/actions.ts +0 -0
  22. /package/src/{api → cli/commands/api}/endpoints/doc.ts +0 -0
  23. /package/src/{api → cli/commands/api}/endpoints/docs-index.ts +0 -0
  24. /package/src/{api → cli/commands/api}/endpoints/document.ts +0 -0
  25. /package/src/{api → cli/commands/api}/endpoints/documents.ts +0 -0
  26. /package/src/{api → cli/commands/api}/endpoints/inspect.ts +0 -0
  27. /package/src/{api → cli/commands/api}/endpoints/models.ts +0 -0
  28. /package/src/{api → cli/commands/api}/endpoints/root.ts +0 -0
  29. /package/src/{api → cli/commands/api}/endpoints/search-reindex.ts +0 -0
  30. /package/src/{api → cli/commands/api}/endpoints/search-status.ts +0 -0
  31. /package/src/{api → cli/commands/api}/endpoints/search.ts +0 -0
  32. /package/src/{api → cli/commands/api}/endpoints/semantic-search.ts +0 -0
  33. /package/src/{api → cli/commands/api}/endpoints/text-search.ts +0 -0
package/docs/models.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
 
10
10
  export const Idea = defineModel("Idea", {
11
11
  prefix: "ideas",
12
+ description: 'Ideas are used to capture thoughts for new features, apis, etc',
12
13
  meta: z.object({
13
14
  goal: z.string().optional().describe("Slug of the goal this idea is aligned to"),
14
15
  tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the idea"),
@@ -18,8 +19,10 @@ export const Idea = defineModel("Idea", {
18
19
 
19
20
  export const Tutorial = defineModel("Tutorial", {
20
21
  prefix: "tutorials",
22
+ pattern: ['tutorials/:slug', 'tutorials/:category/:slug'],
21
23
  meta: z.object({
22
24
  tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the tutorial"),
25
+ category: z.string().default('contentbase')
23
26
  }),
24
27
  })
25
28
 
@@ -35,4 +38,4 @@ export const Example = defineModel("Example", {
35
38
  meta: z.object({
36
39
  tags: z.array(z.string()).default([]).describe("Arbitrary tags for categorizing the example"),
37
40
  }),
38
- })
41
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentbase",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "repository": "https://github.com/soederpop/contentbase",
5
5
  "website": "https://contentbase.soederpop.com",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "release": "./scripts/release.sh"
21
21
  },
22
22
  "dependencies": {
23
- "@soederpop/luca": "latest",
23
+ "@soederpop/luca": ">=0.0.16",
24
24
  "gray-matter": "^4.0.3",
25
25
  "js-yaml": "^4.1.0",
26
26
  "mdast-util-mdxjs-esm": "^2.0.1",
@@ -50,6 +50,11 @@
50
50
  "vitest": "^1.6.0"
51
51
  },
52
52
  "luca": {
53
- "aliases": ["contentbase", "content base", "content", "the orm"]
53
+ "aliases": [
54
+ "contentbase",
55
+ "content base",
56
+ "content",
57
+ "the orm"
58
+ ]
54
59
  }
55
60
  }
@@ -38,6 +38,7 @@ async function handler(options: z.infer<typeof argsSchema>, context: { container
38
38
 
39
39
  commands.register('action', {
40
40
  description: 'Run a named action on the collection',
41
+ usage: '<name>',
41
42
  help: `# cnotes action
42
43
 
43
44
  Run a named action registered on the collection. Actions are custom functions defined in your collection's entry point.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Static imports of all built-in endpoint modules.
3
+ * This allows endpoints to be bundled into a compiled bun binary
4
+ * instead of relying on runtime filesystem scanning.
5
+ */
6
+ import * as actions from './actions.js'
7
+ import * as doc from './doc.js'
8
+ import * as docsIndex from './docs-index.js'
9
+ import * as document from './document.js'
10
+ import * as documents from './documents.js'
11
+ import * as inspect from './inspect.js'
12
+ import * as models from './models.js'
13
+ import * as query from './query.js'
14
+ import * as root from './root.js'
15
+ import * as searchReindex from './search-reindex.js'
16
+ import * as searchStatus from './search-status.js'
17
+ import * as search from './search.js'
18
+ import * as semanticSearch from './semantic-search.js'
19
+ import * as textSearch from './text-search.js'
20
+ import * as validate from './validate.js'
21
+
22
+ export const builtinEndpoints = [
23
+ actions,
24
+ doc,
25
+ docsIndex,
26
+ document,
27
+ documents,
28
+ inspect,
29
+ models,
30
+ query,
31
+ root,
32
+ searchReindex,
33
+ searchStatus,
34
+ search,
35
+ semanticSearch,
36
+ textSearch,
37
+ validate,
38
+ ]
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { resolveModelDef } from '../helpers.js'
3
- import { queryDSLSchema, executeQueryDSL } from '../../query/query-dsl.js'
3
+ import { queryDSLSchema, executeQueryDSL } from '../../../../query/query-dsl.js'
4
4
 
5
5
  export const path = '/api/query'
6
6
  export const description = 'Query model instances with filtering'
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { validateDocument } from '../../index.js'
2
+ import { validateDocument } from '../../../../index.js'
3
3
  import { resolveModelDef } from '../helpers.js'
4
4
 
5
5
  export const path = '/api/validate'
@@ -3,8 +3,8 @@ import remarkParse from 'remark-parse'
3
3
  import remarkGfm from 'remark-gfm'
4
4
  import remarkRehype from 'remark-rehype'
5
5
  import rehypeStringify from 'rehype-stringify'
6
- import type { Collection } from '../collection.js'
7
- import { introspectMetaSchema } from '../collection.js'
6
+ import type { Collection } from '../../../collection.js'
7
+ import { introspectMetaSchema } from '../../../collection.js'
8
8
 
9
9
  export async function renderMarkdownToHtml(markdown: string): Promise<string> {
10
10
  const result = await unified()
@@ -2,6 +2,7 @@ import { z } from 'zod'
2
2
  import fs from 'fs/promises'
3
3
  import path from 'path'
4
4
  import matter from 'gray-matter'
5
+ import { execSync } from 'child_process'
5
6
  import { commands } from '../registry.js'
6
7
  import { loadCollection } from '../load-collection.js'
7
8
  import { kebabCase } from '../../utils/inflect.js'
@@ -10,6 +11,7 @@ import { introspectMetaSchema } from '../../collection.js'
10
11
  const argsSchema = z.object({
11
12
  title: z.string().optional(),
12
13
  contentFolder: z.string().optional(),
14
+ openEditor: z.boolean().optional(),
13
15
  })
14
16
 
15
17
  async function handler(options: z.infer<typeof argsSchema>, context: { container: any }) {
@@ -125,10 +127,37 @@ async function handler(options: z.infer<typeof argsSchema>, context: { container
125
127
  await fs.writeFile(filePath, content, 'utf8')
126
128
 
127
129
  console.log(`Created ${filePath}`)
130
+
131
+ if (options.openEditor) {
132
+ const editor = process.env.EDITOR
133
+ if (editor) {
134
+ execSync(`${editor} ${filePath}`, { stdio: 'inherit' })
135
+ } else {
136
+ const candidates = ['cursor', 'code', 'vim']
137
+ let found = false
138
+ for (const cmd of candidates) {
139
+ try {
140
+ execSync(`which ${cmd}`, { stdio: 'ignore' })
141
+ execSync(`${cmd} ${filePath}`, { stdio: 'inherit' })
142
+ found = true
143
+ break
144
+ } catch {
145
+ // not available, try next
146
+ }
147
+ }
148
+ if (!found) {
149
+ console.error(
150
+ 'No editor found. Tried: cursor, code, vim. Set $EDITOR to your preferred editor.'
151
+ )
152
+ process.exit(1)
153
+ }
154
+ }
155
+ }
128
156
  }
129
157
 
130
158
  commands.register('create', {
131
159
  description: 'Create a new document for a model type',
160
+ usage: '<model>',
132
161
  help: `# cnotes create
133
162
 
134
163
  Create a new document for a registered model, with proper frontmatter defaults and section scaffolding.
@@ -151,6 +180,7 @@ cnotes create <model> --title "Document Title" [options]
151
180
  |--------|-------------|
152
181
  | \`--title\` | **Required.** Title for the new document (used as H1 and slug) |
153
182
  | \`--meta.*\` | Set frontmatter fields (e.g. \`--meta.status active\`) |
183
+ | \`--open-editor\` | Open the new file in your editor ($EDITOR, or tries cursor/code/vim) |
154
184
  | \`--contentFolder\` | Path to content folder |
155
185
 
156
186
  ## Template Lookup
@@ -123,6 +123,7 @@ async function handler(options: z.infer<typeof argsSchema>, context: { container
123
123
 
124
124
  commands.register('extract', {
125
125
  description: 'Extract specific sections from documents',
126
+ usage: '<glob>',
126
127
  help: `# cnotes extract
127
128
 
128
129
  Extract specific sections from one or more documents. Supports glob patterns for matching multiple documents and outputs clean markdown with normalized headings.
@@ -22,7 +22,8 @@ async function handler(_options: any, context: { container: any }) {
22
22
 
23
23
  for (const name of commands.available) {
24
24
  const def = commands.get(name)!
25
- lines.push(`| \`${name}\` | ${def.description} |`)
25
+ const label = def.usage ? `${name} ${def.usage}` : name
26
+ lines.push(`| \`${label}\` | ${def.description} |`)
26
27
  }
27
28
 
28
29
  lines.push(
@@ -60,6 +60,7 @@ collection.register(Post);
60
60
 
61
61
  commands.register('init', {
62
62
  description: 'Initialize a new contentbase project',
63
+ usage: '[name]',
63
64
  help: `# cnotes init
64
65
 
65
66
  Scaffold a new contentbase project with a sample model and document.
@@ -6,7 +6,7 @@ import matter from 'gray-matter'
6
6
  import { commands } from '../registry.js'
7
7
  import { loadCollection } from '../load-collection.js'
8
8
  import { introspectMetaSchema, validateDocument } from '../../index.js'
9
- import { resolveModelDef, buildSchemaJSON } from '../../api/helpers.js'
9
+ import { resolveModelDef, buildSchemaJSON } from './api/helpers.js'
10
10
  import { queryDSLSchema, executeQueryDSL } from '../../query/query-dsl.js'
11
11
  // MCPServer type comes from container.server('mcp', ...) at runtime
12
12
 
@@ -223,6 +223,7 @@ async function handler(options: z.infer<typeof argsSchema>, { container }: { con
223
223
 
224
224
  commands.register('search', {
225
225
  description: 'Semantic search across collection documents',
226
+ usage: '<query>',
226
227
  help: `# cnotes search
227
228
 
228
229
  Search documents in the collection using keyword, semantic, or hybrid search modes. Requires a search index — run \`cnotes embed\` first.
@@ -3,7 +3,8 @@ import path from 'node:path'
3
3
  import fs from 'node:fs'
4
4
  import { commands } from '../registry.js'
5
5
  import { loadCollection } from '../load-collection.js'
6
- import { buildSchemaJSON } from '../../api/helpers.js'
6
+ import { buildSchemaJSON } from './api/helpers.js'
7
+ import { builtinEndpoints } from './api/endpoints/index.js'
7
8
 
8
9
  const argsSchema = z.object({
9
10
  port: z.number().default(8000),
@@ -118,9 +119,8 @@ async function handler(options: z.infer<typeof argsSchema>, context: { container
118
119
  historyFallback: false,
119
120
  }) as any
120
121
 
121
- // Load built-in contentbase endpoints
122
- const builtinEndpointsDir = path.resolve(import.meta.dir, '../../api/endpoints')
123
- await expressServer.useEndpoints(builtinEndpointsDir)
122
+ // Load built-in contentbase endpoints (statically imported so they survive bun compile)
123
+ await expressServer.useEndpointModules(builtinEndpoints)
124
124
 
125
125
  // Load user endpoints if present
126
126
  if (userEndpointsDir) {
@@ -6,6 +6,7 @@ import { loadCollection } from '../load-collection.js'
6
6
 
7
7
  const argsSchema = z.object({
8
8
  contentFolder: z.string().optional(),
9
+ includeIds: z.boolean().optional().default(false),
9
10
  })
10
11
 
11
12
  async function handler(options: z.infer<typeof argsSchema>) {
@@ -13,7 +14,7 @@ async function handler(options: z.infer<typeof argsSchema>) {
13
14
  contentFolder: options.contentFolder,
14
15
  })
15
16
 
16
- await collection.saveModelSummary()
17
+ await collection.saveModelSummary({ includeIds: options.includeIds })
17
18
  console.log(`README.md written to ${collection.rootPath}/README.md`)
18
19
 
19
20
  const toc = collection.tableOfContents({ title: 'Table of Contents' })
@@ -39,6 +40,7 @@ cnotes summary [options]
39
40
  | Option | Description |
40
41
  |--------|-------------|
41
42
  | \`--contentFolder\` | Path to content folder |
43
+ | \`--include-ids\` | Include document IDs in the summary (default: false) |
42
44
 
43
45
  ## Generated Files
44
46
 
@@ -86,6 +86,7 @@ async function handler(options: z.infer<typeof argsSchema>, { container }: { con
86
86
 
87
87
  commands.register('text-search', {
88
88
  description: 'Search file contents with pattern matching',
89
+ usage: '<pattern>',
89
90
  help: `# cnotes text-search
90
91
 
91
92
  Search file contents within the collection using ripgrep. Returns matching files by default, or line-level detail with \`--expanded\`.
@@ -85,6 +85,7 @@ async function handler(options: z.infer<typeof argsSchema>, context: { container
85
85
 
86
86
  commands.register('validate', {
87
87
  description: 'Validate documents against their model schemas',
88
+ usage: '[model]',
88
89
  help: `# cnotes validate
89
90
 
90
91
  Validate documents against their model schemas. Check frontmatter types, required fields, and optionally fill in missing defaults.
@@ -2,6 +2,7 @@ import type { z } from 'zod'
2
2
 
3
3
  export interface CommandDefinition {
4
4
  description: string
5
+ usage?: string
5
6
  help?: string
6
7
  argsSchema?: z.ZodType<any>
7
8
  handler: (options: any, context: { container: any }) => Promise<void>
package/src/collection.ts CHANGED
@@ -715,7 +715,7 @@ export class Collection {
715
715
  * Generate a plain-text summary of the collection and its models.
716
716
  * Returns the same output as `cnotes inspect`.
717
717
  */
718
- generateModelSummary(): string {
718
+ generateModelSummary(options: { includeIds?: boolean } = {}): string {
719
719
  if (!this.#loaded) {
720
720
  throw new Error("Collection has not been loaded. Call load() first.");
721
721
  }
@@ -728,11 +728,17 @@ export class Collection {
728
728
  lines.push("");
729
729
 
730
730
  for (const def of this.modelDefinitions) {
731
- const matchingItems = this.available.filter(
732
- (id) => this.findModelDefinition(id)?.name === def.name
733
- );
734
731
  lines.push(` Model: ${def.name}`);
735
- lines.push(` Prefix: ${def.prefix}`);
732
+ if (def.description) {
733
+ lines.push(` Description: ${def.description}`);
734
+ }
735
+ if (def.pattern) {
736
+ const patterns = Array.isArray(def.pattern) ? def.pattern : [def.pattern];
737
+ lines.push(` Path prefix: ${patterns.join(", ")}`);
738
+ } else {
739
+ const rel = path.relative(process.cwd(), path.join(this.rootPath, def.prefix));
740
+ lines.push(` Path prefix: ${rel}/*.md`);
741
+ }
736
742
  const fields = introspectMetaSchema(def.meta);
737
743
  lines.push(
738
744
  ` Meta: ${fields.length > 0 ? fields.map((f) => `${f.name}(${f.type})`).join(", ") : "(none)"}`
@@ -743,9 +749,13 @@ export class Collection {
743
749
  lines.push(
744
750
  ` Relationships: ${Object.keys(def.relationships).join(", ") || "(none)"}`
745
751
  );
746
- lines.push(` Documents: ${matchingItems.length}`);
747
- if (matchingItems.length > 0) {
748
- lines.push(` IDs: ${matchingItems.join(", ")}`);
752
+ if (options.includeIds) {
753
+ const matchingItems = this.available.filter(
754
+ (id) => this.findModelDefinition(id)?.name === def.name
755
+ );
756
+ if (matchingItems.length > 0) {
757
+ lines.push(` IDs: ${[...matchingItems].sort().join(", ")}`);
758
+ }
749
759
  }
750
760
  lines.push("");
751
761
  }
@@ -762,8 +772,8 @@ export class Collection {
762
772
  * Preserves the `## Overview` section if it already exists.
763
773
  * The generated summary is placed in the `## Summary` section.
764
774
  */
765
- async saveModelSummary(): Promise<string> {
766
- const summary = this.generateModelSummary();
775
+ async saveModelSummary(options: { includeIds?: boolean } = {}): Promise<string> {
776
+ const summary = this.generateModelSummary(options);
767
777
  const modelsPath = path.join(this.rootPath, "README.md");
768
778
 
769
779
  // Preserve existing Overview section content
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import type { Collection } from "../collection";
3
3
  import type { Condition } from "./query-builder";
4
4
  import type { Operator } from "./operators";
5
- import { resolveModelDef } from "../api/helpers.js";
5
+ import { resolveModelDef } from "../cli/commands/api/helpers.js";
6
6
 
7
7
  // ---------------------------------------------------------------------------
8
8
  // Operator mapping: $dsl → internal
File without changes
File without changes