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
@@ -1,59 +1,185 @@
1
- import { defineCommand } from "citty";
2
- import fs from "fs/promises";
3
- import path from "path";
4
- import { loadCollection } from "../load-collection";
5
- import { kebabCase } from "../../utils/inflect";
6
-
7
- export default defineCommand({
8
- meta: {
9
- name: "create",
10
- description: "Create a new document for a model type",
11
- },
12
- args: {
13
- model: {
14
- type: "positional",
15
- description: "Model name",
16
- required: true,
17
- },
18
- title: {
19
- type: "string",
20
- description: "Document title",
21
- required: true,
22
- },
23
- rootPath: {
24
- type: "string",
25
- description: "Root path for the collection",
26
- alias: "r",
27
- },
28
- },
29
- async run({ args }) {
30
- const collection = await loadCollection({
31
- rootPath: args.rootPath as string | undefined,
32
- });
33
-
34
- const modelName = args.model as string;
35
- const title = args.title as string;
36
- const def = collection.getModelDefinition(modelName);
37
-
38
- if (!def) {
39
- console.error(
40
- `Model "${modelName}" not found. Available: ${collection.modelDefinitions.map((d) => d.name).join(", ")}`
41
- );
42
- process.exit(1);
1
+ import { z } from 'zod'
2
+ import fs from 'fs/promises'
3
+ import path from 'path'
4
+ import matter from 'gray-matter'
5
+ import { commands } from '../registry.js'
6
+ import { loadCollection } from '../load-collection.js'
7
+ import { kebabCase } from '../../utils/inflect.js'
8
+ import { introspectMetaSchema } from '../../collection.js'
9
+
10
+ const argsSchema = z.object({
11
+ title: z.string().optional(),
12
+ contentFolder: z.string().optional(),
13
+ })
14
+
15
+ async function handler(options: z.infer<typeof argsSchema>, context: { container: any }) {
16
+ const collection = await loadCollection({
17
+ contentFolder: options.contentFolder,
18
+ })
19
+
20
+ const modelName = context.container.argv._[1] as string | undefined
21
+ if (!modelName) {
22
+ console.error('Usage: cnotes create <model> --title "Document Title"')
23
+ process.exit(1)
24
+ }
25
+
26
+ const title = options.title
27
+ if (!title) {
28
+ console.error('--title is required')
29
+ process.exit(1)
30
+ }
31
+
32
+ const def =
33
+ collection.getModelDefinition(modelName) ??
34
+ collection.modelDefinitions.find(
35
+ (d) => d.name.toLowerCase() === modelName.toLowerCase()
36
+ )
37
+
38
+ if (!def) {
39
+ console.error(
40
+ `Model "${modelName}" not found. Available: ${collection.modelDefinitions.map((d) => d.name).join(', ')}`
41
+ )
42
+ process.exit(1)
43
+ }
44
+
45
+ // Parse --meta.* flags from raw argv
46
+ const metaOverrides: Record<string, unknown> = {}
47
+ const rawArgs = context.container.argv
48
+ for (const key of Object.keys(rawArgs)) {
49
+ if (key.startsWith('meta.')) {
50
+ metaOverrides[key.slice(5)] = rawArgs[key]
43
51
  }
52
+ }
53
+
54
+ // Build meta from priority layers: zod defaults < definition.defaults < template frontmatter < CLI overrides
55
+ const zodDefaults: Record<string, unknown> = {}
56
+ for (const field of introspectMetaSchema(def.meta)) {
57
+ if (field.defaultValue !== undefined) {
58
+ zodDefaults[field.name] = field.defaultValue
59
+ }
60
+ }
61
+
62
+ const definitionDefaults: Record<string, unknown> = def.defaults ?? {}
44
63
 
45
- const slug = kebabCase(title.toLowerCase());
46
- const pathId = `${def.prefix}/${slug}`;
47
- const filePath = path.resolve(
64
+ // Template lookup
65
+ const templateExtensions = ['md', 'mdx']
66
+ let templateContent: string | null = null
67
+
68
+ for (const ext of templateExtensions) {
69
+ const templatePath = path.resolve(
48
70
  collection.rootPath,
49
- `${pathId}.mdx`
50
- );
71
+ 'templates',
72
+ `${modelName.toLowerCase()}.${ext}`
73
+ )
74
+ try {
75
+ templateContent = await fs.readFile(templatePath, 'utf8')
76
+ break
77
+ } catch {
78
+ // template not found, try next extension
79
+ }
80
+ }
81
+
82
+ let content: string
83
+
84
+ if (templateContent) {
85
+ const parsed = matter(templateContent)
86
+
87
+ const mergedMeta = {
88
+ ...zodDefaults,
89
+ ...definitionDefaults,
90
+ ...parsed.data,
91
+ ...metaOverrides,
92
+ }
93
+
94
+ const body = parsed.content.replace(/^# .+$/m, `# ${title}`)
95
+ content = matter.stringify(body, mergedMeta)
96
+ } else {
97
+ const mergedMeta = {
98
+ ...zodDefaults,
99
+ ...definitionDefaults,
100
+ ...metaOverrides,
101
+ }
102
+
103
+ const lines: string[] = []
104
+ lines.push(`# ${title}`)
105
+ lines.push('')
106
+
107
+ const sections = def.sections ?? {}
108
+ for (const [, sec] of Object.entries(sections)) {
109
+ const s = sec as any
110
+ lines.push(`## ${s.heading}`)
111
+ lines.push('')
112
+ if (s.schema?.description) {
113
+ lines.push(s.schema.description)
114
+ lines.push('')
115
+ }
116
+ }
117
+
118
+ content = matter.stringify(lines.join('\n'), mergedMeta)
119
+ }
120
+
121
+ const slug = kebabCase(title.toLowerCase())
122
+ const pathId = `${def.prefix}/${slug}`
123
+ const filePath = path.resolve(collection.rootPath, `${pathId}.md`)
124
+
125
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
126
+ await fs.writeFile(filePath, content, 'utf8')
127
+
128
+ console.log(`Created ${filePath}`)
129
+ }
130
+
131
+ commands.register('create', {
132
+ description: 'Create a new document for a model type',
133
+ help: `# cnotes create
134
+
135
+ Create a new document for a registered model, with proper frontmatter defaults and section scaffolding.
136
+
137
+ ## Usage
138
+
139
+ \`\`\`
140
+ cnotes create <model> --title "Document Title" [options]
141
+ \`\`\`
142
+
143
+ ## Arguments
144
+
145
+ | Argument | Description |
146
+ |----------|-------------|
147
+ | \`model\` | Model name (e.g. \`Post\`, \`Epic\`, \`Task\`) |
148
+
149
+ ## Options
150
+
151
+ | Option | Description |
152
+ |--------|-------------|
153
+ | \`--title\` | **Required.** Title for the new document (used as H1 and slug) |
154
+ | \`--meta.*\` | Set frontmatter fields (e.g. \`--meta.status active\`) |
155
+ | \`--contentFolder\` | Path to content folder |
156
+
157
+ ## Template Lookup
158
+
159
+ If \`templates/<model>.md\` (or \`.mdx\`) exists in the content root, it will be used as the document scaffold. Template frontmatter is merged with schema defaults and CLI overrides.
160
+
161
+ ## Meta Priority
162
+
163
+ Frontmatter values are merged in this order (last wins):
164
+
165
+ 1. Zod schema defaults
166
+ 2. Model definition defaults
167
+ 3. Template frontmatter
168
+ 4. CLI \`--meta.*\` overrides
169
+
170
+ ## Examples
51
171
 
52
- const content = `---\n---\n\n# ${title}\n`;
172
+ \`\`\`bash
173
+ # Create a new post
174
+ cnotes create Post --title "My First Post"
53
175
 
54
- await fs.mkdir(path.dirname(filePath), { recursive: true });
55
- await fs.writeFile(filePath, content, "utf8");
176
+ # Create with meta overrides
177
+ cnotes create Task --title "Fix login bug" --meta.status active --meta.priority high
56
178
 
57
- console.log(`Created ${filePath}`);
58
- },
59
- });
179
+ # Create from a different content folder
180
+ cnotes create Epic --title "Auth System" --contentFolder ./docs
181
+ \`\`\`
182
+ `,
183
+ argsSchema,
184
+ handler,
185
+ })
@@ -0,0 +1,323 @@
1
+ import { z } from 'zod'
2
+ import { existsSync, readdirSync, accessSync, constants as fsConstants } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { commands } from '../registry.js'
5
+ import { loadCollection } from '../load-collection.js'
6
+
7
+ const argsSchema = z.object({
8
+ force: z.boolean().default(false),
9
+ provider: z.enum(['local', 'openai']).default('openai'),
10
+ status: z.boolean().default(false),
11
+ local: z.boolean().default(false),
12
+ installLocal: z.boolean().default(false),
13
+ contentFolder: z.string().optional(),
14
+ })
15
+
16
+ function hasSearchIndex(rootPath: string): boolean {
17
+ const dbDir = path.join(rootPath, '.contentbase')
18
+ if (!existsSync(dbDir)) return false
19
+ try {
20
+ const files = readdirSync(dbDir) as string[]
21
+ return files.some((f: string) => f.startsWith('search.') && f.endsWith('.sqlite'))
22
+ } catch {
23
+ return false
24
+ }
25
+ }
26
+
27
+ function collectDocumentInputs(collection: any) {
28
+ const inputs: any[] = []
29
+ for (const pathId of collection.available) {
30
+ const doc = collection.document(pathId)
31
+ const modelDef = collection.findModelDefinition(pathId)
32
+
33
+ const sections: any[] = []
34
+ const lines = (doc.content as string).split('\n')
35
+ let currentHeading: string | null = null
36
+ let currentContent: string[] = []
37
+
38
+ for (const line of lines) {
39
+ const h2Match = line.match(/^## (.+)/)
40
+ if (h2Match) {
41
+ if (currentHeading) {
42
+ sections.push({
43
+ heading: currentHeading,
44
+ headingPath: currentHeading,
45
+ content: currentContent.join('\n').trim(),
46
+ level: 2,
47
+ })
48
+ }
49
+ currentHeading = h2Match[1].trim()
50
+ currentContent = []
51
+ } else if (currentHeading) {
52
+ currentContent.push(line)
53
+ }
54
+ }
55
+ if (currentHeading) {
56
+ sections.push({
57
+ heading: currentHeading,
58
+ headingPath: currentHeading,
59
+ content: currentContent.join('\n').trim(),
60
+ level: 2,
61
+ })
62
+ }
63
+
64
+ inputs.push({
65
+ pathId,
66
+ model: modelDef?.name ?? undefined,
67
+ title: doc.title,
68
+ slug: doc.slug,
69
+ meta: doc.meta,
70
+ content: doc.content,
71
+ sections: sections.length > 0 ? sections : undefined,
72
+ })
73
+ }
74
+ return inputs
75
+ }
76
+
77
+ function isLocalInstalled(): boolean {
78
+ const modulePath = path.join(process.cwd(), 'node_modules', 'node-llama-cpp')
79
+ try {
80
+ return existsSync(modulePath)
81
+ } catch {
82
+ return false
83
+ }
84
+ }
85
+
86
+ async function installLocal(SemanticSearch: any, container: any): Promise<void> {
87
+ const cwd = process.cwd()
88
+ const nodeModules = path.join(cwd, 'node_modules')
89
+
90
+ // Check permissions
91
+ try {
92
+ if (existsSync(nodeModules)) {
93
+ accessSync(nodeModules, fsConstants.W_OK)
94
+ }
95
+ } catch {
96
+ const version = SemanticSearch.PINNED_LLAMA_VERSION
97
+ const cmd = detectInstallCommand(cwd, version)
98
+ console.error(`Cannot write to ${nodeModules}.`)
99
+ console.error(`Run manually with elevated permissions:\n ${cmd}`)
100
+ process.exit(1)
101
+ }
102
+
103
+ console.error(`Installing node-llama-cpp@${SemanticSearch.PINNED_LLAMA_VERSION}...`)
104
+
105
+ // Create a temporary instance to use the install method
106
+ if (!container.features.available.includes('semanticSearch')) {
107
+ SemanticSearch.attach(container)
108
+ }
109
+ const ss = container.feature('semanticSearch', {
110
+ dbPath: path.join(cwd, '.contentbase/search.sqlite'),
111
+ embeddingProvider: 'local',
112
+ })
113
+
114
+ try {
115
+ await ss.installLocalEmbeddings(cwd)
116
+ console.log(`node-llama-cpp@${SemanticSearch.PINNED_LLAMA_VERSION} installed successfully.`)
117
+ } catch (err: any) {
118
+ const msg = err?.message ?? String(err)
119
+ if (msg.includes('network') || msg.includes('ETIMEDOUT') || msg.includes('ENOTFOUND')) {
120
+ console.error(`Install failed (network unreachable). To use local embeddings offline, manually install node-llama-cpp@${SemanticSearch.PINNED_LLAMA_VERSION} into this project.`)
121
+ } else {
122
+ console.error(msg)
123
+ }
124
+ process.exit(1)
125
+ }
126
+ }
127
+
128
+ function detectInstallCommand(cwd: string, version: string): string {
129
+ const pkg = `node-llama-cpp@${version}`
130
+ if (existsSync(path.join(cwd, 'bun.lockb')) || existsSync(path.join(cwd, 'bun.lock'))) {
131
+ return `bun add --optional ${pkg}`
132
+ } else if (existsSync(path.join(cwd, 'pnpm-lock.yaml'))) {
133
+ return `pnpm add --save-optional ${pkg}`
134
+ } else if (existsSync(path.join(cwd, 'yarn.lock'))) {
135
+ return `yarn add --optional ${pkg}`
136
+ }
137
+ return `npm install --save-optional ${pkg}`
138
+ }
139
+
140
+ async function handler(options: z.infer<typeof argsSchema>, { container }: { container: any }) {
141
+ const { SemanticSearch } = await import('@soederpop/luca/agi')
142
+
143
+ // --install-local: install node-llama-cpp only, then exit
144
+ if (options.installLocal) {
145
+ if (isLocalInstalled()) {
146
+ console.log('node-llama-cpp is already installed. Skipping.')
147
+ return
148
+ }
149
+ await installLocal(SemanticSearch, container)
150
+ return
151
+ }
152
+
153
+ // Resolve effective provider: --local flag overrides --provider
154
+ const provider = options.local ? 'local' : options.provider
155
+
156
+ // --local: auto-install if needed
157
+ if (options.local && !isLocalInstalled()) {
158
+ await installLocal(SemanticSearch, container)
159
+ }
160
+
161
+ const collection = await loadCollection({
162
+ contentFolder: options.contentFolder,
163
+ })
164
+
165
+ // --status: show index health without modifying anything
166
+ if (options.status) {
167
+ if (!hasSearchIndex(collection.rootPath)) {
168
+ console.log('No search index found.')
169
+ console.log(`Documents in collection: ${collection.available.length}`)
170
+ console.log(`\nRun: cnotes embed`)
171
+ return
172
+ }
173
+
174
+ if (!container.features.available.includes('semanticSearch')) {
175
+ SemanticSearch.attach(container)
176
+ }
177
+ const dbPath = path.join(collection.rootPath, '.contentbase/search.sqlite')
178
+ const ss = container.feature('semanticSearch', {
179
+ dbPath,
180
+ embeddingProvider: provider,
181
+ })
182
+ await ss.initDb()
183
+
184
+ const stats = ss.getStats()
185
+ console.log('Search Index Status')
186
+ console.log('-------------------')
187
+ console.log(`Documents indexed: ${stats.documentCount}`)
188
+ console.log(`Chunks created: ${stats.chunkCount}`)
189
+ console.log(`Embeddings stored: ${stats.embeddingCount}`)
190
+ console.log(`Last indexed: ${stats.lastIndexedAt ?? 'never'}`)
191
+ console.log(`Provider: ${stats.provider}`)
192
+ console.log(`Model: ${stats.model}`)
193
+ console.log(`Dimensions: ${stats.dimensions}`)
194
+ console.log(`Database size: ${formatBytes(stats.dbSizeBytes)}`)
195
+ console.log(`Collection docs: ${collection.available.length}`)
196
+
197
+ // Check for staleness
198
+ const docs = collectDocumentInputs(collection)
199
+ const stale = docs.filter((d: any) => ss.needsReindex(d)).length
200
+ if (stale > 0) {
201
+ console.log(`Stale documents: ${stale}`)
202
+ } else {
203
+ console.log(`Status: up to date`)
204
+ }
205
+ return
206
+ }
207
+
208
+ // Embed / re-embed
209
+ if (!container.features.available.includes('semanticSearch')) {
210
+ SemanticSearch.attach(container)
211
+ }
212
+ const dbPath = path.join(collection.rootPath, '.contentbase/search.sqlite')
213
+ const ss = container.feature('semanticSearch', {
214
+ dbPath,
215
+ embeddingProvider: provider,
216
+ })
217
+ await ss.initDb()
218
+
219
+ const docs = collectDocumentInputs(collection)
220
+ const startTime = Date.now()
221
+
222
+ if (options.force) {
223
+ console.error('Clearing existing index...')
224
+ await ss.reindex()
225
+ }
226
+
227
+ const toIndex = options.force ? docs : docs.filter((d: any) => ss.needsReindex(d))
228
+
229
+ // Remove stale
230
+ ss.removeStale(docs.map((d: any) => d.pathId))
231
+
232
+ if (toIndex.length === 0) {
233
+ console.log('All documents are up to date. Nothing to embed.')
234
+ const stats = ss.getStats()
235
+ printStats(stats, 0, Date.now() - startTime)
236
+ return
237
+ }
238
+
239
+ console.error(`Embedding ${toIndex.length} of ${docs.length} document(s)...`)
240
+ console.error(`Provider: ${provider}`)
241
+
242
+ const batchSize = 5
243
+
244
+ for (let i = 0; i < toIndex.length; i += batchSize) {
245
+ const batch = toIndex.slice(i, i + batchSize)
246
+ await ss.indexDocuments(batch)
247
+ const done = Math.min(i + batchSize, toIndex.length)
248
+ const pct = Math.round((done / toIndex.length) * 100)
249
+ const elapsed = Date.now() - startTime
250
+ const eta = toIndex.length > done
251
+ ? Math.round((elapsed / done) * (toIndex.length - done) / 1000)
252
+ : 0
253
+ const etaStr = eta > 0 ? ` ETA: ${eta}s` : ''
254
+ console.error(` [${pct}%] ${done}/${toIndex.length} documents${etaStr}`)
255
+ }
256
+
257
+ const elapsed = Date.now() - startTime
258
+ const stats = ss.getStats()
259
+ printStats(stats, toIndex.length, elapsed)
260
+ }
261
+
262
+ function printStats(stats: any, indexed: number, elapsedMs: number) {
263
+ console.log()
264
+ console.log('Embedding complete.')
265
+ if (indexed > 0) console.log(` Documents indexed: ${indexed}`)
266
+ console.log(` Total documents: ${stats.documentCount}`)
267
+ console.log(` Total chunks: ${stats.chunkCount}`)
268
+ console.log(` Embeddings: ${stats.embeddingCount}`)
269
+ console.log(` Time elapsed: ${(elapsedMs / 1000).toFixed(1)}s`)
270
+ console.log(` Database size: ${formatBytes(stats.dbSizeBytes)}`)
271
+ }
272
+
273
+ function formatBytes(bytes: number): string {
274
+ if (bytes < 1024) return `${bytes} B`
275
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
276
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
277
+ }
278
+
279
+ commands.register('embed', {
280
+ description: 'Generate embeddings for collection documents',
281
+ help: `# cnotes embed
282
+
283
+ Generate or update vector embeddings for all documents in the collection. Required before using \`cnotes search\`.
284
+
285
+ ## Usage
286
+
287
+ \`\`\`
288
+ cnotes embed [options]
289
+ \`\`\`
290
+
291
+ ## Options
292
+
293
+ | Option | Default | Description |
294
+ |--------|---------|-------------|
295
+ | \`--force\` | \`false\` | Re-embed everything (ignore content hashes) |
296
+ | \`--provider\` | \`openai\` | Embedding provider: \`openai\` or \`local\` |
297
+ | \`--status\` | \`false\` | Show index health without embedding |
298
+ | \`--local\` | \`false\` | Use local embeddings; auto-installs node-llama-cpp if not found |
299
+ | \`--install-local\` | \`false\` | Only install node-llama-cpp (no embedding), then exit |
300
+ | \`--contentFolder\` | | Path to content folder |
301
+
302
+ ## Examples
303
+
304
+ \`\`\`bash
305
+ # Generate/update embeddings (uses OpenAI by default)
306
+ cnotes embed
307
+
308
+ # Re-embed everything from scratch
309
+ cnotes embed --force
310
+
311
+ # Use local embeddings (auto-installs node-llama-cpp if needed)
312
+ cnotes embed --local
313
+
314
+ # Only install node-llama-cpp without embedding
315
+ cnotes embed --install-local
316
+
317
+ # Show index health
318
+ cnotes embed --status
319
+ \`\`\`
320
+ `,
321
+ argsSchema,
322
+ handler,
323
+ })
@@ -1,24 +1,58 @@
1
- import { defineCommand } from "citty";
2
- import { loadCollection } from "../load-collection";
3
-
4
- export default defineCommand({
5
- meta: {
6
- name: "export",
7
- description: "Export collection as JSON",
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
- const data = await collection.export();
22
- console.log(JSON.stringify(data, null, 2));
23
- },
24
- });
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
+ const data = await collection.export()
15
+ console.log(JSON.stringify(data, null, 2))
16
+ }
17
+
18
+ commands.register('export', {
19
+ description: 'Export collection as JSON',
20
+ help: `# cnotes export
21
+
22
+ Export the entire collection as a JSON object. Each document includes its path ID, title, frontmatter, content, and model info.
23
+
24
+ ## Usage
25
+
26
+ \`\`\`
27
+ cnotes export [options]
28
+ \`\`\`
29
+
30
+ ## Options
31
+
32
+ | Option | Description |
33
+ |--------|-------------|
34
+ | \`--contentFolder\` | Path to content folder |
35
+
36
+ ## Output
37
+
38
+ Writes JSON to stdout. Pipe to a file or another tool.
39
+
40
+ ## Examples
41
+
42
+ \`\`\`bash
43
+ # Export to stdout
44
+ cnotes export
45
+
46
+ # Save to a file
47
+ cnotes export > backup.json
48
+
49
+ # Export a specific content folder
50
+ cnotes export --contentFolder ./docs > docs.json
51
+
52
+ # Pipe to jq for filtering
53
+ cnotes export | jq '.[] | select(.meta.status == "published")'
54
+ \`\`\`
55
+ `,
56
+ argsSchema,
57
+ handler,
58
+ })