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.
- package/.mcp.json +9 -0
- package/CLI.md +593 -0
- package/MCP-DESCRIPTIONS-REVIEW.md +122 -0
- package/MCP-SERVER-SPEC.md +453 -0
- package/PRIMER.md +540 -0
- package/README.md +289 -13
- package/bun.lock +43 -4
- package/dist/cnotes +0 -0
- package/docs/README.md +110 -0
- package/docs/TABLE-OF-CONTENTS.md +7 -0
- package/docs/models.ts +38 -0
- package/models.ts +38 -0
- package/package.json +12 -3
- package/public/web-demo/index.html +813 -0
- package/showcases/node_modules/.cache/.repl_history +3 -0
- package/showcases/vinyl-collection/artists/nina-simone.mdx +1 -1
- package/src/__tests__/semantic-search.integration.test.ts +284 -0
- package/src/api/endpoints/actions.ts +34 -0
- package/src/api/endpoints/doc.ts +187 -0
- package/src/api/endpoints/docs-index.ts +26 -0
- package/src/api/endpoints/document.ts +171 -0
- package/src/api/endpoints/documents.ts +71 -0
- package/src/api/endpoints/inspect.ts +16 -0
- package/src/api/endpoints/models.ts +10 -0
- package/src/api/endpoints/query.ts +88 -0
- package/src/api/endpoints/root.ts +7 -0
- package/src/api/endpoints/search-reindex.ts +65 -0
- package/src/api/endpoints/search-status.ts +55 -0
- package/src/api/endpoints/search.ts +104 -0
- package/src/api/endpoints/semantic-search.ts +120 -0
- package/src/api/endpoints/text-search.ts +63 -0
- package/src/api/endpoints/validate.ts +34 -0
- package/src/api/helpers.ts +97 -0
- package/src/base-model.ts +12 -0
- package/src/cli/commands/action.ts +82 -44
- package/src/cli/commands/console.ts +124 -0
- package/src/cli/commands/create.ts +179 -53
- package/src/cli/commands/embed.ts +323 -0
- package/src/cli/commands/export.ts +58 -24
- package/src/cli/commands/extract.ts +174 -0
- package/src/cli/commands/help.ts +81 -0
- package/src/cli/commands/index.ts +17 -0
- package/src/cli/commands/init.ts +72 -48
- package/src/cli/commands/inspect.ts +56 -46
- package/src/cli/commands/mcp.ts +1219 -0
- package/src/cli/commands/search.ts +285 -0
- package/src/cli/commands/serve.ts +348 -0
- package/src/cli/commands/summary.ts +60 -0
- package/src/cli/commands/teach.ts +86 -0
- package/src/cli/commands/text-search.ts +134 -0
- package/src/cli/commands/validate.ts +126 -64
- package/src/cli/index.ts +88 -19
- package/src/cli/load-collection.ts +144 -17
- package/src/cli/registry.ts +28 -0
- package/src/collection.ts +361 -10
- package/src/define-model.ts +101 -4
- package/src/document.ts +47 -1
- package/src/extract-sections.ts +1 -1
- package/src/index.ts +14 -2
- package/src/model-instance.ts +35 -5
- package/src/node-shortcuts.ts +1 -1
- package/src/query/collection-query.ts +96 -9
- package/src/query/index.ts +7 -0
- package/src/query/query-dsl.ts +259 -0
- package/src/relationships/has-many.ts +39 -0
- package/src/relationships/index.ts +7 -2
- package/src/section.ts +2 -0
- package/src/types.ts +23 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/match-pattern.ts +65 -0
- package/src/validator.ts +18 -1
- package/test/collection.test.ts +118 -2
- package/test/fixtures/sdlc/MODELS.md +106 -0
- package/test/fixtures/sdlc/SKILL.md +404 -0
- package/test/fixtures/sdlc/index.ts +9 -0
- package/test/fixtures/sdlc/models.ts +8 -6
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +20 -0
- package/test/fixtures/sdlc/templates/epic.md +23 -0
- package/test/fixtures/sdlc/templates/story.md +19 -0
- package/test/pattern.test.ts +191 -0
- package/test/query-dsl.test.ts +431 -0
- package/test/query.test.ts +29 -0
- package/test/relationships.test.ts +130 -0
- package/test/section.test.ts +61 -0
- 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 {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
})
|