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.
- package/docs/models.ts +4 -1
- package/package.json +8 -3
- package/src/cli/commands/action.ts +1 -0
- package/src/cli/commands/api/endpoints/index.ts +38 -0
- package/src/{api → cli/commands/api}/endpoints/query.ts +1 -1
- package/src/{api → cli/commands/api}/endpoints/validate.ts +1 -1
- package/src/{api → cli/commands/api}/helpers.ts +2 -2
- package/src/cli/commands/create.ts +30 -0
- package/src/cli/commands/extract.ts +1 -0
- package/src/cli/commands/help.ts +2 -1
- package/src/cli/commands/init.ts +1 -0
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/search.ts +1 -0
- package/src/cli/commands/serve.ts +4 -4
- package/src/cli/commands/summary.ts +3 -1
- package/src/cli/commands/text-search.ts +1 -0
- package/src/cli/commands/validate.ts +1 -0
- package/src/cli/registry.ts +1 -0
- package/src/collection.ts +20 -10
- package/src/query/query-dsl.ts +1 -1
- /package/src/{api → cli/commands/api}/endpoints/actions.ts +0 -0
- /package/src/{api → cli/commands/api}/endpoints/doc.ts +0 -0
- /package/src/{api → cli/commands/api}/endpoints/docs-index.ts +0 -0
- /package/src/{api → cli/commands/api}/endpoints/document.ts +0 -0
- /package/src/{api → cli/commands/api}/endpoints/documents.ts +0 -0
- /package/src/{api → cli/commands/api}/endpoints/inspect.ts +0 -0
- /package/src/{api → cli/commands/api}/endpoints/models.ts +0 -0
- /package/src/{api → cli/commands/api}/endpoints/root.ts +0 -0
- /package/src/{api → cli/commands/api}/endpoints/search-reindex.ts +0 -0
- /package/src/{api → cli/commands/api}/endpoints/search-status.ts +0 -0
- /package/src/{api → cli/commands/api}/endpoints/search.ts +0 -0
- /package/src/{api → cli/commands/api}/endpoints/semantic-search.ts +0 -0
- /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.
|
|
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": "
|
|
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": [
|
|
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 '
|
|
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'
|
|
@@ -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 '
|
|
7
|
-
import { introspectMetaSchema } from '
|
|
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.
|
package/src/cli/commands/help.ts
CHANGED
|
@@ -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
|
-
|
|
25
|
+
const label = def.usage ? `${name} ${def.usage}` : name
|
|
26
|
+
lines.push(`| \`${label}\` | ${def.description} |`)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
lines.push(
|
package/src/cli/commands/init.ts
CHANGED
package/src/cli/commands/mcp.ts
CHANGED
|
@@ -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 '
|
|
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 '
|
|
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
|
-
|
|
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.
|
package/src/cli/registry.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
package/src/query/query-dsl.ts
CHANGED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|