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,120 @@
1
+ import { z } from 'zod'
2
+ import pathModule from 'node:path'
3
+ import { existsSync, readdirSync } from 'node:fs'
4
+
5
+ export const path = '/api/semantic-search'
6
+ export const description = 'Semantic search across collection documents'
7
+ export const tags = ['query']
8
+
9
+ // ── Helpers ──────────────────────────────────────────────────────────
10
+
11
+ function hasSearchIndex(rootPath: string): boolean {
12
+ const dbDir = pathModule.join(rootPath, '.contentbase')
13
+ if (!existsSync(dbDir)) return false
14
+ try {
15
+ const files = readdirSync(dbDir) as string[]
16
+ return files.some((f: string) => f.startsWith('search.') && f.endsWith('.sqlite'))
17
+ } catch {
18
+ return false
19
+ }
20
+ }
21
+
22
+ let _semanticSearch: any = null
23
+
24
+ async function getSemanticSearch(container: any, rootPath: string) {
25
+ if (_semanticSearch?.state?.get('dbReady')) return _semanticSearch
26
+
27
+ const { SemanticSearch } = await import('@soederpop/luca/agi')
28
+ if (!container.features.available.includes('semanticSearch')) {
29
+ SemanticSearch.attach(container)
30
+ }
31
+
32
+ const dbPath = pathModule.join(rootPath, '.contentbase/search.sqlite')
33
+ _semanticSearch = container.feature('semanticSearch', { dbPath })
34
+ await _semanticSearch.initDb()
35
+ return _semanticSearch
36
+ }
37
+
38
+ // ── GET /api/semantic-search ─────────────────────────────────────────
39
+
40
+ export const getSchema = z.object({
41
+ q: z.string(),
42
+ mode: z.string().optional(),
43
+ model: z.string().optional(),
44
+ limit: z.string().optional(),
45
+ })
46
+
47
+ export async function get(params: any, ctx: any) {
48
+ const collection = ctx.container._contentbaseCollection
49
+ const rootPath = collection.rootPath
50
+
51
+ if (!hasSearchIndex(rootPath)) {
52
+ ctx.response.status(400)
53
+ return { error: 'No search index found. Run: cnotes embed' }
54
+ }
55
+
56
+ const ss = await getSemanticSearch(ctx.container, rootPath)
57
+ const mode = params.mode || 'hybrid'
58
+ const limit = params.limit ? parseInt(params.limit, 10) : 10
59
+ const searchOptions = { limit, model: params.model }
60
+
61
+ let results: any[]
62
+ switch (mode) {
63
+ case 'keyword':
64
+ results = await ss.search(params.q, searchOptions)
65
+ break
66
+ case 'vector':
67
+ results = await ss.vectorSearch(params.q, searchOptions)
68
+ break
69
+ case 'hybrid':
70
+ default:
71
+ results = await ss.hybridSearch(params.q, searchOptions)
72
+ break
73
+ }
74
+
75
+ return results
76
+ }
77
+
78
+ // ── POST /api/semantic-search ────────────────────────────────────────
79
+
80
+ export const postSchema = z.object({
81
+ query: z.string(),
82
+ mode: z.enum(['hybrid', 'keyword', 'vector']).optional(),
83
+ model: z.string().optional(),
84
+ limit: z.number().optional(),
85
+ where: z.record(z.string(), z.any()).optional(),
86
+ })
87
+
88
+ export async function post(params: any, ctx: any) {
89
+ const collection = ctx.container._contentbaseCollection
90
+ const rootPath = collection.rootPath
91
+
92
+ if (!hasSearchIndex(rootPath)) {
93
+ ctx.response.status(400)
94
+ return { error: 'No search index found. Run: cnotes embed' }
95
+ }
96
+
97
+ const ss = await getSemanticSearch(ctx.container, rootPath)
98
+ const mode = params.mode || 'hybrid'
99
+ const searchOptions = {
100
+ limit: params.limit || 10,
101
+ model: params.model,
102
+ where: params.where,
103
+ }
104
+
105
+ let results: any[]
106
+ switch (mode) {
107
+ case 'keyword':
108
+ results = await ss.search(params.query, searchOptions)
109
+ break
110
+ case 'vector':
111
+ results = await ss.vectorSearch(params.query, searchOptions)
112
+ break
113
+ case 'hybrid':
114
+ default:
115
+ results = await ss.hybridSearch(params.query, searchOptions)
116
+ break
117
+ }
118
+
119
+ return results
120
+ }
@@ -0,0 +1,63 @@
1
+ import { z } from 'zod'
2
+
3
+ export const path = '/api/text-search'
4
+ export const description = 'Search file contents with pattern matching'
5
+ export const tags = ['query']
6
+
7
+ export const getSchema = z.object({
8
+ pattern: z.string(),
9
+ expanded: z.string().optional(),
10
+ include: z.string().optional(),
11
+ exclude: z.string().optional(),
12
+ ignoreCase: z.string().optional(),
13
+ maxResults: z.string().optional(),
14
+ })
15
+
16
+ export async function get(params: any, ctx: any) {
17
+ const collection = ctx.container._contentbaseCollection
18
+ const grep = ctx.container.feature('grep')
19
+ const searchPath = collection.rootPath
20
+
21
+ const expanded = params.expanded === 'true'
22
+ const ignoreCase = params.ignoreCase === 'true'
23
+ const maxResults = params.maxResults ? parseInt(params.maxResults, 10) : undefined
24
+
25
+ if (!expanded) {
26
+ const files = await grep.filesContaining(params.pattern, {
27
+ path: searchPath,
28
+ ignoreCase,
29
+ maxResults,
30
+ include: params.include,
31
+ exclude: params.exclude,
32
+ })
33
+
34
+ return { files, count: files.length }
35
+ }
36
+
37
+ const results = await grep.search({
38
+ pattern: params.pattern,
39
+ path: searchPath,
40
+ ignoreCase,
41
+ maxResults,
42
+ include: params.include,
43
+ exclude: params.exclude,
44
+ })
45
+
46
+ // Group by file
47
+ const grouped = new Map<string, Array<{ line: number; column?: number; content: string }>>()
48
+ for (const match of results) {
49
+ if (!grouped.has(match.file)) grouped.set(match.file, [])
50
+ grouped.get(match.file)!.push({
51
+ line: match.line,
52
+ column: match.column,
53
+ content: match.content,
54
+ })
55
+ }
56
+
57
+ const files = Array.from(grouped.entries()).map(([file, matches]) => ({
58
+ file,
59
+ matches,
60
+ }))
61
+
62
+ return { files, count: files.length }
63
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod'
2
+ import { validateDocument } from '../../index.js'
3
+ import { resolveModelDef } from '../helpers.js'
4
+
5
+ export const path = '/api/validate'
6
+ export const description = 'Validate a document against its model schema'
7
+ export const tags = ['validation']
8
+
9
+ export const getSchema = z.object({
10
+ pathId: z.string(),
11
+ model: z.string().optional(),
12
+ })
13
+
14
+ export async function get(params: any, ctx: any) {
15
+ const collection = ctx.container._contentbaseCollection
16
+
17
+ if (!collection.available.includes(params.pathId)) {
18
+ ctx.response.status(404)
19
+ return { error: `Document not found: ${params.pathId}` }
20
+ }
21
+
22
+ const doc = collection.document(params.pathId)
23
+
24
+ const def = params.model
25
+ ? resolveModelDef(collection, params.model)
26
+ : collection.findModelDefinition(params.pathId)
27
+
28
+ if (!def) {
29
+ ctx.response.status(400)
30
+ return { error: `No model definition found for ${params.pathId}` }
31
+ }
32
+
33
+ return validateDocument(doc, def)
34
+ }
@@ -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
+ })