contentbase 0.0.1 → 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 (97) 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 +406 -13
  7. package/bun.lock +45 -6
  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 +14 -4
  14. package/public/web-demo/index.html +813 -0
  15. package/scripts/examples/01-collection-setup.ts +46 -0
  16. package/scripts/examples/02-querying.ts +67 -0
  17. package/scripts/examples/03-sections.ts +36 -0
  18. package/scripts/examples/04-relationships.ts +54 -0
  19. package/scripts/examples/05-document-api.ts +54 -0
  20. package/scripts/examples/06-extract-sections.ts +55 -0
  21. package/scripts/examples/07-validation.ts +46 -0
  22. package/scripts/examples/08-serialization.ts +51 -0
  23. package/scripts/examples/lib/format.ts +87 -0
  24. package/scripts/examples/lib/setup.ts +21 -0
  25. package/scripts/examples/run-all.ts +43 -0
  26. package/showcases/node_modules/.cache/.repl_history +3 -0
  27. package/showcases/vinyl-collection/artists/nina-simone.mdx +1 -1
  28. package/src/__tests__/semantic-search.integration.test.ts +284 -0
  29. package/src/api/endpoints/actions.ts +34 -0
  30. package/src/api/endpoints/doc.ts +187 -0
  31. package/src/api/endpoints/docs-index.ts +26 -0
  32. package/src/api/endpoints/document.ts +171 -0
  33. package/src/api/endpoints/documents.ts +71 -0
  34. package/src/api/endpoints/inspect.ts +16 -0
  35. package/src/api/endpoints/models.ts +10 -0
  36. package/src/api/endpoints/query.ts +88 -0
  37. package/src/api/endpoints/root.ts +7 -0
  38. package/src/api/endpoints/search-reindex.ts +65 -0
  39. package/src/api/endpoints/search-status.ts +55 -0
  40. package/src/api/endpoints/search.ts +104 -0
  41. package/src/api/endpoints/semantic-search.ts +120 -0
  42. package/src/api/endpoints/text-search.ts +63 -0
  43. package/src/api/endpoints/validate.ts +34 -0
  44. package/src/api/helpers.ts +97 -0
  45. package/src/base-model.ts +12 -0
  46. package/src/cli/commands/action.ts +82 -44
  47. package/src/cli/commands/console.ts +124 -0
  48. package/src/cli/commands/create.ts +179 -53
  49. package/src/cli/commands/embed.ts +323 -0
  50. package/src/cli/commands/export.ts +58 -24
  51. package/src/cli/commands/extract.ts +174 -0
  52. package/src/cli/commands/help.ts +81 -0
  53. package/src/cli/commands/index.ts +17 -0
  54. package/src/cli/commands/init.ts +72 -48
  55. package/src/cli/commands/inspect.ts +56 -46
  56. package/src/cli/commands/mcp.ts +1219 -0
  57. package/src/cli/commands/search.ts +285 -0
  58. package/src/cli/commands/serve.ts +348 -0
  59. package/src/cli/commands/summary.ts +60 -0
  60. package/src/cli/commands/teach.ts +86 -0
  61. package/src/cli/commands/text-search.ts +134 -0
  62. package/src/cli/commands/validate.ts +126 -64
  63. package/src/cli/index.ts +88 -19
  64. package/src/cli/load-collection.ts +144 -17
  65. package/src/cli/registry.ts +28 -0
  66. package/src/collection.ts +455 -6
  67. package/src/define-model.ts +104 -7
  68. package/src/document.ts +47 -1
  69. package/src/extract-sections.ts +222 -0
  70. package/src/index.ts +20 -2
  71. package/src/model-instance.ts +35 -5
  72. package/src/node-shortcuts.ts +1 -1
  73. package/src/query/collection-query.ts +96 -9
  74. package/src/query/index.ts +7 -0
  75. package/src/query/query-dsl.ts +259 -0
  76. package/src/relationships/has-many.ts +39 -0
  77. package/src/relationships/index.ts +7 -2
  78. package/src/section.ts +2 -0
  79. package/src/types.ts +24 -2
  80. package/src/utils/index.ts +1 -0
  81. package/src/utils/match-pattern.ts +65 -0
  82. package/src/validator.ts +18 -1
  83. package/test/collection.test.ts +118 -2
  84. package/test/extract-sections.test.ts +356 -0
  85. package/test/fixtures/sdlc/MODELS.md +106 -0
  86. package/test/fixtures/sdlc/SKILL.md +404 -0
  87. package/test/fixtures/sdlc/index.ts +9 -0
  88. package/test/fixtures/sdlc/models.ts +8 -6
  89. package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +20 -0
  90. package/test/fixtures/sdlc/templates/epic.md +23 -0
  91. package/test/fixtures/sdlc/templates/story.md +19 -0
  92. package/test/pattern.test.ts +191 -0
  93. package/test/query-dsl.test.ts +431 -0
  94. package/test/query.test.ts +29 -0
  95. package/test/relationships.test.ts +130 -0
  96. package/test/section.test.ts +65 -4
  97. package/test/table-of-contents.test.ts +135 -0
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod'
2
+ import matter from 'gray-matter'
3
+ import { resolveModelDef } from '../helpers.js'
4
+
5
+ export const path = '/api/documents'
6
+ export const description = 'List or create documents'
7
+ export const tags = ['documents']
8
+
9
+ export const getSchema = z.object({
10
+ model: z.string().optional(),
11
+ })
12
+
13
+ export async function get(params: any, ctx: any) {
14
+ const collection = ctx.container._contentbaseCollection
15
+
16
+ let ids = collection.available as string[]
17
+
18
+ if (params.model) {
19
+ const def = resolveModelDef(collection, params.model)
20
+ if (!def) {
21
+ ctx.response.status(400)
22
+ return { error: `Unknown model: ${params.model}` }
23
+ }
24
+ const prefix = (def as any).prefix + '/'
25
+ ids = ids.filter((id: string) => id.startsWith(prefix))
26
+ }
27
+
28
+ return ids.map((id: string) => {
29
+ const doc = collection.document(id)
30
+ return { id, title: doc.title, meta: doc.meta, size: doc.size, createdAt: doc.createdAt, updatedAt: doc.updatedAt }
31
+ })
32
+ }
33
+
34
+ export const postSchema = z.object({
35
+ pathId: z.string(),
36
+ title: z.string(),
37
+ meta: z.record(z.string(), z.any()).optional(),
38
+ model: z.string().optional(),
39
+ })
40
+
41
+ export async function post(params: any, ctx: any) {
42
+ if (ctx.container._contentbaseReadOnly) {
43
+ ctx.response.status(403)
44
+ return { error: 'Server is running in read-only mode' }
45
+ }
46
+
47
+ const collection = ctx.container._contentbaseCollection
48
+
49
+ if (collection.available.includes(params.pathId)) {
50
+ ctx.response.status(409)
51
+ return { error: `Document already exists: ${params.pathId}` }
52
+ }
53
+
54
+ const def = params.model
55
+ ? resolveModelDef(collection, params.model)
56
+ : collection.findModelDefinition(params.pathId)
57
+
58
+ const metaData = { ...((def as any)?.defaults || {}), ...(params.meta || {}) }
59
+
60
+ const sectionHeadings = def
61
+ ? Object.values((def as any).sections || {}).map((s: any) => `## ${s.heading}\n\n`)
62
+ : []
63
+
64
+ const body = [`# ${params.title}`, '', ...sectionHeadings].join('\n')
65
+ const content = matter.stringify(body, metaData)
66
+
67
+ await collection.saveItem(params.pathId, { content })
68
+
69
+ ctx.response.status(201)
70
+ return { created: params.pathId, model: def ? (def as any).name : null, meta: metaData }
71
+ }
@@ -0,0 +1,16 @@
1
+ import { buildSchemaJSON } from '../helpers.js'
2
+
3
+ export const path = '/api/inspect'
4
+ export const description = 'Collection overview — models, document count, actions'
5
+ export const tags = ['collection']
6
+
7
+ export async function get(_params: any, ctx: any) {
8
+ const collection = ctx.container._contentbaseCollection
9
+ const schema = buildSchemaJSON(collection)
10
+ return {
11
+ rootPath: collection.rootPath,
12
+ documentCount: collection.available.length,
13
+ models: Object.values(schema),
14
+ actions: collection.availableActions,
15
+ }
16
+ }
@@ -0,0 +1,10 @@
1
+ import { buildSchemaJSON } from '../helpers.js'
2
+
3
+ export const path = '/api/models'
4
+ export const description = 'All model definitions with schemas, sections, and relationships'
5
+ export const tags = ['collection']
6
+
7
+ export async function get(_params: any, ctx: any) {
8
+ const collection = ctx.container._contentbaseCollection
9
+ return buildSchemaJSON(collection)
10
+ }
@@ -0,0 +1,88 @@
1
+ import { z } from 'zod'
2
+ import { resolveModelDef } from '../helpers.js'
3
+ import { queryDSLSchema, executeQueryDSL } from '../../query/query-dsl.js'
4
+
5
+ export const path = '/api/query'
6
+ export const description = 'Query model instances with filtering'
7
+ export const tags = ['query']
8
+
9
+ export const getSchema = z.object({
10
+ model: z.string(),
11
+ where: z.string().optional(),
12
+ select: z.string().optional(),
13
+ related: z.string().optional(),
14
+ })
15
+
16
+ export const postSchema = queryDSLSchema
17
+
18
+ export async function get(params: any, ctx: any) {
19
+ const collection = ctx.container._contentbaseCollection
20
+
21
+ const def = resolveModelDef(collection, params.model)
22
+ if (!def) {
23
+ ctx.response.status(400)
24
+ return { error: `Unknown model: ${params.model}` }
25
+ }
26
+
27
+ let q = collection.query(def)
28
+
29
+ if (params.where) {
30
+ let conditions: any[]
31
+ try {
32
+ conditions = JSON.parse(params.where)
33
+ } catch {
34
+ ctx.response.status(400)
35
+ return { error: 'Invalid JSON in where parameter' }
36
+ }
37
+
38
+ for (const condition of conditions) {
39
+ const { path: fieldPath, operator = 'eq', value } = condition
40
+ switch (operator) {
41
+ case 'eq': q = q.where(fieldPath, value); break
42
+ case 'in': q = q.whereIn(fieldPath, value); break
43
+ case 'notIn': q = q.whereNotIn(fieldPath, value); break
44
+ case 'gt': q = q.whereGt(fieldPath, value); break
45
+ case 'lt': q = q.whereLt(fieldPath, value); break
46
+ case 'gte': q = q.whereGte(fieldPath, value); break
47
+ case 'lte': q = q.whereLte(fieldPath, value); break
48
+ case 'contains': q = q.whereContains(fieldPath, value); break
49
+ case 'startsWith': q = q.whereStartsWith(fieldPath, value); break
50
+ case 'endsWith': q = q.whereEndsWith(fieldPath, value); break
51
+ case 'regex': q = q.whereRegex(fieldPath, value); break
52
+ case 'exists': q = q.whereExists(fieldPath); break
53
+ case 'notExists': q = q.whereNotExists(fieldPath); break
54
+ }
55
+ }
56
+ }
57
+
58
+ const results = await q.fetchAll()
59
+ const selectFields = params.select ? params.select.split(',').map((s: string) => s.trim()) : null
60
+ const relatedFields = params.related ? params.related.split(',').map((s: string) => s.trim()) : undefined
61
+
62
+ return results.map((instance: any) => {
63
+ const json = instance.toJSON({ related: relatedFields })
64
+ if (selectFields && selectFields.length > 0) {
65
+ const filtered: Record<string, any> = {}
66
+ for (const key of selectFields) {
67
+ if (key in json) filtered[key] = json[key]
68
+ else if (key.startsWith('meta.') && json.meta) {
69
+ filtered[key] = json.meta[key.slice(5)]
70
+ }
71
+ }
72
+ return filtered
73
+ }
74
+ return json
75
+ })
76
+ }
77
+
78
+ export async function post(params: any, ctx: any) {
79
+ const collection = ctx.container._contentbaseCollection
80
+
81
+ try {
82
+ const dsl = queryDSLSchema.parse(params)
83
+ return await executeQueryDSL(collection, dsl)
84
+ } catch (error: any) {
85
+ ctx.response.status(400)
86
+ return { error: error.message }
87
+ }
88
+ }
@@ -0,0 +1,7 @@
1
+ export const path = '/'
2
+ export const description = 'Redirect to docs table of contents when no static index.html exists'
3
+ export const tags = ['docs']
4
+
5
+ export async function get(_params: any, ctx: any) {
6
+ ctx.response.redirect('/docs/')
7
+ }
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod'
2
+ import pathModule from 'node:path'
3
+
4
+ export const path = '/api/search/reindex'
5
+ export const description = 'Trigger search index rebuild'
6
+ export const tags = ['mutation']
7
+
8
+ export const postSchema = z.object({
9
+ pathIds: z.array(z.string()).optional(),
10
+ force: z.boolean().optional(),
11
+ })
12
+
13
+ export async function post(params: any, ctx: any) {
14
+ const collection = ctx.container._contentbaseCollection
15
+ const rootPath = collection.rootPath
16
+
17
+ const { SemanticSearch } = await import('@soederpop/luca/agi')
18
+ if (!ctx.container.features.available.includes('semanticSearch')) {
19
+ SemanticSearch.attach(ctx.container)
20
+ }
21
+
22
+ const dbPath = pathModule.join(rootPath, '.contentbase/search.sqlite')
23
+ const ss = ctx.container.feature('semanticSearch', { dbPath })
24
+ await ss.initDb()
25
+
26
+ if (params.pathIds) {
27
+ await ss.reindex(params.pathIds)
28
+ } else if (params.force) {
29
+ await ss.reindex()
30
+ }
31
+
32
+ // Collect and re-index documents
33
+ const docs: any[] = []
34
+ const targetIds = params.pathIds || collection.available
35
+
36
+ for (const pathId of targetIds) {
37
+ if (!collection.available.includes(pathId)) continue
38
+ const doc = collection.document(pathId)
39
+ const modelDef = collection.findModelDefinition(pathId)
40
+
41
+ docs.push({
42
+ pathId,
43
+ model: modelDef?.name ?? undefined,
44
+ title: doc.title,
45
+ slug: doc.slug,
46
+ meta: doc.meta,
47
+ content: doc.content,
48
+ })
49
+ }
50
+
51
+ const toIndex = params.force ? docs : docs.filter((d: any) => ss.needsReindex(d))
52
+
53
+ if (toIndex.length > 0) {
54
+ await ss.indexDocuments(toIndex)
55
+ }
56
+
57
+ ss.removeStale(collection.available)
58
+
59
+ const stats = ss.getStats()
60
+ return {
61
+ reindexed: toIndex.length,
62
+ total: docs.length,
63
+ ...stats,
64
+ }
65
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod'
2
+ import pathModule from 'node:path'
3
+ import { existsSync, readdirSync } from 'node:fs'
4
+
5
+ export const path = '/api/search/status'
6
+ export const description = 'Search index health and statistics'
7
+ export const tags = ['query']
8
+
9
+ export const getSchema = z.object({})
10
+
11
+ export async function get(_params: any, ctx: any) {
12
+ const collection = ctx.container._contentbaseCollection
13
+ const rootPath = collection.rootPath
14
+ const dbDir = pathModule.join(rootPath, '.contentbase')
15
+
16
+ const hasIndex = existsSync(dbDir) && (() => {
17
+ try {
18
+ const files = readdirSync(dbDir) as string[]
19
+ return files.some((f: string) => f.startsWith('search.') && f.endsWith('.sqlite'))
20
+ } catch {
21
+ return false
22
+ }
23
+ })()
24
+
25
+ if (!hasIndex) {
26
+ return {
27
+ exists: false,
28
+ documentCount: 0,
29
+ chunkCount: 0,
30
+ embeddingCount: 0,
31
+ lastIndexedAt: null,
32
+ provider: null,
33
+ model: null,
34
+ dimensions: 0,
35
+ dbSizeBytes: 0,
36
+ collectionDocumentCount: collection.available.length,
37
+ }
38
+ }
39
+
40
+ const { SemanticSearch } = await import('@soederpop/luca/agi')
41
+ if (!ctx.container.features.available.includes('semanticSearch')) {
42
+ SemanticSearch.attach(ctx.container)
43
+ }
44
+
45
+ const dbPath = pathModule.join(rootPath, '.contentbase/search.sqlite')
46
+ const ss = ctx.container.feature('semanticSearch', { dbPath })
47
+ await ss.initDb()
48
+
49
+ const stats = ss.getStats()
50
+ return {
51
+ exists: true,
52
+ ...stats,
53
+ collectionDocumentCount: collection.available.length,
54
+ }
55
+ }
@@ -0,0 +1,104 @@
1
+ import { z } from 'zod'
2
+ import pathModule from 'node:path'
3
+ import { existsSync, readdirSync } from 'node:fs'
4
+
5
+ export const path = '/api/search'
6
+ export const description = 'Search across collection documents using keyword, semantic, or hybrid modes'
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
+ async function doSearch(ss: any, query: string, mode: string, options: any) {
39
+ switch (mode) {
40
+ case 'keyword':
41
+ return ss.search(query, options)
42
+ case 'vector':
43
+ return ss.vectorSearch(query, options)
44
+ case 'hybrid':
45
+ default:
46
+ return ss.hybridSearch(query, options)
47
+ }
48
+ }
49
+
50
+ // ── GET /api/search ──────────────────────────────────────────────────
51
+
52
+ export const getSchema = z.object({
53
+ q: z.string(),
54
+ mode: z.string().optional(),
55
+ model: z.string().optional(),
56
+ limit: z.string().optional(),
57
+ })
58
+
59
+ export async function get(params: any, ctx: any) {
60
+ const collection = ctx.container._contentbaseCollection
61
+ const rootPath = collection.rootPath
62
+
63
+ if (!hasSearchIndex(rootPath)) {
64
+ ctx.response.status(400)
65
+ return { error: 'No search index found. Run: cnotes embed' }
66
+ }
67
+
68
+ const ss = await getSemanticSearch(ctx.container, rootPath)
69
+ const mode = params.mode || 'hybrid'
70
+ const limit = params.limit ? parseInt(params.limit, 10) : 10
71
+ const searchOptions = { limit, model: params.model }
72
+
73
+ return doSearch(ss, params.q, mode, searchOptions)
74
+ }
75
+
76
+ // ── POST /api/search ─────────────────────────────────────────────────
77
+
78
+ export const postSchema = z.object({
79
+ query: z.string(),
80
+ mode: z.enum(['hybrid', 'keyword', 'vector']).optional(),
81
+ model: z.string().optional(),
82
+ limit: z.number().optional(),
83
+ where: z.record(z.string(), z.any()).optional(),
84
+ })
85
+
86
+ export async function post(params: any, ctx: any) {
87
+ const collection = ctx.container._contentbaseCollection
88
+ const rootPath = collection.rootPath
89
+
90
+ if (!hasSearchIndex(rootPath)) {
91
+ ctx.response.status(400)
92
+ return { error: 'No search index found. Run: cnotes embed' }
93
+ }
94
+
95
+ const ss = await getSemanticSearch(ctx.container, rootPath)
96
+ const mode = params.mode || 'hybrid'
97
+ const searchOptions = {
98
+ limit: params.limit || 10,
99
+ model: params.model,
100
+ where: params.where,
101
+ }
102
+
103
+ return doSearch(ss, params.query, mode, searchOptions)
104
+ }
@@ -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
+ }